前言:
在平时我们使用字符串一般就是拿来直接搞起, 很少有深入的去想过这方面的知识, 导致别人在考我们的时候, 会问 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");
- t3.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)[此时没有加载进内存, 也就是在文件中] , 用于存放编译期生成的各种字面量和符号引用.
下面对字面量和符号引用进行说明
字面量
字面量类似与我们平常说的常量, 主要包括:
文本字符串: 就是我们在代码中能够看到的字符串, 例如 String a = "aa". 其中 "aa" 就是字面量.
被 final 修饰的变量.
符号引用
主要包括以下常量:
类和接口和全限定名: 例如对于 String 这个类, 它的全限定名就是 java/lang/String.
字段的名称和描述符: 所谓字段就是类或者接口中声明的变量, 包括类级别变量 (static) 和实例级的变量.
方法的名称和描述符. 所谓描述符就相当于方法的参数类型 + 返回值类型.
2.2 运行时常量池
我们知道类加载器会加载对应的 Class 文件, 而上面的 class 文件中的常量池, 会在类加载后进入方法区中的运行时常量池[此时存在在内存中] . 并且需要的注意的是, 运行时常量池是全局共享的, 多个类共用一个运行时常量池. 并且 class 文件中常量池多个相同的字符串在运行时常量池只会存在一份.
注意运行时常量池存在于方法区中.
2.3 字符串常量池
看名字我们就可以知道字符串常量池会用来存放字符串, 也就是说常量池中的文本字符串会在类加载时进入字符串常量池.
那字符串常量池和运行时常量池是什么关系呢? 上面我们说常量池中的字面量会在类加载后进入运行时常量池, 其中字面量中有包括文本字符串, 显然从这段文字我们可以知道字符串常量池存在于运行时常量池中. 也就存在于方法区中.
不过在周志明那本深入 java 虚拟机中有说到, 到了 JDK1.7 时, 字符串常量池就被移出了方法区, 转移到了堆里了.
那么我们可以推断, 到了 JDK1.7 以及之后的版本中, 运行时常量池并没有包含字符串常量池, 运行时常量池存在于方法区中, 而字符串常量池存在于堆中.
3. 问题解析[重点]
3.1 问题 1 解析
tring str1 = new String("1");
解析: 首先此行代码创建了两个对象, 在执行前会在常量池中创建一个 "1" 的对象, 然后执行该行代码时 new 一个 "1" 的对象存放在堆区中; 然后 str1 指向堆区中的对象;
str1.intern();
解析: 该行代码首先查看 "1" 字符串有没有存在在常量池中, 此时存在则直接返回该常量, 这里返回后没有引用接受他,[假如不存在的话在 jdk1.6 中会在常量池中建立该常量, 在 jdk1.7 以后会在堆中建立该字符串, 然后把他的引用放在常量池中]
String str2 = "1";
解析: 此时 "1" 已经存在在常量池中, str2 指向常量池中的对象;
System.out.println(str1 == str2); // 结果是 false or true?
解析: str1 指向堆区的对象, str2 指向常量池中的对象, 两个引用指向的地址不同, 输入 false;
String str3 = new String("2") + new String("2");
解析: 此行代码执行的底层执行过程是 首先使用 StringBuffer 的 append 方法将 "2" 和 "2" 拼接在一块, 然后调用 toString 方法 new 出 "22"; 所以此时的 "22" 字符串是创建在堆区的;
t3.intern();
解析: 此行代码执行时字符串常量池中没有 "22", 所以此时在 jdk1.6 中会在字符串常量池中创建 "22", 而在 jdk1.7 中会在堆中创建该字符串, 然后将其引用添加到常量池中;
String str4 = "22";
解析: 此时的 str4 在 jdk1.6 中会指向方法区, 而在 jdk1,7 中会指向堆区;
System.out.println(str3 == str4); // 结果是 false or true?
解析: 很明显了 jdk1.6 中为 false 在 jdk1.7 中为 true;
3.2 问题 2 解析
String str1 = "aaa";
解析: str1 指向方法区;
String str2 = "bbb";
解析: str2 指向方法区
String str3 = "aaabbb";
解析: str3 指向方法区
String str4 = str1 + str2;
解析: 此行代码上边已经说过原理. str4 指向堆区
String str5 = "aaa" + "bbb";
解析: 该行代码重点说明一下, jvm 对其有优化处理, 也就是在编译阶段就会将这两个字符串常量进行拼接, 也就是 "aaabbb"; 所以他是在方法区中的;'
System.out.println(str3 == str4); // false or true
解析: 很明显 为 false, 一个指向堆 一个指向方法区
System.out.println(str3 == str4.intern()); // true or false
解析: jdk1.6 中 str4.intern 会把 "aaabbb" 放在方法区, 1.7 后放在堆区, 所以在 1.6 中会是 true 但是在 1.7 中是 false
System.out.println(str3 == str5);// true or false
解析: 都指向字符串常量区, 字符串长常量区在方法区, 相同的字符串只存在一份, 其实这个地方在扩展一下, 因为方法区的字符串常量是共享的, 在两个线程同时共享这个字符串时, 如果一个线程改变他会是怎么样的呢, 其实这种场景下是线程安全的, jvm 会将改变后的字符串常量在
字符串常量池中重新创建一个处理, 可以保证线程安全
3.3 问题 3 解析
tring t1 = new String("2");
解析: 创建了两个对象, t1 指向堆区
String t2 = "2";
解析: t2 指向字符串常量池
t1.intern();
解析: 字符串常量池已经存在该字符串, 直接返回;
System.out.println(t1 == t2); //false or true
解析: 很明显 false
String t3 = new String("2") + new String("2");
解析: 过程同问题 1 t3 指向堆区
String t4 = "22";
解析: t4 在 1.6 和 1.7 中指向不同
t3.intern();
解析: 字符串常量池中已经存在该字符串 直接返回
System.out.println(t3 == t4); //false or true
解析: 很明显为 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 方法比较的是数值.
System.out.println(c == d).
解析: 由于常量池的作用, c 与 d 指向的是同一个对象(注意此时的 == 比较的是对象, 也就是地址, 而不是数值). 因此为 true
System.out.println(e == f).
由于 321 超过了 127, 因此常量池失去了作用, 所以 e 和 f 数值虽然相同, 但不是同一个对象, 以此为 false.
System.out.println(c == (a+b)).
此时 == 两边有算术运算, 会进行拆箱, 因此此时比较的是数值, 而并非对象. 因此为 true.
System.out.println(c.equals(a+b))
c 与 a+b 的数值相等, 为 true.
System.out.pirnln(g == (a + b))
由于 == 两边有算术运算, 所以比较的是数值, 因此为 true.
System.out.println(g.equals(a+b)).
Long 类型的 equal 在比较是时候, 会先判断 a+b 是否为 Long 类型, 显然 a+b 不是, 因此 false
来源: https://www.cnblogs.com/gxyandwmm/p/9495923.html