[TOC]
字符串就是一连串的字符序列,Java提供了String、StringBuilder、StringBuffer三个类来封装字符串
String
类是不可变类,String对象被创建以后,对象中的字符序列是不可改变的,直到这个对象被销毁
jdk1.8 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; //jdk1.9中将char数组替换为byte数组,紧凑字符串带来的优势:更小的内存占用,更快的操作速度。 //构造函数 public String(String original) { this.value = original.value; this.hash = original.hash; } //构造函数 public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } //返回一个新的char[] public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; } } 复制代码
根据上面的代码,我们看看String究竟是怎么保证不可变的。
value
的接口 value
被final修饰,所以变量的引用不可变。 char[]·
为引用类型仍可以通过引用修改实例对象,为此 String(char value[])
构造函数内部使用的 copyOf
而不是直接将 value[]
复制给内部变量`。 arraycopy()
的方式返回一个新的 char[]
String
类中的函数也处处透露着不可变的味道,比如: replace()
public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { //重新创建新的char[],不改变原有对象中的值 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } //最后返回新创建的String对象 return new String(buf, true); } } return this; } 复制代码
当然不可变也不是绝对的,还是可以通过反射获取到变value引用,然后通过value[]修改数组的方式改变value对象实例
String a = "Hello World!"; String b = new String("Hello World!"); String c = "Hello World!"; //通过反射修改字符串引用的value数组 Field field = a.getClass().getDeclaredField("value"); field.setAccessible(true); char[] value = (char[]) field.get(a); System.out.println(value);//Hello World! value[5] = '&'; System.out.println(value);//Hello&World! // 验证b、c是否被改变 System.out.println(b);//Hello&World! System.out.println(c);//Hello&World! 复制代码
写到这里该如何引出不可变的好处呢?忘记反射吧,我们聊聊不可变的好处吧
同一个字符串实例可以被多个线程共享。
比如,网络通信的IP地址,类加载器会根据一个类的完全限定名来读取此类诸如此类,不可变性提供了安全性。
具统计,常见应用使用的字符串中有大约一半是重复的,为了避免创建重复字符串,降低内存消耗和对象创建时的开销。JVM提供了字符串缓存的功能——字符串常量池。如果字符串是可变的,我们就可以通过引用改变常量池总的同一个内存空间的值,其他指向此空间的引用也会发生改变。
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
由于它的不可变性,像字符串拼接、裁剪等普遍性的操作,往往对应用性能有明显影响。
为了解决这个问题,java为我们提供了两种解决方案
还是刚才反射的示例
String a = "Hello World!"; String b = new String("Hello World!"); String c = "Hello World!"; //判断字符串变量是否指向同一块内存 System.out.println(a == b); System.out.println(a == c); System.out.println(b == c); // 通过反射观察a, b, c 三者中变量value数组的真实位置 Field a_field = a.getClass().getDeclaredField("value"); a_field.setAccessible(true); System.out.println(a_field.get(a)); Field b_field = b.getClass().getDeclaredField("value"); b_field.setAccessible(true); System.out.println(b_field.get(b)); Field c_field = c.getClass().getDeclaredField("value"); c_field.setAccessible(true); System.out.println(c_field.get(c)); //通过反射发现String对象中变量value指向了同一块内存 复制代码
输出
false true false [C@6f94fa3e [C@6f94fa3e [C@6f94fa3e 复制代码
字符串常量的创建过程:
char["Hello World!".length()]
由此可见,a和b对象指向常量池中相同的内存空间不言自明。
而b对象的创建是建立在以上的创建过程的基础之上的。 "Hello World!"
常量创建完成时返回的引用,会经过 String
的构造函数。
public String(String original) { this.value = original.value; this.hash = original.hash; } 复制代码
构造函数内部将引用的对象成员变量 value
赋值给了内部成员变量 value
,然后将新创建的字符创对象引用赋值给了b,这个过程发生在堆中。
再来感受下下面这两行代码有什么区别
String b = new String(a); String b = new String("Hello World!"); 复制代码
为了弥补String的缺陷,Java先后提供了StringBuffer和StringBuilder可变字符串类。
二者都继承至AbstractStringBuilder,AbstractStringBuilder使用了 char[] value
字符数组
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; AbstractStringBuilder(int capacity) { value = new char[capacity]; } } 复制代码
可以看出AbstractStringBuilder类和其成员变量value都没有使用final关键字。
StringBuilder和StringBuffer的value数组默认初始长度是16
public StringBuilder() { super(16); } public StringBuffer() { super(16); } 复制代码
如果我们拼接的字符串长度大概是可以预计的,那么最好指定合适的capacity,避免多次扩容的开销。
扩容产生多重开销:抛弃原有数组,创建新的数组,进行arrycopy。