在使用java.lang.String会经常涉及到的几个问题:
1. String编译期优化
2. String运行期和String.intern()
String a=“A” ; String a = new String(“A”) 有几个对象; String a = new String(‘A”); a.intern()又有几个对象
3. StringBuilder、StringBuffer、String之间的区别
在这些过程中需要辨明的一些问题: String常量池、 String常量池在jdk 6/7/8中的变化、字面量和String引用以及String对象的区别、String.intern在jdk6/7/8中的变化
String编译期优化
- String s = “a” + “b” + “c”
String+常量, 会在编译期间计算出结果, 实际等于: String s = “abc”
- String a = “a” , b = “b”, c = “c”
String s = a + b +c
String+变量, 会在编译期间优化成StringBuilder.append()进行操作.
这里说明一下, 在jdk 5之前,是被优化成StringBuffer.append()操作的,在jdk 5之后,改成使用StringBuilder.append()进行优化, StringBuilder线程不安全,但是更高效, 这里是内部变量,也涉及不到线程安全内容,就使用更高效的StringBuilder.
- String s = “a“ + b; b是 final 常量: final b = “b“
这种情况和第一种情况一样,也是属于String+常量的情况,在编译期间就会优化为String s = “ab”
- String s = “a” + b; b是变量: b = “b”
属于String+变量的情况,也是优化为StringBuilder.appen()操作
- final String s = “a” + b; b 是变量: b = “b”
同样也是String+变量的情况, 一样优化为StringBuilder.append()操作; final只是表示这个变量s不可变, 但是也不代表它是一个常量.
在这里通常会问到一个问题, String在编译期的长度和在运行期的长度.
1. String编译器长度655334(65535): String编译期的长度就是你可以在编译器里面定义的String能够包含多少个字符, 在编译器中, 最多65534个字符.
这里的长度实际是取决编译器. (openJdk的编译工具是javac,javac也是用java写的)
private void checkStringConstant(DiagnosticPosition pos, Object constValue) {
if (nerrs != 0 || // only complain about a long string once
constValue == null ||
!(constValue instanceof String) ||
((String) constValue).length() < Pool.MAX_STRING_LENGTH)
return;
log.error(pos, "limit.string");
nerrs++;
}
再来看看Pool.MAX_STRING_LENGTH
public class Pool {
//...
public static final int MAX_STRING_LENGTH = 0xFFFF;
//...
}
在Pool中定义的MAX_STRING_LENGTH是十进制数0xFFF: 65535, 在判断的时候又是(String) constValue).length() <Pool.MAX_STRING_LENGTH)小于MAX_STRING_LENGTH, 所以String常量在编译器最大长度为65534.
这里的65535这个长度是哪里来的了? 实际上就是int在占用16位(2个字节)的时候的最大值: 2^16 -1 = 65535. 哪为什么不是不是占有4个字节(32)的长度了? 有的老机器上是设置的只占用2个字节,要考虑兼容性, 不然一上去就挂了可不行.
如果绕过javac编译是能够达到65535长度的,eclipse就是采用自己的编译器JDT, 能够达到65535长度.
2. String运行期长度 4G: String运行期取决于String类中定义的成员变量char value[]数组的长度. 这个数组的最大长度就是Integer.MAX_VALUE: 2^31 -1. 计算String大小: 2^ 31 -1 个Unicode character(char: 2个字节)
(2^ 31 -1 ) * 2 byte ~ 2 ^ 32 byte= 2 ^22 kb = 2 ^12 mb = 2^2 gb = 4 GB
也就是近似4G大小.
String运行期和String.intern
1. String s = “abc”
首先,在常量池中检查是否存在内容为”abc”的字符串对象, 如果不存在,则在常量池中创建“abc”对象,并返回该对象引用. 如果已经存在, 则直接返回引用给str. (jdk 1.7)
2. String s = new String(“abc”)
new String(“abc”)和new StringBuilder()和new StringBuffer()一样都是在堆中构建一个对象. (创建第一个对象), 返回引用给s . 但是”abc”是字符串, 这个字符串在第一次声明的时候会被加入到常量池中. (创建的第二个对象): 检查常量池中是否存在”abc”对象, 不存在则创建“abc”对象,然后让堆中的String对象引用刚创建好的常量池中的对象;如果存在,则让堆中创建好的字符串对象对常量池中的对象进行引用.
3. new StringBuilder(“ab“).append(“c“).toString().intern()
jdk1.6 :String.intern()发现常量池(String pool)存在”abc”的字符串对象,则返回String pool中这个字符串对象的引用. 如果不存在则将字符串对象放入到常量池中, 然后返回String Pool中的字符串对象的引用.
jdk1.7: String常量池从方法区(Method Area)迁移到堆(Heap)中 ; 有一点改变, 在不存在的时候, 如果堆中存在”abc”对象则直接将堆中的”abc”对象引用放入到String Pool中. 如果堆中不存在”abc”对象, 则先在堆中创建”abc”对象, 然后保存堆中对象的引用到String Pool.
(这里堆中不存在“abc”对象的情况: String ab = “ab”, String c = “c”, String s = ab +c;s.intern(). ) 这里就是堆中不存在”abc” String对象的情况, String s = ab +c会在编译器被优化为StringBuilder.append(). 所以在堆中就不存在”abc”对象. 在调用s.intern() 的时候会调用stringBuilder.toString()会创建一个String(“abc”)对象, 上面所说的堆中不存在,先在堆中创建一个”abc”对象也就是说的这一步: StringBuilder.toString().
怎么在代码中求证?
jdk1.8: 取消方法区(Method Area) , 改为元空间(Meta space)
常见面试问题:
1. String s = new String(“abc”) 创建了几个对象? 1个或者2个, 如果在String Pool中存在”abc” , 则只在堆中创建“abc”对象; String Pool中不存在, 说明“abc”这个字面量第一次出现, 然后在String Pool中创建一个”abc”对象
2. String s = “abc” 创建了几个对象? 0个或者1个, 如果String Pool中不存在“abc”对象, 则创建”abc”对象,将对象放到String Pool中; 如果String Pool中已经有了”abc”的对象或者引用,则返回String Pool中的对象引用, 也就没有创建对象.
3. String.intern()创建了几个对象? 0个或者1个, 只有当String Pool和堆中都没有的时候才会在堆中创建对象, 将对象的引用放到String Pool中, 然后在返回这个对象的引用.
StringBuilder、StringBuffer和String的区别
String是不可变的,StringBuilder和StringBuffer都是可变的字符串对象.
StringBuilder是线程不安全的, StringBuffer是线程安全的, StringBuffer的方法都是synchronized关键字修饰了的.