前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >携程一面:String 类型的变量和常量做“+”运算时发生了什么?

携程一面:String 类型的变量和常量做“+”运算时发生了什么?

作者头像
Guide哥
发布2021-09-16 10:08:01
3200
发布2021-09-16 10:08:01
举报
文章被收录于专栏:JavaGuideJavaGuide

大家好,我是 Guide!分享一篇今天起早写的原创。

相关阅读:String s="a"+"b"+"c",到底创建了几个对象?

前言

看到了一个球友分享的面试题,一定要分享一下。

这个面试题不论是面试还是笔试中都是非常常见的,搞懂原理非常重要!

球友的描述如下:

不过,这个问题我们在日常开发中不会遇到。

因为,比较 String 字符串的值是否相等,可以使用 equals() 方法。String 中的 equals 方法是被重写过的。Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是字符串的值是否相等。

不过,这个面试题会涉及到很多 Java 基础以及 JVM 相关的知识点。还是非常有必要搞懂的!

问题解答&原理分析

我对问题进行了完善了修改,我们先来看字符串不加 final 关键字拼接的情况。完善后的代码如下(JDK1.8):

代码语言:javascript
复制
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 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

代码语言:javascript
复制
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"; 。 并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  1. 基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量
  2. final 修饰的基本数据类型和字符串变量
  3. 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

因此,str1str2str3 都属于字符串常量池中的对象。

引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

代码语言:javascript
复制
String str4 = new StringBuilder().append(str1).append(str2).toString();

因此,str4 并不是字符串常量池中存在的对象,属于堆上的新对象。

我画了一个图帮助理解:

我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer

不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

代码语言:javascript
复制
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 在运行时才能确定其值):

代码语言:javascript
复制
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";
}

我们再来看一个类似的问题!

代码语言:javascript
复制
String str1 = "abcd";
String str2 = new String("abcd");
String str3 = new String("abcd");
System.out.println(str1==str2);
System.out.println(str2==str3);

上面的代码运行之后会输出什么呢?

答案是:

代码语言:javascript
复制
false
false

这是为什么呢?

我们先来看下面这种创建字符串对象的方式:

代码语言:javascript
复制
// 从字符串常量池中拿对象
String str1 = "abcd";

这种情况下,jvm 会先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";

因此,str1 指向的是字符串常量池的对象。

我们再来看下面这种创建字符串对象的方式:

代码语言:javascript
复制
// 直接在堆内存空间创建一个新的对象。
String str2 = new String("abcd");
String str3 = new String("abcd");

只要使用 new 的方式创建对象,便需要创建新的对象

使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:

  1. 在堆中创建一个字符串对象
  2. 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
  3. 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。

因此,str2str3 都是在堆中新创建的对象。

字符串常量池比较特殊,它的主要使用方法有两种:

  1. 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  2. 如果不是用双引号声明的 String 对象,使用 String 提供的 intern() 方法也有同样的效果。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。

示例代码如下 :

代码语言:javascript
复制
String s1 = "计算机";
String s2 = s1.intern();
System.out.println(s1 == s2);

JDK1.7 之前的输出(不包含 1.7):

代码语言:javascript
复制
false

JDK1.7 以及之后版本的输出(包含 1.7):

代码语言:javascript
复制
true

推荐阅读

  • R 大(RednaxelaFX)关于常量折叠的回答:https://www.zhihu.com/question/55976094/answer/147302764
  • 《深入理解 Java 虚拟机》第 10 章程序编译与代码优化

总结

  1. 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
  2. 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
  3. 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 String 对象( String s1 = "java" )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
  4. final 关键字修改之后的 String 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就想到于访问常量。

< END >

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JavaGuide 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 问题解答&原理分析
  • 推荐阅读
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档