字符串的存储
运行时常量池是Class常量池加载后,存在方法区中的运行时数据,通常说的常量池是运行时常量池。方法区是一块逻辑区域,现在hotspot的实现是元数据区。
字符串常量池是一个特殊的区域,原来 jdk 1.7以前在方法区,后来移到了堆区。文中说的方法区,统一更正为字符串常量池所在的堆中的一块特殊区域。
两个不是一个区域
String a = "abc";// 字符串常量池在,地址在堆区1
String b = new String("abc");// 在堆区开辟了新的空间,地址在堆区2,引用为b。值是"abc"
System.out.println(a == b);// 引用类型的相互比较,比较的是地址
----*----
结果:false
a指向哪片内存,b又指向哪片内存呢?
对象储存在堆中,这个是不用质疑的
a b作为字面量一开始储存在了class文件中的Class常量池,之后运行期,转存至方法区常量池中。 a == b,是引用的比较(经解析,转成运行区数据后,就是比较的地址(指针值)。a对应的值是堆区字符串常量池中“abc”的地址,b对应的是堆区new String(“abc”)的地址。两个不一样的地址,自然不一样。
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
分析: 1、s1 = = s2 很容易可以判断出来。s1 和 s2 都指向了字符串常量池中的Hello。 2、s1 = = s3
这里要注意一下,因为做+号的时候,会进行优化,在字符串常量池会生成 Hel 和lo,然后拼接成Hello,Hello已经生成了 也在堆区的字符串常量池中, 地址和s1指向的地址是一样的,所以也是true 3、s1 = = s4
s4是分别用了常量池中的字符串和存放对象的堆中的字符串,做+的时候会进行动态调用,最后生成的仍然是一个String对象存放在堆中。
4、s1 = = s9
在JAVA9中,因为用的是动态调用,所以返回的是一个新的String对象。所以s9和s4,s5这三者都不是指向同一块内存
5、s1 = = s6 为啥s1 和 s6地址相等呢?
归功于intern方法,这个方法首先在常量池中查找是否存在一份equal相等的字符串如果有的话就返回该字符串的引用,没有的话就将它加入到字符串常量池中,所以存在于class中的常量池并非固定不变的,可以用intern方法加入新的
需要注意的特例
public static final String a = "123";
public static final String b = "456";
public static void main(String[] args)
{
String c = "123456";
String d = a + b;
System.out.println(c == d);// true
}
------反编译结果-------
0: ldc #2 // String 123456
2: astore_1
3: ldc #2 // String 123456
5: astore_2
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
我们可以发现,对于final类型的常量它们已经在编译中被确定下来,自动执行了+号,把它们拼接了起来,所以就相当于直接”123” +
“456”;
public static final String a;
public static final String b;
static {
a = "123";
b = "456";
}
public static void main(String[] args)
{
String c = "123456";
String d = a + b;
System.out.println(c == d);
}
------反编译结果-------
3: getstatic #3 // Field a:Ljava/lang/String;
6: getstatic #4 // Field b:Ljava/lang/String;
9: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
上个例子是在编译期间,就已经确定了a和b(确定的放在常量池中),但是在这段代码中,编译期static不执行的,a和b的值是未知的,static代码块,在初始化的时候被执行,初始化属于运行期。看看反编译的结果,很明显使用的是indy指令,动态调用返回String类型对象。一个在堆中一个在字符串常量池中,自然是不一样的。
包装类的常量池技术(缓存)
相信学过java的同学都知道自动装箱和自动拆箱,自动装箱常见的就是valueOf这个方法,自动拆箱就是intValue方法。在它们的源码中有一段神秘的代码值得我们好好看看。除了两个包装类Long和Double
没有实现这个缓存技术,其它的包装类均实现了它。
Integer a=40:如果是-128到127之间的Integer则从IntegerCache取出Integer,这些引用指向常量池中的int 值。类加载的过程中,先将-128到127之间的数据放到常量池,解析时,Integer引用 a指向常量池中的数据。如果是-128到127之外指向的是堆中new出的对象。
Integer和Integer 的比较==不会自动拆箱,比较的仍是地址,+ -运算会拆箱,赋值运算如Integer a=40是装箱,会自动执行valueOf方法。
Integer a = =40(int类型) 比较是值的比较,Interger a=b+c(其中b c是Ingeger类型),b+c先拆箱intValue,再做值的比较。
public static Integer valueOf(int i) {// Integer a=40是装箱:如果是-128到127之间的Integer:从IntegerCache取出Integer,这些引用指向常量池中的数据。否则指向的是堆中的对象。
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {// 将-128到127之间的数据放到常量池
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}
分析:我们可以看到从-128~127的数全部被自动加入到了常量池里面,意味着这个段的数使用的Integer指向的地址都是一样的。
Integer i1 = 40;// 装箱 自动调用valueOf
Integer i2 = 40;
Double i3 = 40.0;
Double i4 = 40.0;
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i3=i4 " + (i3 == i4));
-----结果----
true
false
1、== 这个运算在不出现算数运算符的情况下 不会自动拆箱,所以i1 和 i 2它们不是数值进行的比较,仍然是比较地址是否指向同一块内存
2、它们都在常量池中存储着
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 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));// i2+i3拆箱(intValue)加和(40),比较Interger和int类型,先将Integer转换成int类型,再做值比较,所以返回的是true
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));// 地址比较
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));// 拆箱后做值比较
System.out.println("40=i5+i6 " + (40 == i5 + i6));// 拆箱后做值比较
----结果----
(1)i1=i2 true
(2)i1=i2+i3 true
(3)i1=i4 false
(4)i4=i5 false
(5)i4=i5+i6 true
(6)40=i5+i6 true
注意点
1、当出现运算符的时候,Integer不可能直接用来运算,所以会进行一次拆箱成为基本数字进行比较
2、==这个符号,既可以比较普通基本类型,也可以比较内存地址看比较的是什么了
分析:
(1)号成立不用多说
(2) i2+i3拆箱(intValue)加和(40),比较Interger和int类型,先将Integer转换成int类型,再做值比较,所以返回的是true
比价的是地址(3)(4)号是因为内存地址不同
(5)(6)自动拆箱,最终比较的是值PS:equals方法比较的时候不会处理数据之间的转型,比如Double类型和Integer类型。
练习
Integer i1 = 400;
Integer i2 = 400;
Integer i3 = 0;
Integer i4 = new Integer(400);
Integer i5 = new Integer(400);
Integer i6 = new Integer(0);
Integer i7 = 1;
Integer i8 = 2;
Integer i9 = 3;
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("400=i5+i6 " + (400 == i5 + i6));
----结果----
i1=i2 false
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
400=i5+i6 true
字符串是存在常量池中的不可变对象,变的是地址
package com.example.test;
public class TestString {
private static String s="123";
private static final String sFinal="123";
private static StringBuffer stringBuffer=new StringBuffer("23");
public static void main(String[] args){
System.out.println("s sFinal 指向了方法区中常量池中同一个对象 就是true :" + (s == sFinal));
String t=s.replace("1", "2");
System.out.println("如果字符串不可变:123 --"+s);
s=s.replace("1", "2");
System.out.println("字符串不可变,但是生成了一个新字符串对象,原来字符串引用 s 指向这个新字符串对象--"+s);
System.out.println("字符串不可变,但是生成了一个新字符串对象,字符串引用 t 指向这个新字符串对象--"+t);
System.out.println("如果字符串不可变:false --"+(s==t));
// 加final编译不通过,指向对象不能改变
//sFinal=sFinal.replace("1", "2");
// stringBuffer.replace使stringBuffer发生了变化,stringBuffer1和stringBuffer指向同一个对象
StringBuffer stringBuffer1=stringBuffer.replace(0, 1, "2");
System.out.println("如果stringBuffer可变true--"+(stringBuffer==stringBuffer1));
}
}