字符串类
字符串类主要是指String、StringBuffer和StringBuilder,从源码注释可以看到,String和StringBuffer都是jdk1.0就有的,而StringBuilder则是jdk1.5才有。
一般来说,最常用的是String,是不可变的,然后是可变的StringBuilder和StringBuffer,其中StringBuffer是线程安全的,因为里边的方法都是加了synchronized关键字的。
StringBuilder和StringBuffer都是继承自AbstractStringBuilder类,里边很多方法也都是共用的这个抽象类你的逻辑,所以除了是否线程安全,其他的基本都一样。
理解字符串不能被继承
上述三个字符串类都不能被继承,原因是这几个类定义都是final的,如:
1
| public final class String
|
fainal修饰类的时候不能被继承,修饰方法的时候不能被重写,修饰基础类型变量不能改变值,修饰引用类型变量,不能改变引用。
理解字符串是不可变的
String是不可变的,指的是一个字符串变量一旦创建后,所指向的引用的内容不能再变,如果要改变这个变量的值,实际上会同时改变变量的引用。
StringBuilder和StringBuffer可变,指的是创建之后可以在这个引用不变的情况下改变里边的值,而实际上是改变的这个引用对象里边的数组的值,在jdk1.8中指的就是字符数组,jdk12指的是byte数组。
理解String中的equals
equals方法是Object类中的方法,在不重写的情况下实际就是直接比较的两个对象的引用,Object中equals源码如下:
1 2 3
| public boolean equals(Object obj) { return (this == obj); }
|
String中重写了equals方法,所以在String中的equals方法不再是直接比较String对象的引用,jdk1.8里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; }
|
上边的逻辑中可以看到,当两个对象引用相同时,就会返回true,当引用不同时,会先判断类型然后进行强转,之后再对两个字符串底层的字符数组元素进行遍历依次比较大小,所有元素都相等时返回true。
注:在jdk12中,String的equals进行了重写,逻辑进行了很大的改变,逻辑也没有上边这么直观了,根本原因好像是底层存储变了,jdk1.8底层是字符串数组,而jdk12则是byte数组,这种改动具体是从jdk哪个版本开始的,暂未细究。
从源码看compareTo
compareTo是Comparable接口中的方法,用来比较两个对象大小,String类实现了这个接口并实现了compareTo方法,在jdk1.8中相应的源码如下:
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; }
|
这个方法的逻辑也比较直观,就是分别取了两个字符串的底层字符数组的长度,然后再取最小的那个的长度来做循环,之后一次判断每个位置的字符的大小。
注:同样的,由于jdk12底层存储的改变,compareTo的实现也发现了很大的变化。
从源码看replace
replace用来进行字符串内容的替换,jdk1.8中源码如下:
1 2 3 4
| public String replace(CharSequence target, CharSequence replacement) { return Pattern.compile(target.toString(), Pattern.LITERAL).matcher( this).replaceAll(Matcher.quoteReplacement(replacement.toString())); }
|
可以看到replace里边使用了正则表达式相关的一些方法,如果再进去replaceAll方法,可以看到里边还用到了StringBuffer和StringBuilder。
注:同样的,jdk12中这个方法的逻辑也发生了很大变化。
关于字符串拼接的详细分析
字符串拼接如果是String,一般都是直接用”+”,如果是StringBuilder或者StringBuffer,则是使用append。
实际上,jdk8中用”+”拼接也不全是一样的,例如如下代码:
1 2 3 4 5 6 7 8 9 10
| public static void main(String[] args) { String a1="ab"+"cd";
String a="ab"; String b="cd"; String d=a+b;
StringBuilder sb=new StringBuilder("ab"); sb.append("cd"); }
|
上边代码有三个字符串的拼接操作,一个是直接字面量拼接,一个是字符串变量之间拼接,还有一个是StringBuilder的拼接。
结果javap工具执行”javap -c xxx.class”可以查看编译的过程,编译过程如下:
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
| 0: ldc #2 // String abcd 2: astore_1 3: ldc #3 // String ab 5: astore_2 6: ldc #4 // String cd 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_2 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_3 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore 4 29: new #5 // class java/lang/StringBuilder 32: dup 33: ldc #3 // String ab 35: invokespecial #9 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 38: astore 5 40: aload 5 42: ldc #4 // String cd 44: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 47: pop 48: return
|
上边内容很多,需要有一些jvm基础后才能较全面的理解,但是这里其实可以仅针对后边的注释先进行一定的理解,主要关注这样几行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 0: ldc #2 // String abcd
3: ldc #3 // String ab 6: ldc #4 // String cd 9: new #5 // class java/lang/StringBuilder 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: new #5 // class java/lang/StringBuilder 33: ldc #3 // String ab 35: invokespecial #9 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 42: ldc #4 // String cd 44: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
从上边的内容可以看到,针对字符串字面量的加号拼接,实际上jvm在编译的时候就进行了优化,编译出来的class文件就已经拼成了一个字符串。
针对变量的加号拼接,会先定义两个String字符串变量,然后创建StringBuilder对象再进行初始胡和append的拼接操作,最后再使用toString方法转回String。
针对StringBuilder的,先创建StringBuilder对象,然后创建了String类型的变量,之后再初始化和append拼接。
关于StringBuild扩容
String和StringBuilder底层都是用的数组存储,jdk8中是字符数组,之后有的版本是byte数组,而数组长度是不可变的,因次使用StringBuilder的append进行字符串拼接的时候就涉及到底层数组的扩容,在jdk8源码中主要是下边的一些代码:
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
| public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }
|
这里首先会取一个实际使用的数组长度和新增字符串的长度的和,然后传给扩容的方法。
在扩容方法里拿这个参数和底层数组长度进行比较,当超过数组长度时则进行底层数组的扩容。
在扩容的时候可以看到,如果长度超过了Integer.MAX_VALUE,则抛出内存溢出异常,也就是这个数组最大长度是Integer.MAX_VALUE,正常情况下扩容是在新的实际字符串长度基础上乘以2,然后加2。