[TOC]java
字符串就是一连串的字符序列,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中的键每每都使用字符串。ui
因为它的不可变性,像字符串拼接、裁剪等广泛性的操做,每每对应用性能有明显影响。this
为了解决这个问题,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()]
数组对象,而后在常量池中建立一个字符串对象并用数组对象初始化字符串对象的成员变量value,而后将这个字符串的引用返回,好比赋值给a因而可知,a和c对象指向常量池中相同的内存空间不言自明。
而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。
StringBuilder是非线程安全的,StringBuffer是线程安全的。
StringBuffer类中的方法使用了synchronized
同步锁来保证线程安全。
关于锁的话题很是大,会单独成文来讲明,这里推荐一篇不错的博客,有兴趣的能够看看