字符串的不可变

字符串的存储

运行时常量池是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));
    }
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值