共计 2752 个字符,预计需要花费 7 分钟才能阅读完成。
Java 面试题系列:将面试题中比拟经典和外围的内容写成系列文章继续在公众号更新,可坚固基础知识,可梳理底层原理,欢送大家继续关注【程序新视界】。本篇为面试题系列第 2 篇。
常见面试问题
上面代码中创立了几个对象?
new String("abc");
答案七嘴八舌,有说创立了 1 个对象,也有说创立了 2 个对象。答案对,也不对,要害是要学到问题底层的原理。
底层原理剖析
在上篇文章《面试题系列第 1 篇:说说 == 和 equals 的区别?你的答复可能是谬误的》中咱们曾经提到,String 的两种初始化模式是有本质区别的。
String str1 = "abc"; // 在常量池中
String str2 = new String("abc"); // 在堆上
当间接赋值时,字符串“abc”会被存储在常量池中,只有 1 份,此时的赋值操作等于是创立 0 个或 1 个对象。如果常量池中曾经存在了“abc”,那么不会再创建对象,间接将援用赋值给 str1;如果常量池中没有“abc”,那么创立一个对象,并将援用赋值给 str1。
那么,通过 new String(“abc”); 的模式又是如何呢?答案是 1 个或 2 个。
当 JVM 遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创立这个一个字符串。而后再执行 new 操作,会在堆内存中创立一个存储“abc”的 String 对象,对象的援用赋值给 str2。此过程创立了 2 个对象。
当然,如果检索常量池时发现曾经存在了对应的字符串,那么只会在堆内创立一个新的 String 对象,此过程只创立了 1 个对象。
在上述过程中查看常量池是否有雷同 Unicode 的字符串常量时,应用的办法便是 String 中的 intern() 办法。
public native String intern();
上面通过一个简略的示意图看一下 String 在内存中的两种存储模式。
下面的示意图咱们能够看到在堆内创立的 String 对象的 char value[] 属性指向了常量池中的 char value[]。
还是下面的示例,如果咱们通过 debug 模式也可能看到 String 的 char value[] 的援用地址。
图中两个 String 对象的 value 值的援用均为 {char[3]@1355},也就是说,尽管是两个对象,但它们的 value 值均指向常量池中的同一个地址。当然,大家还能够拿一个简单对象(Person)的字符串属性(name)雷同时的 debug 后果进行比对,后果是一样的。
深刻问法
如果面试官说程序的代码只有上面一行,那么会创立几个对象?
new String("abc");
答案是 2 个?
还真不肯定。之所以独自列出这个问题是想揭示大家一点:没有间接的赋值操作(str=”abc”),并不代表常量池中没有“abc”这个字符串。也就是说掂量创立几个对象、常量池中是否有对应的字符串,不仅仅由你是否创立决定,还要看程序启动时其余类中是否蕴含该字符串。
降级加码
以下实例咱们暂且不思考常量池中是否曾经存在对应字符串的问题,假如都不存在对应的字符串。
以下代码会创立几个对象:
String str = "abc" + "def";
下面的问题波及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它本人也必定是字符串常量。字符串常量的“+”号连贯 Java 虚构机会在程序编译期将其优化为连贯后的值。
就下面的示例而言,在编译时曾经被合并成“abcdef”字符串,因而,只会创立 1 个对象。并没有创立长期字符串对象 abc 和 def,这样加重了垃圾收集器的压力。
咱们通过 javap 查看 class 文件能够看到如下内容。
很显著,字节码中只有拼接好的 abcdef。
针对下面的问题,咱们再次降级一下,上面的代码会创立几个对象?
String str = "abc" + new String("def");
创立了 4 个,5 个,还是 6 个对象?
4 个对象的说法:常量池中别离有“abc”和“def”,堆中对象 new String(“def”) 和“abcdef”。
这种说法对吗?不齐全对,如果说上述代码创立了几个字符串对象,那么能够说是正确的。但上述的代码 Java 虚拟机在编译的时候同样会优化,会创立一个 StringBuilder 来进行字符串的拼接,实际效果相似:
String s = new String("def");
new StringBuilder().append("abc").append(s).toString();
很显然,多出了一个 StringBuilder 对象,那就应该是 5 个对象。
那么创立 6 个对象是怎么回事呢?有同学可能会想了,StringBuilder 最初 toString() 之后的“abcdef”难道不在常量池存一份吗?
这个还真没有存,咱们来看一下这段代码:
@Test
public void testString3() {
String s1 = "abc";
String s2 = new String("def");
String s3 = s1 + s2;
String s4 = "abcdef";
System.out.println(s3==s4); // false
}
依照下面的剖析,如果 s1+s2 的后果在常量池中存了一份,那么 s3 中的 value 援用应该和 s4 中 value 的援用是一样的才对。上面咱们看一下 debug 的成果。
很显著,s3 和 s4 的值雷同,但 value 值的地址并不相同。即使是将 s3 和 s4 的地位调整一下,成果也一样。s4 很明确是存在于常量池中,那么 s3 对应的值存储在哪里呢?很显然是在堆对象中。
咱们来看一下 StringBuilder 的 toString() 办法是如何将拼接的后果转化为字符串的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很显然,在 toString 办法中又新创建了一个 String 对象,而该 String 对象传递数组的构造方法来创立的:
public String(char value[], int offset, int count)
也就是说,String 对象的 value 值间接指向了一个曾经存在的数组,而并没有指向常量池中的字符串。
因而,下面的精确答复应该是创立了 4 个字符串对象和 1 个 StringBuilder 对象。
小结
咱们通过一行创立字符串的代码逐渐剖析 String 对象的整个构建及拼接过程,理解了底层实现原理。是不是很有意思?当你把握了这些底层基本知识,即使面试题的模式如何变动,你必然能一眼识破假相。
下篇文章,(读者提议)咱们来讲讲 Integer 的比拟的底层逻辑,欢送继续关注。
<center> 程序新视界 :精彩和成长都不容错过 </center>