字符串的不可变

字符串的存储

运行时常量池是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);  // trueSystem.out.println(s1 == s3);  // trueSystem.out.println(s1 == s4);  // falseSystem.out.println(s1 == s9);  // falseSystem.out.println(s4 == s5);  // falseSystem.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 1234562: astore_13: ldc           #2                  // String 1234565: astore_26: 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 propertyint 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_VALUEh = 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类型,再做值比较,所以返回的是trueSystem.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));}
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部