一翻开Java面试题,基本上都会有考察字符串的不可变性,new String()和“”的区别,字符串+内部实现等相关问题,您可能也知道常量池,看了一些堆栈图,了解了上述答案,可是过阵子可能就会模糊,因为也许不懂它为什么要这样设计。
本文将从为什么这么设计,到你也是这样设计的认知过程,带你去了解一下Java字符串,有了这个过程,你就很难忘记,因为它本身就该如此。
所谓常量池
String s1 = "abc";
String s2 = "abc";
String s3 = "abc";
String s4 = "abc";
复制代码
上述代码我们不考虑什么“abc”几份,先考虑一下如果你有这样的代码,你该怎么优化,很显然,这里的“abc”都是一样的,而我居然傻啦吧唧的写了4份,继续下去还不得累死,所以优化如下:
// 建立字符串常量
String constant = "abc";
String s1 = constant;
String s2 = constant;
String s3 = constant;
String s4 = constant;
复制代码
上述优化是很自然的,那么javac编译时也不傻,即使我们没有做上述优化,javac编译时也会建立一个UTF-8常量,形式如下:
Constant pool:
……
#31 = Utf8 abc
……
复制代码
而后s1,s2,s3,s4最终值都会引用到这个#31,你会发现,这和你自己优化所建立的字符串常量是一个意思,没有任何特殊之处,对的,这就是很自然的设计,所以每个.java文件编译成.class文件的时候,都会有一个块重要的数据,叫做Constant pool(常量池),就是用来存储这些所谓的常量,比如上述的“abc”,当然你的类名,方法名,字段名,变量名最终都会存储为UTF-8常量,还有其它,这里就不复述了,通过javap -v xxx.class可以很直观的看到所有常量池的数据。
回到面试题,s1,s2,s3,s4是一个值么?答案是废话啊,不然我要写4份么,既然是一份,那么地址和值怎么可能不同呢?
在实际应用中,我们通常会建立一个常量类,用来表示常量,例如:
public class Constant {
public static final String STR_CONST = "abc";
}
复制代码
其它任何类的方法使用都调用Constant.STR_CONST即可,我们都知道这样“abc”只有一份,而且不可变,那么很显然, Jvm也不傻,当它把所有.class中的Constant pool(常量池)数据放入Jvm内存中一个专门区域(运行时常量池),发现有多份"abc"的时候,Jvm和我们一样只保存一份,就如同上述STR_CONST一样,其它引用这一份即可。
所谓字符串不可变性
当我们建立了常量:
public static final String STR_CONST = "abc";
复制代码
为什么要加final?因为我们知道很多地方引用了这个STR_CONST,如果不加final,STR_CONST一旦改变,所有引用这个值得都改变了,增加了不可确定性,程序会不可控。
通过上节你知道,Jvm会把“abc”作为常量,只存储到运行时常量池一份,就如同我们建立的STR_CONST一样,那么我们都知道要设置final让其不可变,Jvm当然也知道,Jvm中的"abc"被很多地方引用,当然也要不可变,这样系统才健硕,这也是很自然的一个过程,所以类似下面的操作。
String constant = "abc";
constant.toUpperCase()
constant.substring(2);
constant = constant + "ef";
……
复制代码
都是不会改变"abc"本身的,就如同我们会用STR_CONST来进行其他操作,但是却不会改变STR_CONST本身值一样,Jvm同样不傻,也不会改变"abc"本身的,这是一个道理。
其实上述操作本质都是创建了一个新的String对象,new String(...),而不是对“abc”的操作,那么new有什么不一样呢?
String constant = "abc";
String str = new String("abc")
复制代码
new String("abc")和常量池完全没关系了么?,并不是,new String("abc")里的"abc",其实还是引用常量池的"abc"。
但是因为是new, 创建了一个新对象,和所有创建对象一样都经过了new(创建对象,分配堆空间,为堆空间初始化零值),invokespecial(调用构造方法,写入实例变量值)等指令,最终在堆有才有了一份String对象,而且str引用它。
我们的"abc"是存储在运行时常量池的,被很多地方引用,而str是一个对象,存储在堆中,它们属于不同的内存空间,所以单纯比较地址(==)是肯定不同的。
所谓+内部实现
String constant = "abc";
String result = constant + "ef";
复制代码
我们现在都知道 + 内部其实是调用StringBuilder来实现的,StringBuilder内部有一个可扩容的char[] value,初始是16,上述+"ef",不需要扩容,16完全能满足。
看起来大材小用,然而如果让你提供一个+操作,当然不能只考虑上述这么简单的情况,多次+操作呢,循环+操作呢,StringBuilder作为一个通用的实现,就是为了一次解决上述各种问题的,我们实际开发中尽可能的让一个方法能兼容一种操作的多种情况,那么Javac编译也是相同的,一遇到+号,就直接编译成StringBuilder来操作,这样我们实际开发中的各种+操作,最终都将是高效的,换做我们不也会这样设计么。
最后
我们开发学习中会遇到很多所谓的术语,它们提炼的很好,然而通常都没有配以很好的解释,最终只记得术语,其实,拨开术语的面纱,大多数术语内部思想是很简单易懂的,毕竟也是人通过经验总结的,当理解了内部思想后,术语什么的只是一段文字了而已了。