String s = new String(“xyz“) 创建了多少个实例?

引用

问题:Java代码

String s = new String(“xyz”);

创建了几个String Object?

这个问题自身就没有合理的答案

引用

答案:两个(一个是“xyz”,一个是指向“xyz”的引用对象s)
(好吧这个答案的吐槽点很多……大家慢慢来)

这问题的毛病是什么呢?它并没有定义“创建了”的意义。
什么叫“创建了”?什么时候创建了什么?
而且这段Java代码片段实际运行的时候真的会“创建两个String实例”么?

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

如果是笔试题就没有提问要求澄清的机会了。不过会出这种题目的地方水平多半也不怎么样。说不定出题的人就是从各种宝典上把题抄来的,那就按照宝典把那不大对劲的答案写上去就能混过去了

=======================================================

先换成另一个问题来问:

Java代码

String s = new String(“xyz”);
在运行时涉及几个String实例?

一种合理的解答是:

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

《2020最新Java基础精讲视频教程和学习路线!》
这是根据Java语言规范相关规定可以给出的合理答案。考虑到Java语言规范中明确指出了:

The Java Language Specification, Third Edition 写道

The Java programming language is normally compiled to the bytecoded instruction set and binary format defined in The Java Virtual Machine Specification, Second Edition (Addison-Wesley, 1999).

也就是规定了Java语言一般是编译为Java虚拟机规范所定义的Class文件,但并没有规定“一定”(must),留有不使用JVM来实现Java语言的余地。
考虑上Java虚拟机规范,确实在这段代码里涉及的常量种类为CONSTANT_String_info的字符串常量也只有"xyz"一个。CONSTANT_String_info是用来表示Java语言中String类型的常量表达式的值(包括字符串字面量)用的常量种类,只在这个层面上考虑的话,这个解答也没问题。
所以这种解答可以认为是合理的。

值得注意的是问题中“在运行时”既包括类加载阶段,也包括这个代码片段自身执行的时候。下文会再讨论这个细节与楼主原本给出的问题的关系。

碰到这种问题首先应该想到去查阅相关的规范,这里具体是Java语言规范与Java虚拟机规范,以及一些相关API的JavaDoc。很多人喜欢把“按道理说”当作口头禅,规范就是用来定义各种“道理”的——“为什么XXX是YYY的意思?”“因为规范里是这样定义的!”——无敌了。

在Java虚拟机规范中相关的定义有下面一些:

The Java Virtual Machine Specification, Second Edition 写道

2.3 Literals

A literal is the source code representation of a value of a primitive type (§2.4.1), the String type (§2.4.8), or the null type (§2.4). String literals and, more generally, strings that are the values of constant expressions are “interned” so as to share unique instances, using the method String.intern.

The null type has one value, the null reference, denoted by the literal null. The boolean type has two values, denoted by the literals true and false.

2.4.8 The Class String

Instances of class String represent sequences of Unicode characters (§2.1). A String object has a constant, unchanging value. String literals (§2.3) are references to instances of class String.

2.17.6 Creation of New Class Instances

A new class instance is explicitly created when one of the following situations occurs:

Evaluation of a class instance creation expression creates a new instance of the class whose name appears in the expression.
Invocation of the newInstance method of class Class creates a new instance of the class represented by the Class object for which the method was invoked.

A new class instance may be implicitly created in the following situations:

Loading of a class or interface that contains a String literal may create a new String object (§2.4.8) to represent that literal. This may not occur if the a String object has already been created to represent a previous occurrence of that literal, or if the String.intern method has been invoked on a String object representing the same string as the literal.
Execution of a string concatenation operator that is not part of a constant expression sometimes creates a new String object to represent the result. String concatenation operators may also create temporary wrapper objects for a value of a primitive type (§2.4.1).

Each of these situations identifies a particular constructor to be called with specified arguments (possibly none) as part of the class instance creation process.

5.1 The Runtime Constant Pool

● A string literal (§2.3) is derived from a CONSTANT_String_info structure (§4.4.3) in the binary representation of a class or interface. The CONSTANT_String_info structure gives the sequence of Unicode characters constituting the string literal.

