字符串常量池

引用:https://www.cnblogs.com/gxyandwmm/p/9495923.html

前言:

在平时我们使用字符串一般就是拿来直接搞起,很少有深入的去想过这方面的知识,导致别人在考我们的时候,会问 String str = new String("123"); 这个一行代码执行创建了几个对象, String str1= str + new String("456"); 这行代码中str1存储在内存的哪个位置,堆 or 字符串常量区(方法区)? 会把我们问的哑口无言了;哈哈哈哈,其实也不是水平问题,是我们平时可以仔细的去总结该类问题,下面就详细的对这类问题进行总结;

问题1:

String str1 = new String("1");
str1.intern();
String str2 = "1";
System.out.println(str1 == str2);  //结果是 false or true?

String str3 = new String("2") + new String("2");
str3.intern();
String str4 = "22";
System.out.println(str3 == str4); //结果是 false or true?

问题2:

String str1 = "aaa";
String str2 = "bbb";
String str3 = "aaabbb";
String str4 = str1 + str2;
String str5 = "aaa" + "bbb";
System.out.println(str3 == str4); // false or true
System.out.println(str3 == str4.intern()); // true or false
System.out.println(str3 == str5);// true or false

问题3:

String t1 = new String("2");
String t2 = "2";
t1.intern();
System.out.println(t1 == t2); //false or true

String t3 = new String("2") + new String("2");
String t4 = "22";
t3.intern();
System.out.println(t3 == t4); //false or true

问题4:

Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;

System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a+b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));

二、知识储备

在解答这四个问题的过程中,我们首先说一下几个知识,很重要:

1.intern()函数

intern函数的作用是将对应的符号常量进入特殊处理,在1.6以前 和 1.7以后有不同的处理;

先看1.6:

在1.6中,intern 的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量

在1.7中:

在1.7中,intern 的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中

2.常量池的分类【理解即可】

2.1 class文件常量池

在Class文件中除了有类的版本【高版本可以加载低版本】、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)【此时没有加载进内存,也就是在文件中】,用于存放编译期生成的各种字面量和符号引用

下面对字面量和符号引用进行说明
字面量
字面量类似与我们平常说的常量,主要包括:

  1. 文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
  2. 被final修饰的变量。

符号引用
主要包括以下常量:

  1. 类和接口和全限定名:例如对于String这个类,它的全限定名就是java/lang/String。
  2. 字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
  3. 方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型
2.2 运行时常量池

我们知道类加载器会加载对应的Class文件,而上面的class文件中的常量池,会在类加载后进入方法区中的运行时常量池【此时存在在内存中】。并且需要的注意的是,运行时常量池是全局共享的,多个类共用一个运行时常量池。并且class文件中常量池多个相同的字符串在运行时常量池只会存在一份
注意运行时常量池存在于方法区中。

2.3 字符串常量池

看名字我们就可以知道字符串常量池会用来存放字符串,也就是说常量池中的文本字符串会在类加载时进入字符串常量池。
那字符串常量池和运行时常量池是什么关系呢?上面我们说常量池中的字面量会在类加载后进入运行时常量池,其中字面量中有包括文本字符串,显然从这段文字我们可以知道字符串常量池存在于运行时常量池中。也就存在于方法区中。
不过在周志明那本深入java虚拟机中有说到,到了JDK1.7时,字符串常量池就被移出了方法区,转移到了[堆]里了
那么我们可以推断,到了JDK1.7以及之后的版本中,运行时常量池并没有包含字符串常量池,运行时常量池存在于方法区中,而字符串常量池存在于中。

3.问题解析【重点】

3.1 问题1解析

@Test
public void question1(){
    String str1 = new String("1");
    // 解析:首先执行此行代码创建两个对象,在执行前会在常量池中创建一个 "1" 的对象,
    // 然后执行该行代码时 new 一个 "1" 的对象存放在堆区中;
    // 然后 str1 指向堆区中的对象;

    String intern1 = str1.intern();
    // 解析:该行代码首先查看 "1" 字符串有没有存在在常量池中,此时存在则直接返回该常量,这里返回后没有引用接受他,
    // 【假如不存在的话在 jdk1.6中会在常量池中建立该常量,在jdk1.7以后会把堆中该对象的引用放在常量池中】

    String str2 = "1";
    // 解析:此时 "1" 已经存在在常量池中,str2指向常量池中的对象;

    System.out.println(intern1 == str2); // [true]
    // 解析:intern1,str2 都指向的是常量池中的对象。

    System.out.println(str1 == str2);  //结果是 false or true?  【false】
    // 解析:str1 指向堆地区的对象,str2 指向的是常量池中的对象,两个引用指向的地址不同,输出 【false】。

    String str3 = new String("2") + new String("2");
    // 解析:此行代码执行的底层执行过程是:
    // 首先使用StringBuffer的append方法将"2"和"2"拼接在一块,然后调用toString方法 new 出 “22” ;
    // 所以此时的 “22” 字符串是创建在堆区的;

    String intern = str3.intern();
    // 解析:此行代码执行时字符串常量池中没有"22",
    // 所以此时在jdk1.6中会在字符串常量池中创建"22",而在jdk1.7以后会把堆中该对象的引用放在常量池中;

    String str4 = "22";
    // 解析:此时的str4在jdk1.6中会指向方法区,而在jdk1,7中会指向堆区;

    System.out.println(intern == str4); // [true]
    // 解析,由于str3.intern(); 把对象的引用放在了常量池中,所以它拿到的是引用,str4 = "22",在常量池中拿到的也是指向堆中对象的引用。

    System.out.println(str3 == str4); //结果是 false or true?  【true】
    // 解析:str3 指向的是对象的引用,str4,由于 str3.intern()的原因导致,常量池中存放的也是堆中对象的引用。所以为 【true】
}

