标签:ams 继承 实现 == rgs title 本地 性能 func
定义 先看一下文档中的注释
String对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享。
1 2 3 4 5 6 7 8 9 10 11 12 13 public final class implements java .io .Serializable , Comparable <>, CharSequence { private final char value[]; private int hash; private static final long serialVersionUID = -6849794470754667710L ; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0 ]; ...... }
从源码中可以看出,String是被final修饰的,说明该类不能被继承。并且实现了CharSequence
, Comparable
, Serializable
接口。Serializable
接口用于实现String的序列化和反序列化操作;Comparable
接口用于实现字符串的比较操作;CharSequence
是字符串类的父接口,StringBuffer和StringBuilder都继承自该接口。
value字段是实现String类的底层数组,用于存储字符串内容。final修饰基本数据类型,那么在运行期间其内容不可变,如果修饰的是引用类型,那么引用的对象(包括数组)运行期地址不可变,但是对象的内容是可以改变的。 hash字段用于缓存String对象的hash值,防止多次计算hash造成的时间损耗。 因为String实现了Serializable
接口,所以需要serialVersionUID字段用来在String反序列化时,通过对比字节流中的serialVersionUID和本地实体类中的serialVersionUID是否一致,如果相同就可以进行反序列化,否则就会抛出InvalidCastException
异常。 构造方法 空参构造方法 1 2 3 public () { this .value = "" .value; }
该构造方法会创建一个空的字符序列,因为字符串的不可变对象,之后对象的赋值会指向新的字符串,因此使用这种构造方法会多创建一个无用对象。
使用字符串类型的对象初始化 1 2 3 4 public (String original) { this .value = original.value; this .hash = original.hash; }
直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后就不可改变,所以也就不用担心源String的值会影响到目标String的值。
使用字符数组初始化 1 2 3 4 5 6 7 8 9 10 public (char value[]) { this .value = Arrays.copyOf(value, value.length); } public String (char value[], int offset, int count) { ...... this .value = Arrays.copyOfRange(value, offset, offset+count); }
使用字节数组初始化 在Java中,String实例保存有一个char[]字符数组,char[]字符数组是以Unicode编码方式存储的,String和char为内存形式,byte是网络传输或存储的序列化形式,所以在很多传输和存储过程中需要将byte[]数组和String进行相互转化。字节和字符自检的转化需要指定编码,不然很可能会出现乱码。String提供了多种字节数组的重载构造函数:
1 2 3 4 5 6 public String (byte bytes[], int offset, int length, String charsetName) public String (byte bytes[], int offset, int length, Charset charset) public String (byte bytes[], String charsetName) public String (byte bytes[], Charset charset) public String (byte bytes[], int offset, int length) public String (byte bytes[])
如果我们在使用 byte[] 构造 String 的时候,如果指定了charsetName
或者charset
参数的话,那么就会使用 StringCoding.decode
方法进行解码,使用的解码的字符集就是我们指定的 charsetName
或者 charset
。如果没有指定解码使用的字符集的话,那么StringCoding
的decode
方法首先会使用系统的默认编码格式(ISO-8859-1)。
使用StringBuffer和StringBuilder初始化 1 2 3 4 5 6 7 8 9 public String (StringBuffer buffer) { synchronized (buffer) { this .value = Arrays.copyOf(buffer.getValue(), buffer.length()); } } public String (StringBuilder builder) { this .value = Arrays.copyOf(builder.getValue(), builder.length()); }
因为StringBuilder不是线程安全的,所以在初始化时不需要加锁;而StringBuilder则需要加锁。我们一般使用StringBuffer和StringBuilder的toString
方法来获取String,而很少使用String的这两种构造方法。
特殊的构造方法 String除了提供了很多共有的构造方法,还提供了一个保护类型的构造方法:
1 2 3 4 String(char [] value, boolean share) { this .value = value; }
该方法和String(char[] value)
有两点区别:
该方法多了一个参数:boolean share,但该参数在函数中并没有使用。因此加入该参数的目的只是为了区分String(char[] value)
方法,只有参数不同才能被重载。 该方法直接修改了value数组的引用,也就是说共享char[] value数组。而String(char[] value)
通过Arrays.copyOf
将参数数组内容复制到String中。 使用这种方式的的优点很明显:
经典方法技巧 equals方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public boolean equals (Object anObject) { if (this == anObject) { return true ; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0 ; while (n-- != 0 ) { if (v1[i] != v2[i]) return false ; i++; } return true ; } } return false ; }
先判断两个对象的地址是否相等 在判断是否是String类型 如果都是String类型,就先比较长度是否相等,然后再逐一比较值。值的比较采取了短路操作,发现不一样的就返回false compareTo方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int compareTo (String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0 ; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; }
从0开始逐一判断字符是否相等,若不相等则做差运算,巧妙的避免了三种判断情况。若字符都相等,接直接返回长度差值。所以在判断两个字符串大小时,使用是否为正数/负数/0,而不是通过1//-1/0判断。
hashCode方法 1 2 3 4 5 6 7 8 9 10 11 12 public int hashCode () { int h = hash; if (h == 0 && value.length > 0 ) { char val[] = value; for (int i = 0 ; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
若第一次调用hashCode
方法且value数组长度大于0,则通过算法s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
计算hash值。hash值很多时候用来判断两个对象的值是否相等,所以需要尽可能的避免冲突。选择31是因为31是一个素数,且i * 31
可以通过(i << 5) - 1
来提高运算速度,现在很多虚拟机都有做相关优化。 hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。 返回缓存的hash值。 replaceFirst、replaceAll,replace区别 1 2 3 4 String replaceFirst (String regex, String replacement) String replaceAll (String regex, String replacement) String replace (Char Sequencetarget, Char Sequencereplacement) String replace (CharSequence target, CharSequence replacement)
replace
的参数是char和CharSequence,既可以支持字符的替换,也支持字符串的替换replaceFirst
和replaceAll
的参数是regex,基于正则表达式替换replace
和replaceAll
方法会替换字符串中的全部字符或字符串,replaceFirst
只替换第一次出现的字符或字符串copyValueOf和valueOf String的底层是通过char[]实现的,早期的String构造器的实现并不会拷贝数组。为了防止char[]数组被外部修改,提供了copyValueOf
方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String在构造器中就通过拷贝新数组实现,所以这两个方法在本质上已经没区别了。
valueOf()
有很多种重载形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static String valueOf (boolean b) { return b ? "true" : "false" ; } public static String valueOf (char c) { char data[] = {c}; return new String(data, true ); } public static String valueOf (int i) { return Integer.toString(i); } public static String valueOf (long l) { return Long.toString(l); } public static String valueOf (float f) { return Float.toString(f); } public static String valueOf (double d) { return Double.toString(d); }
底层调用了基本数据类型的toString()
方法。
intern方法 1 public native String intern () ;
intern
方法是Native调用,它的作用是每当定义一个字符字面量,字面量进行字符串连接或final的String字面量初始化的变量的连接,都会检查常量池中是否有对应的字符串,如果有就不创建新的字符串,而是返回指向常量池对应字符串的引用。所有通过new String(str)
方式创建的对象都会保存在堆中,而不是常量区。普通变量的连接,由于不能在编译期确定下来,所以不会储存在常量区。
其他方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int length () boolean isEmpty () char charAt (int index) char [] toCharArray () void trim () String toUpperCase () String toLowerCase () String concat (String str) String replace (char oldChar, char newChar) boolean matches (String regex) boolean contains (CharSequence s) String[] split (String regex, int limit) String[] split (String regex) boolean equals (Object anObject) boolean contentEquals (String Buffersb) boolean contentEquals (Char Sequencecs) boolean equalsIgnoreCase (String anotherString) int compareTo (String anotherString) int compareToIgnoreCase (String str) boolean regionMatches (int toffset, String other, int ooffset, int len) boolean regionMatches (boolean ignoreCase, int toffset, String other, int ooffset, int len)
String对“+”的重载 Java不支持运算符重载,但是String可以通过+
来连接两个字符串。那么java是如何实现对+
的重载的呢?
1 2 3 4 5 6 public class Main { public static void main (String[] args) { String str1 = "windy" ; string str2 = str1 + "lee" ; } }
反编译Main.java,执行命令javap -c Main
,输出结果:
我们看到了StringBuilder,还有windy和lee,以及调用了StringBuilder的append
和toString
方法。既然编译器已经在底层为我们进行了优化,那么为什么还要提倡我们用StringBuilder呢?
我们注意到在第3行代码,new了一个StringBuilder对象,如果实在一个循环里面,我们使用”+”号就会创建多个StringBuilder的对象。但是编译器事先不知道我们StringBuilder的长度,并不能事先分配好缓冲区,会加大内存的开销,而且使用重载的时候根据java的内存分配也会创建多个对象。
switch对字符串支持的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) { String str = "world" ; switch (str) { case "hello" : System.out.println("hello" ); break ; case "world" : System.out.println("world" ); break ; default : break ; } } }
反编译之后得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String args[]) { String str = "world" ; String s; switch ((s = str).hashCode()) { case 99162322 : if (s.equals("hello" )) System.out.println("hello" ); break ; case 113318802 : if (s.equals("world" )) System.out.println("world" ); break ; default : break ; } }
首先调用String的hashCode
方法,拿到相应的Code,通过这个code然后给每个case唯一的标识 判断时先获取对象的hashCode,进入对应的case分支 通过equals
方法进行安全检查,这个检查是必要的,因为哈希可能会发生冲突 switch只支持整型,其他数据类型都是转换成整型之后在使用switch的
总结 String被final修饰,一旦被创建,无法修改
final保证value不会指向其他的数组,但不保证数组内容不可修改 private属性保证不可在类外访问数组,也就不能改变其内容 String内部没有改变value内容的函数,保证String不可变 String声明为final杜绝了通过集成的方法添加新的函数 基于数组的构造方法,会拷贝数组元素,避免了通过外部引用修改value的情况 用String构造其他可变对象时,返回的数组的拷贝 final只在编译期有效,在运行期间无效,因此可以通过反射改变value引用的对象。反射虽然改变了s的内容,并没有创建新的对象。而且由于String缓存了hash值,所以通过反射改变字符数组内容,hashCode
返回值不会自动更新。
String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果你需要一个可修改的字符串,应该使用StringBuilder或者 StringBuffer。
如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。
原文:大专栏 String源码解析
String源码解析
标签:ams 继承 实现 == rgs title 本地 性能 func
原文地址:https://www.cnblogs.com/wangziqiang123/p/11643619.html