吐槽一道常见的java面试题

题目为:“请问String s = new String(“xyz”);创建了多少个String实例?”

后来我看到这道题被知乎的R大吐槽了(甚至专门写了一篇文章批判),所以研究一下这道题(相当于R大文章的读书笔记)。

参考:http://rednaxelafx.iteye.com/blog/774673

 

一、存在什么问题

这道题的问题在于没有定义“创建了”的意义。什么叫“创建了”?什么时候创建了什么?这段Java代码片段实际运行的时候真的会“创建两个String实例”么?

这道题留下了太多的可变因素,所以不存在标准答案。网传的答案说得太绝对:

两个(一个是“xyz”,一个是指向“xyz”的引用对象s)

这肯定是有问题的,或者说考虑得并不全面。

如果这道是面试题,那么可以当面让面试官澄清“创建了”的定义,然后再对应的回答。这种时候面试官多半会让被面试者自己解释,那就好办了,好好晒给面试官看。

下面我们尝试完善这道题目。

二、完善

(1)请问String s = new String(“xyz”);在运行时涉及了多少个String实例?

一种合理的解答是:

答案:两个,一个是字符串字面量”xyz”所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与”xyz”相同的实例。

这是根据Java语言规范相关规定可以给出的合理答案。

那么问题来了,我们该如何定义这里的“运行时”?

要知道“运行时”既包括类加载阶段,也包括这个代码片段自身执行的时候。在这两个不同阶段中,这段代码可能有不同表现,必须分开考虑。

这个问题将在下面进行讨论。

(2)请问String s = new String(“xyz”);涉及用户声明的几个String类型的变量

这个问题比较好回答,我们只声明了一个String s,当然只有一个String变量。回答为:

答案:一个,就是String s。

(3)请问String s = null;涉及用户声明的几个String类型的变量

Java里变量就是变量,引用类型的变量只是对某个对象实例或者null的引用,不是实例本身。声明变量的个数跟创建实例的个数没有必然关系,像是说:

这段代码会涉及3个String类型的变量:

1、s1,指向”a”。
2、s2,指向与s1相同。
3、s3,值为null,只有句柄,不指向任何实例。

以及3个String实例:

1、”a”字面量对应的驻留的字符串常量的String实例。
2、””字面量对应的驻留的字符串常量的String实例。
(String.concat()是个有趣的方法,当发现传入的参数是空字符串时会返回this,所以这里不会额外创建新的String实例)
3、通过new String(String)创建的新String实例;没有任何变量指向它。

所以,答案依然为:

答案:一个,就是String s。

(4)回到原问题

原始问题为:请问String s = new String(“xyz”);创建了多少个String实例?

我们假设网传的答案是正确的:两个(一个是“xyz”,一个是指向“xyz”的引用对象s)。

那么新的问题来了,下面的代码片段在执行时应该创建几个String实例?

如果参照标准答案,是不是要创建四个实例?

这当然有问题。马上就会有人跳出来说:上下两个”xyz”字面量都引用了同一个String对象,所以不应该是创建了4个对象。

那么应该是多少个?

上面提到了,“运行时”应该分为“类加载过程”与“实际执行某个代码片段”,因此两者必须分开讨论。

1.类加载过程中

为了执行问题中的代码片段,其所在的类必然要先被加载,而且同一个类最多只会被加载一次(要注意,对JVM来说“同一个类”并不是类的全限定名相同就足够了,而是<类全限定名, 定义类加载器>一对都相同才行)。

根据JVM的规范,符合规范的JVM实现应该在类加载的过程中创建并驻留一个String实例,作为常量来对应”xyz”字面量。具体是在类加载的resolve阶段进行的。

这个常量是全局共享的,只在先前尚未有内容相同的字符串驻留过的前提下,才需要创建新的String实例。

我个人理解为:在类加载时,加载第一条语句时,直接在“全局共享的字符串常量池”中创建了一个”xyz”实例。加载第二条语句时,因为字符串常量池中已经存在”xyz”实例,所以不会再创建。

综上,在类加载过程中,jvm只在字符串变量池中创建了一个”xyz”实例。

2.实际执行某个代码片段时

真正执行原问题中的代码片段时,

JVM需要执行的字节码类似这样:

第4行的ldc指令只是把先前在类加载过程中已经创建好的一个String对象(”xyz”)的一个引用压到操作数栈顶而已,并不会新建String对象。

实际上,字节码中出现过多少次new java/lang/String,就创建了多少个String对象。在这段字节码中只有第0行new了一次,所以原问题中的代码每执行一次只会新建一个String实例。

我们javap一下这段代码:

就会发现每执行一次只会新建2个String实例。

综上,在实际执行代码片段时,jvm在堆中创建了两个”xyz”实例。

(5)new的意义

R大分别解释了Java和JVM中new的不同含义,我还没能完全理解。

为了避免一些同学犯糊涂,再强调一次:

在Java语言里,“new”表达式是负责创建实例的,其中会调用构造器去对实例做初始化;构造器自身的返回值类型是void,并不是“构造器返回了新创建的对象的引用”,而是new表达式的值是新创建的对象的引用。

对应的,在JVM里,“new”字节码指令只负责把实例创建出来(包括分配空间、设定类型、所有字段设置默认值等工作),并且把指向新创建对象的引用压到操作数栈顶。此时该引用还不能直接使用,处于未初始化状态(uninitialized);如果某方法a含有代码试图通过未初始化状态的引用来调用任何实例方法,那么方法a会通不过JVM的字节码校验,从而被JVM拒绝执行。

能对未初始化状态的引用做的唯一一种事情就是通过它调用实例构造器,在Class文件层面表现为特殊初始化方法“<init>”。实际调用的指令是invokespecial,而在实际调用前要把需要的参数按顺序压到操作数栈上。在上面的字节码例子中,压参数的指令包括dup和ldc两条,分别把隐藏参数(新创建的实例的引用,对于实例构造器来说就是“this”)与显式声明的第一个实际参数(”xyz”常量的引用)压到操作数栈上。

在构造器返回之后,新创建的实例的引用就可以正常使用了。

(6)注意问题的前提

以上讨论都只是针对规范所定义的Java语言与Java虚拟机而言。概念上是如此,但实际的JVM实现可以做得更优化,原问题中的代码片段有可能在实际执行的时候一个String实例也不会完整创建(没有分配空间)。

我们需要注意这个问题没有给出任何前提,这段代码跑在不同的JVM上可能有不同的效果。

三、总结

通过解读R大的文章,我大致上明白了“为什么这道题目有问题,哪里有问题”。

还要多学学dalao提出问题、寻找论据、解决问题的能力。

发表评论

电子邮件地址不会被公开。 必填项已用*标注