首先看一下这道常见的面试题,下面代码中,会创建几个字符串对象?

String s="a"+"b"+"c";

如果你比较一下Java源代码和反编译后的字节码文件,就可以直观的看到答案,只创建了一个String对象。

估计大家会有疑问了,为什么源代码中字符串拼接的操作,在编译完成后会消失,直接呈现为一个拼接后的完整字符串呢?

这是因为在编译期间,应用了编译器优化中一种被称为常量折叠(Constant Folding)的技术,会将编译期常量的加减乘除的运算过程在编译过程中折叠。编译器通过语法分析,会将常量表达式计算求值,并用求出的值来替换表达式,而不必等到运行期间再进行运算处理,从而在运行期间节省处理器资源。

而上边提到的编译期常量的特点就是它的值在编译期就可以确定,并且需要完整满足下面的要求,才可能是一个编译期常量:

  • 被声明为final

  • 基本类型或者字符串类型

  • 声明时就已经初始化

  • 使用常量表达式进行初始化

上面的前两条比较容易理解,需要注意的是第三和第四条,通过下面的例子进行说明:

final String s1="hello "+"Hydra";
final String s2=UUID.randomUUID().toString()+"Hydra";

编译器能够在编译期就得到s1的值是hello Hydra,不需要等到程序的运行期间,因此s1属于编译期常量。而对s2来说,虽然也被声明为final类型,并且在声明时就已经初始化,但使用的不是常量表达式,因此不属于编译期常量,这一类型的常量被称为运行时常量。再看一下编译后的字节码文件中的常量池区域:

可以看到常量池中只有一个String类型的常量hello Hydra,而s2对应的字符串常量则不在此区域。对编译器来说,运行时常量在编译期间无法进行折叠,编译器只会对尝试修改它的操作进行报错处理。

另外值得一提的是,编译期常量与运行时常量的另一个不同就是是否需要对类进行初始化,下面通过两个例子进行对比:

public class IntTest1 {public static void main(String[] args) {System.out.println(a1.a);}
}
class a1{static {System.out.println("init class");}public static int a=1;
}

运行上面的代码,输出:

init class
1

如果对上面进行修改,对变量a添加final进行修饰:

public static final int a=1;

再次执行上面的代码,会输出:

1

可以看到在添加了final修饰后,两次运行的结果是不同的,这是因为在添加final后,变量a成为了编译期常量,不会导致类的初始化。另外,在声明编译器常量时,final关键字是必要的,而static关键字是非必要的,上面加static修饰只是为了验证类是否被初始化过。

我们再看几个例子来加深对final关键字的理解,运行下面的代码:

public static void main(String[] args) {final String h1 = "hello";String h2 = "hello";String s1 = h1 + "Hydra";String s2 = h2 + "Hydra";System.out.println((s1 == "helloHydra"));System.out.println((s2 == "helloHydra"));
}

执行结果:

true
false

代码中字符串h1h2都使用常量赋值,区别在于是否使用了final进行修饰,对比编译后的代码,s1进行了折叠而s2没有,可以印证上面的理论,final修饰的字符串变量属于编译期常量。

再看一段代码,执行下面的程序,结果会返回什么呢?

public static void main(String[] args) {String h ="hello";final String h2 = h;String s = h2 + "Hydra";System.out.println(s=="helloHydra");
}

答案是false,因为虽然这里字符串h2final修饰,但是初始化时没有使用编译期常量,因此它也不是编译期常量。

在上面的一些例子中,在执行常量折叠的过程中都遵循了使用常量表达式进行初始化这一原则,这里可能有的同学还会有疑问,到底什么样才能算得上是常量表达式呢?在Oracle官网的文档中,列举了很多种情况,下面对常见的情况进行列举(除了下面这些之外官方文档上还列举了不少情况,如果有兴趣的话,可以自己查看):

  • 基本类型和String类型的字面量

  • 基本类型和String类型的强制类型转换

  • 使用+-!等一元运算符(不包括++--)进行计算

  • 使用加减运算符+-,乘除运算符*/% 进行计算

  • 使用移位运算符 >><<>>>进行位移操作

  • ……

字面量(literals)是用于表达源代码中一个固定值的表示法,在Java中创建一个对象时需要使用new关键字,但是给一个基本类型变量赋值时不需要使用new关键字,这种方式就可以被称为字面量。Java中字面量主要包括了以下类型的字面量:

//整数型字面量:
long l=1L;
int i=1;//浮点类型字面量:
float f=11.1f;
double d=11.1;//字符和字符串类型字面量:
char c='h';
String s="Hydra";//布尔类型字面量:
boolean b=true;

当我们在代码中定义并初始化一个字符串对象后,程序会在常量池(constant pool)中缓存该字符串的字面量,如果后面的代码再次用到这个字符串的字面量,会直接使用常量池中的字符串字面量。

除此之外,还有一类比较特殊的null类型字面量,这个类型的字面量只有一个就是null,这个字面量可以赋值给任意引用类型的变量,表示这个引用类型变量中保存的地址为空,也就是还没有指向任何有效的对象。

那么,如果不是使用的常量表达式进行初始化,在变量的初始化过程中引入了其他变量(且没有被final修饰)的话,编译器会怎样进行处理呢?我们下面再看一个例子:

public static void main(String[] args) {String s1="a";String s2=s1+"b";String s3="a"+"b";System.out.println(s2=="ab");System.out.println(s3=="ab");
}

结果打印:

false
true

为什么会出现不同的结果?在Java中,String类型在使用==进行比较时,是判断的引用是否指向堆内存中的同一块地址,出现上面的结果那么说明指向的不是内存中的同一块地址。

