String是Java中除了基本数据类型之外最重要的类型。很多人会认为他比较简单。但是和String相关的面试题很多。下面我就随机找两道面试题,看看你能不能全部答对:Q1:Strings=newString("hollis");定义了几个对象。Q2:如何理解String的intern方法?以上两个是经常考的面试题和String相关的题,很多人一般都知道答案。A1:如果常量池中已经存在“hollis”,则直接引用,即此时只创建一个对象。如果常量池中不存在“hollis”,则先创建后引用,即有两个对象。A2:String实例调用intern()方法时,JVM会检查常量池中是否有相同Unicode的字符串常量,如果有则返回其引用,如果没有则在其中添加一个等于str的Unicode常量池字符串并返回对它的引用;两个答案看似没什么问题,但仔细想想,好像哪里不对劲。根据上面两道面试题的答案,也就是说newString会去查常量池,如果有就直接引用。如果不存在,则必须在常量池中创建,那么intern还做什么呢?下面的代码没有意义吗?Strings=newString("霍利斯").intern();如果,每当我们使用new创建一个字符串时,我们都会去字符串池中检查然后返回。那么下面的代码也应该输出true?Strings1="Hollis";Strings2=newString("Hollis");Strings3=newString("Hollis").intern();System.out.println(s1==s2);System.out.println(s1==s3);但是,上面代码的输出结果是(basejdk1.8.0_73):falsetrue不知道,聪明的读者看完这段代码是不是有点懵,这是怎么回事?别着急,慢慢听我说。文字和运行时常量池JVM在实例化字符串常量时会执行一些优化,以提高性能并减少内存开销。为了减少在JVM中创建的字符串数量,字符串类维护了一个字符串常量池。在JVM运行时区的方法区中,有一个区域就是运行时常量池,主要用来存放编译过程中产生的各种字面量和符号引用。了解Class文件结构或者做过Java代码反编译的朋友可能知道,java代码经过javac编译后,文件结构中包含了一部分Constantpool。例如下面的代码:publicstaticvoidmain(String[]args){Strings="Hollis";}经过编译,常量池的内容如下:Constantpool:#1=Methodref#4.#20//java/lang/Object."":()V#2=String#21//Hollis#3=Class#22//StringDemo#4=Class#23//java/lang/Object...#16=Utf8s..#21=Utf8Hollis#22=Utf8StringDemo#23=在Utf8java/lang/Object上面的Class文件中的常量池中,有几个重要的内容:#16=Utf8s#21=Utf8Hollis#22=Utf8StringDemo以上其中常量,s是前面提到的符号引用,而Hollis是前面提到的字面值。Class文件中常量池部分的内容在运行时会被加载到运行时常量池中。关于字面量,详见JavaSESpecificationsnewString。在下面创建几个对象。我们可以分析Strings=newString("Hollis");的情况创建对象。在这段代码中,我们可以知道的是,在编译过程中,符号引用s和字面量Hollis会被加入到Class文件的常量池中,然后在类加载阶段,这两个常量会进入常量池。但是,这个“入口”过程并不会直接加载所有类中定义的所有常量,而是会进行比较。如果需要添加到字符串常量池中的字符串已经存在,那么就不需要再将字符串字面量加载进去。因此,当我们说<如果常量池中已经存在"hollis",那么会直接引用它,即此时只创建一个对象>就是在字符串中创建字符串字面量的过程水池。说完编译期,就到了运行期了。在运行时,当newString("Hollis");执行时,会在Java堆中创建一个字符串对象,这个对象对应的String字面量存放在字符串常量池中。但是,Strings=newString("Hollis");,对象的符号引用s存放在Java虚拟机栈中,它存放的是刚刚在堆中创建的字符串对象的引用。所以,你也知道为什么下面代码的输出是假的。Strings1=newString("Hollis");Strings2=newString("Hollis");System.out.println(s1==s2);因为,==比较的是s1和s2创建的对象在堆中的地址,当然不同的。但是如果你用equals,那么你比较的是字面内容,那么你就会得到true。在不同版本的JDK中,Java堆和字符串常量池的关系也不同。这里为了表达方便,画成两个独立的物理区域。有关详细信息,请参阅Java虚拟机规范。上图中,s1和s2是两个完全不同的对象,在堆中有自己的内存空间,当然不相等。所以,Strings=newString("Hollis");您将知道创建多个对象的答案。常量池中的“对象”在编译时确定,在类加载时创建。如果类加载时字符串常量已经存在于常量池中,则省略这一步。堆中的对象在运行时确定,并在代码执行到new时创建。运行时常量池的动态扩展编译过程中产生的各种字面量和符号引用是运行时常量池的重要组成部分,但不是全部。那么还有一种情况可以在运行时将常量添加到运行时常量池中。那就是String的intern方法。当String实例调用intern()方法时,JVM会检查常量池中是否有Unicode相同的字符串常量,如果有则返回其引用,如果没有则添加一个Unicode等于str的字符在常量池String中返回它的引用;intern()有两个作用,第一个是将字符串字面量放入常量池(如果池中没有),第二个是返回这个常量的引用。让我们看看开头那个令人困惑的例子:Strings1="Hollis";Strings2=newString("Hollis");Strings3=newString("Hollis").intern();System.out.println(s1==s2);System.out.println(s1==s3);你可以简单的理解为Strings1="Hollis";和Strings3=newString("Hollis").intern();同理(但其实也有一些区别,这里不展开)。两者都定义了一个字符串对象,然后将其字符串字面量保存在常量池中,并将这个字面量的引用返回给定义的对象引用。如下图:对于Strings3=newString("Hollis").intern();,如果不调intern,s3指向JVM在堆中创建的对象的引用(s2在数字)。但是执行intern方法时,s3会指向字符串常量池中的字符串常量。由于s1和s3都是对字符串常量池中字面量的引用,所以s1==s3。但是,对s2的引用是堆中的一个对象,所以s2!=s1。我不知道实习生的正确用法。有没有发现在Strings3=newString("Hollis").intern();中,intern其实是多余的?因为即使不使用intern,Hollis也会以字面量的形式加载到Class文件的常量池中,然后加入到运行时常量池中。何必呢?intern会在什么场景下使用?在解释这个之前,我们先看下面这段代码:Strings1="Hollis";Strings2="Chuang";Strings3=s1+s2;Strings4="Hollis"+"Chuang";反编译后代码如下:Strings1="Hollis";Strings2="Chuang";Strings3=(newStringBuilder()).append(s1).append(s2).toString();Strings4="HollisChuang";可以发现,同样的字符串拼接,s3和s4经过编译器编译后的实现方式不同。s3转成StringBuilder并追加,s4直接拼接成新的字符串。如果你有兴趣,你还可以发现Strings4=s1+s2;编译后,常量池中有两个字符串常量,分别是Hollis和Chuang(其实Hollis和Chuang是Strings1="Hollis";和Strings2="Chuang";定义的),拼接结果HollisChuang不在常量池。如果代码只有Strings4="Hollis"+"Chuang";,那么常量池中就只有HollisChuang,而不是Hollis和Chuang。原因是因为常量池是用来保存确定的字面值的。也就是说,对于字符串拼接,纯字面量和字面量拼接,拼接结果都会作为常量保存到字符串中。如果字符串拼接中有参数不是字面值而是变量,则整个拼接操作会被编译成StringBuilder.append。在这种情况下,编译器无法知道它的确定值。它只能在运行时确定。嗯,有了这个功能,intern就派上用场了。也就是很多时候,我们在程序中使用的字符串只能在运行时确定,不能在编译时确定,所以没有办法在编译时加入到常量池中。这时候对于可能会经常用到的字符串,使用intern来定义。JVM每次运行到这段代码,都会直接返回常量池中字面值的引用,这样可以减少大量的字符串。对象已创建。比如美团点评团队的《深入解析String#intern》文章中给出的一个例子:staticfinalintMAX=1000*10000;staticfinalString[]arr=newString[MAX];publicstaticvoidmain(String[]args)throwsException{Integer[]DB_DATA=newInteger[10];Randomrandom=newRandom(10*10000);for(inti=0;i