我们先看下面几个常见的面试题:
String s1 = "abc";
String s2 = new String("abc");
String s3 = "a" + "b" + "c";
String s4 = s2.intern();
System.out.printf("s1=s2:%s\n", s1 == s2);
System.out.printf("s1=s3:%s\n", s1 == s3);
System.out.printf("s1=s4:%s\n", s1 == s4);
那么请问各自的结果是什么?
我们知道,String这个类是被final修饰,是不可变的。String的对象一旦建立,是不能更改的。我相信很多同学并不真正了解这里的不可变的真正含义,因为我开始学习java的时候,这个地方就困惑了我很久。
有下面一段代码:
String a ="Hello";
a ="你好";
那这时候我们说,这个a变了啊。其实这里我们主要是理解有误区,没有分清楚什么是对象和对象引用。我们这里的a 指的是对象引用而不是对象本身;对象在内存中是一块内存地址,而a则是一个指向该内存地址的引用。
要比较两个对象是否相等,要用==;要比较两个对象的值是否相等,要用equals方法来判断。所以在上面额说的这个例子中,第一次赋值的时候,创建了一个“Hello”对象,a 引用指向“Hello”地址;第二次赋值的时候,又重新创建了一个对象“你好”,a 引用指向了“你好”,但“Hello”对象依然存在于内存中。
如下图:
在java中,主要有两种创建字符串对象的方式,
- 通过字符串常量的方式来创建,如上面String s1 = "abc";
- 通过new形式创建,如上面的String s2 = new String("abc")。
当使用第一种方式创建时,虚拟机会先检查该对象在常量池中是否存在,如果存在,就回返该对象的引用,如果不存在,就在常量池中创建该字符串对象。这样就能让字符串对象重复使用,降低内存;
当使用第二种方式创建时,在类加载的时候,会在常量池中先创建“abc”,然后程序调用new的时候,会引用常量池中“abc”字符串(其实就是将堆中String对象char[]数组指向常量池对象abc中的char[])在堆中创建一个String对象,最后将s2的引用指向这个堆中String对象。
基于以上的String字符串基础内容的分析,我们看下最初那几个题的答案:
s1=s2:false
s1=s3:true
s1=s4:true
第一,s1=s2为false,因为s1指向了常量池中的“abc”,而s2指向了堆内存的String对象,显然不相等;
第二,s1=s3为false,因为在编译的时候,虚拟机给我们代码优化成了String s3 = "abc",显然s3也指向了常量池中的"abc",所以相等;
第三,s1=s4为true,这里的intern()方法我们了解的可能不是很多。这个intern()方法,它的作用是如果字符串常量池已经包含一个等于此String对象的字符串,则返回字符串常量池中这个字符串的引用, 否则将当前String对象的引用地址(堆中)添加到字符串常量池中并返回。
所以s4也指向了常量池中的"abc",显然s1=s4。
明白了上面几个问题,也知道intern()这个方法的原理,我们再看一个题:
String s1 = new String("5") + new String("5");
s1.intern();
String s2 = "55";
System.out.printf("s1=s2:%s\n", s1 == s2);
这个最后的结果是:s1=s2:true。这个就比较好分析了:通过加号动态生成的“55”字符串由于在运行时常量中没有该字符串的引用,所以会在调用s1.intern()时,在运行时常量池中生成一个s1的引用,当s2再次引用该字符串时,发现运行时常量池中存在相同值的字符串的引用,就直接返回s1的引用。所以s1==s2是返回的true。
那么接下来我们再看一个题,哈哈:
String s1 = new String("55");
s1.intern();
String s2 = "55";
System.out.printf("s1=s2:%s\n", s1 == s2);
挺有意思,知识把上面的题和这个题调换一下顺序,但是结果却是:
s1=s2:false
分析如下:首先"55"在类加载的时候,已经存在静态常量池中,在new string(“55”)时,会在运行时常量池中创建一个“55”字符串的直接引用。而s1指向的并不是该引用,而是new string这个对象的引用。当s2=“55”时,返回的是运行时常量池中的引用。所以s1==s2返回false。