通过之前的分析,我们知道s3会进行常量折叠,引用的是常量池中的ab,所以相等。而字符串s2在进行拼接时,表达式中引用了其他对象,不属于编译期常量,因此不能进行折叠。

那么,在没有常量折叠的情况下,为什么最后返回的是false呢?我们看一下这种情况下,编译器是如何实现,先执行下面的代码:

public static void main(String[] args) {String s1="my ";String s2="name ";String s3="is ";String s4="Hydra";String s=s1+s2+s3+s4;
}

然后使用javap对字节码文件进行反编译,可以看到在这一过程中,编译器同样会进行优化:

可以看到,虽然我们在代码中没有显示的调用StringBuilder,但是在字符串拼接的场景下,Java编译器会自动进行优化,新建一个StringBuilder对象,然后调用append方法进行字符串的拼接。而在最后,调用了StringBuildertoString方法,生成了一个新的字符串对象,而不是引用的常量池中的常量。这样,也就能解释为什么在上面的例子中,s2=="ab"会返回false了。

本文代码基于Java 1.8.0_261-b12 版本测试

公众号后台回复

"面试"---领取大厂面试资料

"导图"---领取24张Java后端学习笔记导图

"架构"---领取29本java架构师电子书籍

"实战"---领取springboot实战项目

关注公众号

有趣、深入、直接

与你聊聊技术

觉得有用,一键四连吧~

String s=a+b+c,到底创建了几个对象?相关推荐

  1. 【Java深入理解】String str = “a“ + “b“ + “c“到底创建了几个对象?

    String str = "a" + "b" + "c"到底创建了几个对象?这是我们在讨论中最经常遇到的一个问题同时也是面试题.我们都知道在 ...

  2. 工作10年后,再看String s = new String(xyz) 创建了几个对象?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 转自:艾小仙 这个问题相信每个学习java的同学都不陌生, ...

  3. String s = new String(123) 究竟创建了几个对象

    分享一波:程序员赚外快-必看的巅峰干货 前言 今天上班划水的过程中有人询问到这个问题,网上对于这个问题也有争议,有说创建了一个对象,有说两个,有说三个. 首先说三个的肯定是扯淡了,今天来讨论一下这条语 ...

  4. string s = new string(“xyz“);创建了几个对象_「005」-JavaSE面试题(五):String类

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [005] - JavaSE面试题(五):String类 第1问:String.StringBuf ...

  5. 众所周知,static修饰的成员只实例化一次,而string类型每次赋值都会重新创建一个实例,那么用static修饰string呢?...

    string 类型每次实例化都会重新创建一个实例: 解释:string 类型重载了运算符 "=" ,每次 "=" 操作都是一次 "new". ...

  6. String s=new String(abc)创建了2个对象的原因

    问题:String str=new String("abc"); 这行代码究竟创建了几个String对象呢? 相信大家对这道题并不陌生,答案也是众所周知的,2个. 接下来我们就从这 ...

  7. java string对象创建对象_Java String 创建了几个对象

    我们首先来看一段代码: Java代码 String str=new String("abc"); 紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢 ...

  8. new String(123) 创建了几个对象?

    String 对象可谓再熟悉不过了,与此相关的面试题经常会引出内存性能优化的问题,本篇主要以 new String("123") 创建了几个对象为例记录. 一.你能回答正确吗 St ...

  9. String s = new String(xyz);创建了几个对象?

    两个或一个都有可能 . "xyz"对应一个对象,这个对象放在字符串常量池,常量"xyz"不管出现多少遍,都是常量池中的那一个. new String每写一遍,就 ...

最新文章

  1. OpenCV学习中遇到的小问题—关于图像赋值
  2. Zend Framework一
  3. 图片视角转换 cv2.warpPerspective
  4. php childnodes,小tips:HTML DOM中的children和childNodes属性
  5. httpclient base64 文件上传_选择HttpClient还是OkHttp?
  6. Android Studio 导入新工程项目
  7. Bootstrap导航中禁用导航链接
  8. 交换机接口用了那几根线_【网工必知】图集:交换机接口知识大全
  9. POJ-1426 Find The Multiple
  10. 如何让百度快速收录自己的wordpress网站
  11. 记录一个可以word,xls,PDF互转思维导图的工具
  12. 抖音快手短视频去水印小程序解析接口API开发文档
  13. 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇二:基于OneNote难点突破和批量识别...
  14. 安卓系统加速_谷歌与安卓合作开发预警系统,安卓手机将成“迷你地震仪”
  15. 费控产品之易快报洞察解析
  16. 一篇关于保险与公积金的文章,看了绝对受益匪浅!!
  17. Set集合下的奇葩,TreeSet有序而且类型相同
  18. 原声JS瀑布流加延迟加载
  19. 【唐老狮】C#四部曲之C#基础:习题汇总
  20. 工贸企业重大事故隐患判定标准,自2023年5月15日起施行

热门文章

  1. 排序 (5)计数排序“概念”
  2. C++ Primer 5th笔记(chap 14 重载运算和类型转换)函数调用运算符
  3. [密码学] DES(一)
  4. Kubernetes中部署Docker registry2.7.1并通过containerd实现拉取镜像到应用Pod的部署
  5. GPTEE中的Crypto API的使用
  6. CVE-2014-4113_Win32k提权漏洞学习笔记
  7. svn回退到历史版本
  8. 【MySQL】 批量修改数据表和数据表中所有字段的字符集
  9. 【Linux】Centos7 下使用Apache 配置网站虚拟地址, 另附 laravel 虚拟地址配置
  10. 25、Java Swing文本编辑器的实现