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”难道不在常量池存一份吗?

这个还真没有存,咱们来看一下这段代码:

@Testpublic 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()办法是如何将拼接的后果转化为字符串的:

@Overridepublic 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>