本文已经收录进 Github 110k+ 点赞的 Java 知识点总结类开源项目JavaGuide,【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。
前言
看到了一个球友分享的面试题,一定要分享一下。
这个面试题不论是面试还是笔试中都是非常常见的,搞懂原理非常重要!
球友的描述如下:
不过,这个问题我们在日常开发中不会遇到。
因为,比较 String 字符串的值是否相等,可以使用 equals()
方法。 String
中的 equals
方法是被重写过的。 Object
的 equals
方法是比较的对象的内存地址,而 String
的 equals
方法比较的是字符串的值是否相等。
不过,这个面试题会涉及到很多 Java 基础以及 JVM 相关的知识点。还是非常有必要搞懂的!
问题解答&原理分析
我对问题进行了完善了修改,我们先来看字符串不加 final
关键字拼接的情况。完善后的代码如下(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针为字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 System.out.println("aa==bb");// true
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于
String str3 = "str" + "ing";
编译器会给你优化成String str3 = "string";
。并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量
final
修饰的基本数据类型和字符串变量- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
因此,str1
、 str2
、 str3
都属于字符串常量池中的对象。
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
因此,str4
并不是字符串常量池中存在的对象,属于堆上的新对象。
我画了一个图帮助理解:
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder
或者 StringBuffer
。
不过,字符串使用 final
关键字声明之后,可以让编译器当做常量来处理。
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "str2";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final
关键字修改之后的 String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就想到于访问常量。
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码如下(str2
在运行时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "str2";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
我们再来看一个类似的问题!
String str1 = "abcd";
String str2 = new String("abcd");
String str3 = new String("abcd");
System.out.println(str1==str2);
System.out.println(str2==str3);
上面的代码运行之后会输出什么呢?
答案是:
false
false
这是为什么呢?
我们先来看下面这种创建字符串对象的方式:
// 从字符串常量池中拿对象
String str1 = "abcd";
这种情况下,jvm 会先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
因此,str1
指向的是字符串常量池的对象。
我们再来看下面这种创建字符串对象的方式:
// 直接在堆内存空间创建一个新的对象。
String str2 = new String("abcd");
String str3 = new String("abcd");
只要使用 new 的方式创建对象,便需要创建新的对象 。
使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:
- 在堆中创建一个字符串对象
- 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
- 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。
因此,str2
和 str3
都是在堆中新创建的对象。
字符串常量池比较特殊,它的主要使用方法有两种:
- 直接使用双引号声明出来的
String
对象会直接存储在常量池中。 - 如果不是用双引号声明的
String
对象,使用String
提供的intern()
方法也有同样的效果。String.intern()
是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此String
内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。
示例代码如下(JDK 1.8) :
String s1 = "Javatpoint";
String s2 = s1.intern();
String s3 = new String("Javatpoint");
String s4 = s3.intern();
System.out.println(s1==s2); // True
System.out.println(s1==s3); // False
System.out.println(s1==s4); // True
System.out.println(s2==s3); // False
System.out.println(s2==s4); // True
System.out.println(s3==s4); // False
推荐阅读
- R 大(RednaxelaFX)关于常量折叠的回答:www.zhihu.com/question/55…
- 《深入理解 Java 虚拟机》第 10 章程序编译与代码优化
总结
- 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
- 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
- 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的
String
对象(String s1 = "java"
)更利于让编译器有机会优化我们的代码,同时也更易于阅读。 - 被
final
关键字修改之后的String
会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就想到于访问常量。