● The Java programming language requires that identical string literals (that is, literals that contain the same sequence of characters) must refer to the same instance of class String. In addition, if the method String.intern is called on any string, the result is a reference to the same class instance that would be returned if that string appeared as a literal. Thus,
Java代码

(“a” + “b” + “c”).intern() == “abc”

must have the value true.

● To derive a string literal, the Java virtual machine examines the sequence of characters given by the CONSTANT_String_info structure.

○ If the method String.intern has previously been called on an instance of class String containing a sequence of Unicode characters identical to that given by the CONSTANT_String_info structure, then the result of string literal derivation is a reference to that same instance of class String.

○ Otherwise, a new instance of class String is created containing the sequence of Unicode characters given by the CONSTANT_String_info structure; that class instance is the result of string literal derivation. Finally, the intern method of the new String instance is invoked.

The remaining structures in the constant_pool table of the binary representation of a class or interface, the CONSTANT_NameAndType_info (§4.4.6) and CONSTANT_Utf8_info (§4.4.7) structures are only used indirectly when deriving symbolic references to classes, interfaces, methods, and fields, and when deriving string literals.

把Sun的JDK看作参考实现(reference implementation, RI),其中String.intern()的JavaDoc为:

JavaDoc 写道

public String intern()

Returns a canonical representation for the string object.

A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String 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.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

All literal strings and string-valued constant expressions are interned. String literals are defined in §3.10.5 of the Java Language Specification

Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

=======================================================

再换一个问题来问:

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

答案也很简单:

答案:一个,就是String s。

把问题换成下面这个版本,答案也一样:

String s = null;
涉及用户声明的几个String类型的变量?

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

String s1 = “a”;
String s2 = s1.concat("");
String s3 = null;
new String(s1);
这段代码会涉及3个String类型的变量,
1、s1,指向下面String实例的1
2、s2,指向与s1相同
3、s3,值为null,不指向任何实例

以及3个String实例,

1、"a"字面量对应的驻留的字符串常量的String实例

2、""字面量对应的驻留的字符串常量的String实例

(String.concat()是个有趣的方法,当发现传入的参数是空字符串时会返回this,所以这里不会额外创建新的String实例)

3、通过new String(String)创建的新String实例;没有任何变量指向它。

回到开头引用的问题与“标准答案”

String s = new String(“xyz”);
创建了几个String Object?

答案:两个(一个是“xyz”,一个是指向“xyz”的引用对象s)
用归谬法论证。假定问题问的是“在执行这段代码片段时创建了几个String实例”。如果“标准答案”是正确的,那么下面的代码片段在执行时就应该创建4个String实例了:

String s1 = new String(“xyz”);
String s2 = new String(“xyz”);
马上就会有人跳出来说上下两个"xyz"字面量都是引用了同一个String对象,所以不应该是创建了4个对象。

那么应该是多少个?

运行时的类加载过程与实际执行某个代码片段,两者必须分开讨论才有那么点意义。

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

根据上文引用的规范的内容,符合规范的JVM实现应该在类加载的过程中创建并驻留一个String实例作为常量来对应"xyz"字面量;具体是在类加载的resolve阶段进行的。这个常量是全局共享的,只在先前尚未有内容相同的字符串驻留过的前提下才需要创建新的String实例。

等到真正执行原问题中的代码片段时,JVM需要执行的字节码类似这样:

Java bytecode代码:

0: new #2; //class java/lang/String
3: dup
4: ldc #3; //String xyz
6: invokespecial #4; //Method java/lang/String."":(Ljava/lang/String;)V
9: astore_1
这之中出现过多少次new java/lang/String就是创建了多少个String对象。也就是说原问题中的代码在每执行一次只会新创建一个String实例。
这里,ldc指令只是把先前在类加载过程中已经创建好的一个String对象(“xyz”)的一个引用压到操作数栈顶而已,并不新创建String对象。

所以刚才用于归谬的代码片段:

String s1 = new String(“xyz”);
String s2 = new String(“xyz”);
每执行一次只会新创建2个String实例。


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

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

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

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

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

连接地址:https://www.iteye.com/blog/rednaxelafx-774673

猜你喜欢

转载自blog.csdn.net/weixin_46699878/article/details/110688570