一道和Java常量池有关的笔试题

前天做某公司的在线笔试题时做到了这样一道题。

下面这段Java代码输出的结果是?

1
2
3
4
5
6
7
8
9
public class HelloWorld {
public static void main(String[] args) {
Integer i1 = 127, i2 = 127, i3 = 128, i4 = 128;
System.out.println(i1 == i2);
System.out.println(i1.equals(i2));
System.out.println(i3 == i4);
System.out.println(i3.equals(i4));
}
}

A false true true true
B true false true true
C true true false true
D true true true false

我的第一反应就是这个题没的选啊,true true true true嘛,但是隐隐约约又觉得和127、128的界限有关,看这段代码看了半天,最后随便口胡了一个答案,我自己都不记得选的什么了。

后来笔试结束后,运行一下就发现其实结果应该是 C true true false true。那么为什么是这个结果呢?

首先大家都知道的就是“a==b”和”a.equals(b)”的区别。如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。

接着就是涉及到Java的常量池了。JVM会自动维护八种基本类型的常量池,int常量池中初始化-128~127的范围,所以当为Integer i=127时,在自动装箱过程中是取自常量池中的数值,而当Integer i=128时,128不在常量池范围内,所以在自动装箱过程中需new 128,所以地址不一样。

Java常量池

什么是Java常量池

java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。 String类也是java中用得多的类,同样为了创建String对象的方便,也实现了常量池的技术。以字符串为例,在Java源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号为 8(CONSTANT_String_info)的常量表 。当JVM加载 class文件的时候,会为对应的常量池建立一个内存数据结构,并存放在方法区中。

java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

那么下一段代码的结果就显而易见了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IntegerTest {
public static void main(String[] args) {
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println(i1 == i2); //false,i1、i2分别位于堆中不同的内存空间

Integer i3 = 1;
Integer i4 = 1;
System.out.println(i3 == i4); //true,i3、i4指向常量池中同一个内存空间

System.out.println(i1 == i3); // fasle,显然i1、i3指向两个不同的空间
}
}

下面我们再来看一道比较常见的String的笔试题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringTest {
public static void main(String[] args) {
String s = new String("abc"); //line1
String s1 = "abc"; //line2
String s2 = new String("abc"); //line3

System.out.println(s == s1); //lin4
System.out.println(s == s2); //lin5
System.out.println(s1 == s2); //lin6

System.out.println(s == s.intern()); //lin7
System.out.println(s1 == s1.intern()); //line8
System.out.println(s == s2.intern()); //line9

String hello = "hello";
String hel = "hel";
String io = "io";

System.out.println(hello == "hel"+"io"); //lin10
System.out.println(hello == "hel"+io); //line11
}
}

问:
(1)当执行完line1后在内存中生成几个对象,是什么在什么地方?
(2)当执行完line2后在内存中生成几个对象,是什么在什么地方?
(3)当执行完line3后在内存中生成几个对象,是什么在什么地方?
(4)line4、5、6分别输出什么?
(5)line7、8、9分别输出什么?
(6)line10、11分别输出什么?

答:
(1)有2个对象,内容都是abc,s是对象的地址(引用),首先生成一个对象“abc”在String池(pool)中,当生成一个字符串对象时先到String池中先找有没有内容为“abc”的对象,这时没有这样一个对象,于是把“abc”这个对象放到String池中,在new的时候在堆中生成一个内容为“abc”的对象。因此一共有两个对象,一个在String池中,一个在Java内存的堆中。此时s指向堆中的对象。
(2)还是2个对象,没有再生成新的对象。首先先从String池中有没有内容为“abc”的对象存在,此时有,所以不会生成新的对象,将s1指向String池中的对象。
(3)一共3个对象。String池中有“abc”这个内容的对象,所以在堆中new一个内容为“abc”的新的对象,并将引用地址赋给了s2,此时s2指向堆中的这个对象。
(4)s s1 s2分别指向不同的对象,内存地址不同,因此返回的都是false。
(5)false true true。关于intern方法的定义可以查看JDK帮助文档。

When the intern method is invoked, if the pool already contains a string equal to thisString object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

如果String池中已经包含了String对象,并且与调用intern()方法的String对象进行equals()结果相等时,来自String池的字符串就被返回了,否则String对象会被增加到String池中,并返回对这个字符串对象的引用。s.intern()方法,先找String池有没有内容为“abc”的对象,此时在String池中有这样一个对象,因此将s.intern()返回的是String池中“abc”的地址,也就是s1,所以s和s.intern()是指向不同的地址。
另外String s和t, s.intern() == t.intern()当且仅当s.equals(t)返回true。
(6)true false。“+”左右两边都是常量值,会检查String池有没有这个对象存在,有的话直接返回String池中的对象,没有的话就在String池中生成一个新的对象。line10不会产生新的对象,返回的是String池中已有的“hello”对象。“+”左右两边有一个不是常量的话,不检查String池而直接在堆中生成一个新的对象。所以line11中”hel”+io会导致在堆中生成一个新的对象,和String池中肯定就不是同一个对象了。