一提到java的String首先想到的应该就是它的不可变性,其原因是因为String类的内部是使用一个private final char value[]的字符数组来存储数据,本身没有相应的set方法,同时String类又是final的,所以该内部数组就与外界隔绝了,唯一的方法就是通过反射破除private的限制,虽然value[]也是final的,我们不可以修改其内部的数据,但是可以修改其引用的指向从而改变String的值。
至于为什么要把String设计成不可变的,首先,究其原因还是因为字符串是我们编程过程中最经常使用的对象,所以有必要在其使用内存时做一些优化,其中一个优化就是字符串常量池,在这个池中不存在两个相同值的字符串。对于字符串,其对象的引用都是存储在栈中的,对于对象本身,如果是编译期就确定其值的话(比如直接使用双引号创建)就会存储在常量池中,如果是运行期才能确定其值的话(比如使用new创建)就会存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中可以有多份。
对于语句String str = new String("abc")执行时,会先去常量池中查找是否存在字符串“abc”对象,如果没有则先在常量池中创建“abc”对象,再将该对象拷贝到堆中。如果已经存在的话则将其直接拷贝到堆中。如果我们想直接获取一个存储在常量池的字符串对象,可以使用intern()方法,例如String s1 = new String("abc");s2 = s1.intern();注意,此时s1.equals(s2)是true,但是s1==s2是false,因为s1是指向堆中的“abc”对象,s2是指向常量池中的“abc”对象。
虽然常量池的使用节省了内存,但是多个引用同时指向常量池中的一个字符串对象时,如果其中一个引用更改了字符串的值,其他几个便会受到影响,而字符串的不可变性则完全避免了这种问题的出现。
其次,因为String对象的内部值不可改变,所以其自身的hashCode值也不会改变,所以每一个字符串的hashCode都会被提前计算好缓存在自身对象中,所以我们经常使用String作为HashMap的键,这样在查询时便节省了计算hashCode的步骤,同时因为String的不可变性,也不会出现因为某个键改变后与其他键冲突的情况。
最后,因为String对象是不可变的所以它肯定是线程安全的,至于字符串拼接返回新对象的问题,本文就不在此赘述。