(1)String是不可变(immutable)的。
查看Java源代码不难发现,Java类定义为final,且里边的大多数字段也是final的,如下图:
(2)两种创建String的方式和区别。
一般来说,我们会采取两种方式来创建String,一种是直接指向字符串常量,另一种是new String(...)。这两种究竟有什么区别,来看看下面这个例子:
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
System.out.println(s3 == s4); // false
System.out.println(s3.equals(s4)); // true
System.out.println(s4 == s1); // false
System.out.println(s4.equals(s1)); // true
System.out.println(s4.intern() == s1); // true
System.out.println(s4.intern().equals(s1)); // true
关于equals(..)和==的区别,equals(..)比较两个字符串的value是否相同,==比较两个字符串是否指向同一个内存地址。当然,如果两个字符串指向同一个内存地址,调用equals(..)进行比较肯定会返回true。
从上面的demo不难发现,s1和s2指向同一内存地址,而s3和s4并不指向同一内存地址。这牵涉Java内存管理的知识,简单说来,我们通过第一种方式创建的字符串(即指向字符串常量的方式),它会返回string pool中"hello"字符串常量的引用,且string pool保证"hello"的唯一性。而通过第二种方式创建的字符串(即new String(...)),它会在heap中动态申请内存,调用一次则会重新申请一次,这样每次申请的内存地址会不一致。注意string pool和heap不是一个概念,不是同一段内存,这也就解释了s4和s1指向的内存地址不同。
我们注意到s4.intern()与s1的指向是相同的,说一下intern()方法:
public String intern()
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in §3.10.5 of the Java Language Specification
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
大致意思就是说,它会字符串在string pool中的地址,如果string pool中存在相等的字符串(equals()返回true)则直接返回该字符串引用;如果不存在则在string pool中创建字符串返回引用。那么,s4.intern()返回的地址正是s1引用的字符串地址。
(3)可变的字符串。
前面说到String是不可变的,但有时候我们需要可变的字符串来完成一些任务,通常采用的方式是用+号在字符串尾部添加另一段字符串,但需要注意的是,这种方式并不会在原有的字符串基础上进行修改,而是会创建新的字符串,即在heap中不断地new新的String,这种方式可想而知效率会很低。为了解决这类问题,JDK提供了两个类StringBuilder和StringBuffer。我们在使用时需要区分两者的使用范围,StringBuilder是非线程同步的,StringBuffer是线程同步的。换句话说,在多线程的情况下推荐使用StringBuffer,在单线程的情况下推荐使用StringBuilder。
在使用StringBuilder和StringBuffer的时候,我们还需要注意一个问题,如果预先清楚最终生成的字符串的大小,最好在初始化StringBuilder和StringBuffer时指定String的capacity,这样能够获得一定程度的性能提升。至于为什么要这样用,可以参考JDK源码关于这两个类的实现,提示与容量相关。
(4)String为什么设计为不可变?
可能有人会有疑问,为什么String设计为不可变,设计为可变不是会更方便?
在上面的那个例子中,s1和s2指向的同一个引用,如果此时s1发生了变化,当String设计为可变时,那么此时s2也会发生变化,这样会带来一些麻烦。
我们看到,在String类的定义中,有一个int字段hash,它是用来保存String对象的hashcode,如果String是可变的,需要反复调用hashCode方法计算hash值,而如果String是不可变的,那么能够保证hash值不会发生变化。它应用在HashMap的key中会更加高效。
同时,不可变的也是更加安全的,举个例子:打开文件时要指定文件路径,传入String引用,如果String是可变的,不能保证打开的文件是我们所想要的。