字符串常量池常见问题
在研究问题之前,先了解一下字符串常量池
字符串常量池
对于编译期间可以确定值的字符串,也就是字符串常量池,JVM会将其放入字符串常量池。字符串常量池是JVM为了提高性能和减少内存消耗专门为字符串开辟的一块区域,主要目的是为了避免字符串的重复创建。JDK1.7之前字符串常量池存放在运行时常量池存放在方法区中,JDK1.7时字符串常量池从方法区拿到了堆中。
问题研究
我们先看下这段代码的执行结果:
String str1 = "Str";
String str2 = "ing";
String str3 = "Str" + "ing"; //常量池上的对象
String str4 = str1 + str2; //堆上的对象
String str5 = "String"; //常量池中对象
System.out.println(str3 == str4); //false
System.out.println(str3 == str5); //true
System.out.println(str4 == str5); //false
对于基本类型来说,== 比较的是值,对于引用类型来说 ==比较的是地址
字符串str1,str2,str3,str5都是常量池中的对象,由于引用的值在程序编译期间是未知的,编译器无法对其进行优化,所以str4并不是字符串常量池中的对象,属于堆上的对象
示例图:
字符串常量经过拼接得到的字符串常量在编译阶段就被存放在字符串常量池中,这个得益于编译期的优化。
在编译过程中,编译期会进行一个常量折叠(Constant Floding) 的过程,在《深入理解Java虚拟机》中也有介绍。
常量折叠会把常量表达式求出来的值作为常量嵌在最终代码中,这是javac编译期对源代码做的极少量优化措施之一(代码优化几乎都在即时编译期中进行)
也并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本类型数据以及字符串常量
final
修改的基本数据类型和字符串常量- 字符串通过“+”拼接得到的字符串,基本数据类型之间算术运算,基本数据类型的位运算
小提示:对象引用和“+”的字符串拼接方式,实际上是通过StringBuilder
调用append()
方法实现的,最后调用一个toString()
生成String对象。所以我们平常应尽量避免多个字符串拼接,因为这样会重新创建对象,如果需要改变字符串的话,可以使用StringBuilder
和StringBuffer
。
通过前面学习,我们也可以看到,当一个基本类型和字符串常量被final
修饰的时候,也会被进行常量折叠的代码优化。
就拿我们上面的例子来说,将str1和str2使用final修饰:
final String str1 = "Str";
final String str2 = "ing";
String str3 = "Str" + "ing";
String str4 = str1 + str2;
String str5 = "String";
System.out.println(str3 == str4); //true
System.out.println(str3 == str5); //true
System.out.println(str4 == str5); //true
可以看到结果是不同的,为什么使用final
修饰过后,str4 == str3,str4 == str5的结果就是true了呢?
被final
关键字修改的字符串会被编译期当做常量处理,编译期在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果编译期在运行时才知道其确切值的话,就无法对其进行优化。实例代码:
public static void main(String[] args) {
final String str1 = "Str";
//final String str2 = "ing";
final String str2 = getStr(); //str2是运行时才确定其值
String str3 = "Str" + "ing";
String str4 = str1 + str2;
String str5 = "String";
System.out.println(str3 == str4); //false
System.out.println(str3 == str5); //false
System.out.println(str4 == str5); //false
}
public static String getStr(){
return "ing";
}
使用new创建字符串时的问题
先来看下面代码:
String str1 = "String";
String str2 = new String("String");
String str3 = new String("String");
System.out.println(str1 == str2); //false
System.out.println(str2 == str3); //false
这是为什么呢?
当创建字符串是使用的String str = “String”;
这种方式的时候,jvm首先会检查字符串常量池中有没有“String”,如果字符串常量池中没有,则创建一个,然后str指向字符串常量池中的对象,如果有,直接将str指向“String”,因此str指向的是字符串常量池中的对象。
如果是使用的String str = new String("String");
这种方式,便需要创建新的对象。
使用new的方式创建对象的方式如下,主要分为三部:
- 在堆中创建一个字符串对象
- 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
- 如果没有的话则需要在字符串常量池中创建一个值相等的字符串常量,如果有的话,就直接返回对中的字符串对象实例地址。
因此,str2和str3是堆中的对象,并且各自的地址也不相同。
字符串常量池的两种主要使用方式:
- 直接使用双引号声明出来的
String
对象直接存放在字符串常量池中 - 使用
String
提供的intern()
方法。String.intern()
是一个Native方法,它的作用是:如果字符串常量池已经包含一个此String对象内容的字符串,则返回字符串常量池中该字符串的引用;如果没有,JDK1.7之前的处理方式是在字符串常量池中创建一个与此String
内容相同的字符串,并返回字符串常量池中创建的对象的引用,JDK1.7以及之后,字符串常量池被拿到了堆中,JVM不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池,减少不必要的内存开销。
String str = new String(“String”);这句话创建了几个对象
从前面的学习可以看出,总共创建了1或2个对象:
- 如果字符串常量池中有该字符串的内容,则在堆中创建一个字符串对象
- 如果字符串常量池中没有该字符串的内容,首先在字符串常量池中创建一个字符串对象,然后在堆中创建。
拓展:基本类型的包装类和常量池
Java基本类型的包装类大部分都实现了常量池技术。
Byte
,Short
,Integer
,Long
这四种包装类默认创建了数值 [-128,127] 的响应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回True
或False
,两种浮点型数据类型的包装类Float
,Double
并没有实现常量池技术。
Integer
Integer包装类可以缓存[-128,127]之间的数,也就是说对于每次Integer i1 = 40;Integer i2 = 40;
,i1 == i2的结果为true,因为Integer有缓存技术,源代码如下:
public static Integer valueOf(int i){
if(i >= IntegerCache.low && i <= IntegerCache.high)
return new IntegerCache.cache[i + (-IntegerCache.lwo)];
return new Integer(i);
}
private static class IntegerCache{
static final int low = -128;
static final int high = 127;
static final Integer cache[];
}
当数字超过华缓存的范围,比如Integer i1 = 333; Integer i2 = 333;
,则i1 == i2的结果为false,因为两次都会new一个对象,所以地址并不相同。
Character
public static Character valueOf(char c){
if(c <= 127){
return new CharacterCache.cache[(int)c];
}
return new Character(c);
}
public static class CharacterCache{
private characterCache(){};
static final Character cache[] = new Character[127 + 1];
static{
for(int i = 0; i < cache.length; i++){
cache[i] = new Character((char)i);
}
}
}
对于下面的情况:
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1 == i2); //false
输出结果为false,所以,所有整形包装类对象之间值的比较,全部使用equals方法比较。
Integer一个例子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println(i1 == i2); //true
System.out.println(i1 == i2 + i3); //true
System.out.println(i1 == i4); //false
System.out.println(i4 == i5); //false
System.out.println(i4 == i5 + i6); //true
System.out.println(40 == i5 + i6); //true
i4 == i5 + i6为什么是true呢?因为i5 + i6会进行自动拆箱操作,进行数值相加,即 i4 == 40。又因为Integer对象无法与数值进行比较,所以 i4 会自动拆箱为 int 值40,最终这条语句转为 40 == 40 进行数值比较。