Java 字符串操作的底层原理解析

开门见山

  • 常量与常量的拼接结果在常量池,原理就是编译器优化
  • 常量池中不会存在相同内容的常量
  • 只要拼接的元素中,有一个是变量,结果就是在堆中,变量拼接的原理是StringBuilder
  • 如果拼接的结果调用了intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

常量拼接

String s1 = "a" + "b" + "c";
String s2 = "abc";//abc一定是放在字符串常量池中的,此地址赋给s2

System.out.println(s1 == s2);
System.out.println(s1.equals(s2));//值比较一定为true

//实际上编译器将做出如下编译优化
String s1 = "abc";//abc作为字面量将被放入常量池
String s2 = "abc";//s2相当于使用了已有的abc

//所以System.out.println(s1 == s2); 输出为true

拼接元素中有变量

String s1 = "a";
String s2 = "b";
String s3 = "c";

//如下拼接相当于在堆空间中new String(),并非在字符串常量池,只不过字符串对象结果为abc
String s4 = s1 + "b" + "c";
String s5 = "a" + s2 + s3;
String s6 = a + b + c;

//所以 s4 == s5 false, s4 == s6 false, s5 == s6 false

intern()方法

intern()是判断字符串常量池中是否存在该值,如果存在,则返回常量池中该值的地址;如果字符串常量池中不存在该字符串值,则在常量池中加载一份该字符串常量,并返回此字符串常量的地址

String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = "abc";//此时字符串常量池已经存在abc

//如下拼接相当于在堆空间中new String(),并非在字符串常量池,只不过字符串对象结果为abc
String s5 = s1 + "b" + "c";
String s6 = "a" + s2 + s3;
String s7 = a + b + c;

String s8 = s6.intern();
//s8 == s4 true

String.intern()能保证在字符串常量池中创建唯一的字符串常量。

字节码指令角度来看字符串的拼接

s1+s2的字节码指令分析

  1. new StringBuilder()。在JDK5之前是使用StringBuffer,JDK5之后使用的是StringBuilder
  2. aload_1获取操作数栈上索引为1的操作数,也就是s1,实际上在局部变量表中的s1指向的是堆中字符串常量池中的"a"
  3. StringBuilder.append("a")
  4. 同step2一样,获取了操作数栈中的s2的值
  5. StringBuilder.append("b")
  6. StringBuilder.toString()返回结果

优化建议

  • 基于该实例,所以在大量操作字符串的拼接的时候(循环中拼接字符串),不要直接使用变量进行拼接,由于每次都会创建一个StringBuilder,并且每次都会调用StringBuilder.toString()方法,toString方法实际还是创建new了一个String对象。而是声明一个StringBuilder对象去append字符串,这样的效率远高于字符串拼接的方式。
  • 同理可以,大量的字符串拼接可能会造成堆空间内存的浪费,更容易会引起GC的发生
  • 在实际的开发中,如果可以基本确定循环拼接的字符串长度不高于某个限定值,建议使用构造器StringBuilder(int capacity),显式的指定StringBuilder底层的char[]数组的长度,以免过长的字符串拼接导致StringBuilder的扩容情况发生(会创建一个新的更大的char[]进行拷贝)


字符串拼接操作不一定使用的是StringBuilder

如果参与拼接的两个元素是字符串常量或者常量引用(下面final修饰之后就是常量引用了),则仍然使用编译器优化,并非创建StringBuilder

new String("ab")创建了几个对象

一个存放在堆中new的String对象,一个字符串常量池的常量字符串。

new String("a") + new String("b")创建了几个对象

  1. new StringBuilder(),用作字符串拼接
  2. new String()
  3. 字符串常量a
  4. new String()
  5. 字符串常量b
  6. StringBuilder.toString(),也会new一个String对象

JDK7之前与之后关于intern()的区别

  • JDK6中,将该字符串对象放入字符串常量池中
    • 如果池中有,则不会放入,返回串池中已有的对象的地址
    • 如果池中没有,则会把此字符串对象赋值一份,放入池中并返回该池中该对象的地址
  • JDK7之后
    • 如果池中有,则不会放入,返回串池中已有的对象的地址
    • 如果没有,则会把对象的引用地址复制一份,放入字符串常量池中,并返回池中的引用地址

根据以上解释尝试理解如下代码在JDK6和JDK7之后的区别

String s3 = new String("1") + new String("1");//s3作为一个变量,指向堆空间中的String对象,值为11,然后字符串常量池中并没有11
s3.intern();//在字符串常量池中生成11
String s4 = "11";//使用的是上一行代码在字符串常量池中生成的11的地址
System.out.println(s3 == s4);

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值