3.2 问题2解析

@Test
public void question2(){
    String str1 = "aaa";
    // 解析:str1 指向方法区。
    String str2 = "bbb";
    // 解析:str2 指向方法区。
    String str3 = "aaabbb";
    // 解析:str3 指向方法区。
    String str4 = str1 + str2;
    // 解析:此行代码执行的底层执行过程是:
    // 首先使用StringBuffer的append方法将 "aaa" 和 "bbb" 拼接在一块,然后调用toString方法 new 出 “aaabbb” ;
    // 所以此时的 “aaabbb” 字符串是创建在堆区的;所以 str4 指向堆区。

    String str5 = "aaa" + "bbb";
    // 解析:【该行代码重点说明一下】。jvm 对其有优化处理,也就是在编译阶段,就会将这两个字符创常量进行拼接,
    // 也就是 "aaabbb";所以它是在方法区的;
    // 这是 class 文件写出的样子:【 String str5 = "aaabbb";】。

    System.out.println(str3 == str4); // false or true  [false]
    // 解析:str3 指向方法区,str4指向堆。结果【false】

    System.out.println(str3 == str4.intern()); // true or false [true]
    // 解析:str4.intern() 会先判断常量池中是否有该对象,现在是有,因为 str3 已经提前创建了;
    // 所以 str4.intern() 返回的是 常量池中的对象,和 str3 指向的是一个地方。结果为 [true]。

    System.out.println(str3 == str5);// true or false   [true]
    // 解析:str5 会在编译期间 变成【String str5 = "aaabbb"】,所以和 str3 一样都是指向的是常量池。
    // 都指向字符串常量区,字符串长常量区在方法区,相同的字符串只存在一份,
    // 其实这个地方在扩展一下,因为方法区的字符串常量是共享的,在两个线程同时共享这个字符串时,
    // 如果一个线程改变他会是怎么样的呢,其实这种场景下是线程安全的,jvm会将改变后的字符串常量在
    // 字符串常量池中重新创建一个处理,可以保证线程安全
}

3.3 问题3解析

@Test
public void question3(){
    String t1 = new String("2");
    // 解析:首先执行此行代码创建两个对象,在执行前会在常量池中创建一个 "2" 的对象,
    // 然后执行该行代码时 new 一个 "2" 的对象存放在堆区中;
    // 然后 t1 指向堆区中的对象;

    String t2 = "2";
    // 解析:创建一个对象,指向字符串常量池。

    t1.intern();
    // 解析:字符串常量池中已经存在 "2" 了,所以返回的是字符串常量池中的对象。

    System.out.println(t1 == t2); //false or true   [false]
    // 解析:t1 指向的是堆,t2 指向的是常量池。结果[false]

    String t3 = new String("2") + new String("2");
    // 解析:此行代码执行的底层执行过程是:
    // 首先使用StringBuffer的append方法将"2"和"2"拼接在一块,然后调用toString方法 new 出 “22” ;
    // 所以此时的 “22” 字符串是创建在堆区的;

    String t4 = "22";
    // 解析:常量池中,没有 "22" ,创建一个。
    t3.intern();
    // 解析:由于 t4 已经在 常量池中创建了一个对象,直接返回。
    System.out.println(t3 == t4); //false or true   [false]
    // 解析:t3 指向的是堆中的引用,t4 指向的是常量池。结果[false]。
}

3.4 问题4解析

这个地方存在一个知识点。可能是个盲区,这次要彻底记住“

(1). 内存中有一个java基本类型封装类的常量池。这些类包括Byte, Short, Integer, Long, Character, Boolean。需要注意的是,Float和Double这两个类并没有对应的常量池。

(2).上面5种整型的包装类的对象是存在范围限定的;范围在-128~127存在在常量池,范围以外则在堆区进行分配。

(3). 在周志明的那本虚拟机中有这样一句话:包装类的
==运行符在不遇到算术运算的情况下不会自动拆箱,以及他们的equals()方法不处理数据类型的关系,通俗的讲也就是 “==”两边如果有算术运算, 那么自动拆箱和进行数据类型转换处理,比较的是数值等不等能。

(4).Long的equals方法会先判断是否是Long类型。

(5).无论是Integer还是Long,他们的equals方法比较的是数值。

@Test
public void question4(){
    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 321;
    Integer f = 321;
    Long g = 3L;

    System.out.println(c == d); // [true]
    // 解析:由于常量池的作用,c 与 d 指向的是同一个对象(注意:
    // 此时的 == 比较的是对象,也就是地址,而不是数值)。由于都指向常量池,所以为 true.
    System.out.println(e == f); // [false]
    // 解析:由于 321 超出了 127,因此常量池失去了作用,所以 e 和 f 数值虽然相同,但不是同一个对象,地址不同,所以为 false.
    System.out.println(c == (a + b));   // [true]
    // 解析:此时 == 两边有算数运算,会进行拆箱,因此此时比较的是数值,而并非对象。因此为 true。
    System.out.println(c.equals(a+b));  // [true]
    // 解析:c 与 a+b 的数值相同, 结果为 true。
    System.out.println(g == (a + b));   //  [true]
    // 解析:由于 == 两边有算数运算符,所以比较的是数值,因此为 true。
    System.out.println(g.equals(a + b));    // [false]
    // 解析:Long 类型的 equal 在比较时,会先判断 a+b 是否为 Long 类型,显然 a+b 不是,所以直接返回 false.
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值