结论是jdk1.8及以前String使用的是char数组,jdk1.9及以后使用的是byte数组。因为开发人员发现人们大多数使用的String还是拉丁字符而之前使用的char数组每一个char占用两个字节而拉丁字符只需要一个字节就可以存储,剩下的一个字节就浪费了,造成gc的更加频繁。因此在jdk9中将String底层的实现改为了byte数组。
在openjdk的开发日志中也是标注了这一改动以及改动的动机。
我们再去jdk中验证一下,先是jdk8
确实底层使用的是char数组。
而到了jdk9时,我们再看
就变成了byte数组。那么问题就来了,String是怎么实现存储汉字的呢?我们将jdk9的源码往下翻看会发现有一个属性
1 2 3 4 5 6 7 8 9 10 11 12 | /** * The identifier of the encoding used to encode the bytes in * {@code value}. The supported values in this implementation are * * LATIN1 * UTF16 * * @implNote This field is trusted by the VM, and is a subject to * constant folding if String instance is constant. Overwriting this * field after construction will cause problems. */ private final byte coder; |
什么?coder?程序员被封装成String的一个属性了?没有买卖就没有伤害,每创建一个String就会有一个程序员失去自由…(手动狗头)
回归正题,在看到上面的注解时,眼前一亮 出现了Latin和UTF16的字眼,我们知道这就是我们要找的那个“她”。注解上说明了该属性攻两个实现Latin和UTF16,这时候我们就知道要去看构造器了,所有的含参构造器最终都会调用一个源构造器。
看构造器之前我们先看一个属性COMPACT_STRINGS翻译过来就是压缩字符串,默认静态代码块赋值true;很明显这个就是决定该String对象是否采用压缩策略的关键属性。
1 2 3 4 5 | static final boolean COMPACT_STRINGS; static { COMPACT_STRINGS = true; } |
接下来就是String的构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | String(char[] value, int off, int len, Void sig) { //空判断 if (len == 0) { this.value = "".value; this.coder = "".coder; return; } //如果开启压缩字符串策略那么就尝试压缩 if (COMPACT_STRINGS) { byte[] val = StringUTF16.compress(value, off, len); if (val != null) { this.value = val; this.coder = LATIN1; return; } } //如果没有压缩成功并返回就直接设置为UTF16,我们点进UTF16我们也可以看到下面这两个属性 this.coder = UTF16; this.value = StringUTF16.toBytes(value, off, len); } //点进上面的UTF16找到的两个属性,也就证实了coder属性上面的注解(两个实现) static final byte LATIN1 = 0; static final byte UTF16 = 1; |
最后分享java常见的面试问题,考考大家
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | String s0="java No.1"; String s1="java "; String s2="No.1"; String s3="java "+"No.1"; String s4=s1+"No.1"; String s5=s1+s2; System.out.println(s3==s0); System.out.println(s4==s0); System.out.println(s5==s0); System.out.println(s5==s4); System.out.println("-----------------------------"); final String s6="I am "; final String s7="guYue"; String s8="I am guYue"; String s9=s6+s7; System.out.println(s8==s9); |
结果如下
大家都答对了吗?
我总结了一下共有以下几种情况:
① “?”+"?"
底层直接优化为“??”
我们使用jclasslib查看以下代码的字节码
1 2 3 4 | String s0="java No.1"; String s1="java "; String s2="No.1"; String s3="java "+"No.1"; |
看不懂字节码没有关系,很简单的 比如
1 . ldc就是从常量池中加载字符串到操作数栈中,
2 . astore_1就是将操作数栈中的引用类型变量放到局部变量表中下标为1的位置,a表示引用变量,store表示存储到局部变量表,_1表示存储在句柄变量表哪个位置。比如我们现在要将int i=2;放到局部变量表的下标为2的位置,那么字节码指令就是istore_2.
3 . 很明显后面<>包起来的就是具体的字符串。
所以我们现在对应起来看发现s0和s3的加载过程完全相同
②“?”+si ,si+"?" , si+sj
我把这三种归为一类因为都有引用类型加入运算,这时候底层会new一个StringBuilder再调用append方法,最后调用toString方法完成拼接。
我们开如下代码的字节码文件
1 | String s4=s1+"No.1"; |
invokexxxxxx就是调用方法的意思,其实自己猜也能猜出来的。
只要看<>里面的数据就可以知道这一步字节码在干什么了。跟我述说的过程完全一致。
我们最后看一下StringBuilder的源码就知道了,他是new 了一个String并返回(new String()和“ ”这两个不同大家肯定知道的吧,前面举例子的时候都忘了,我就不加了哈,默认大家都知道的),所以肯定不是同一个对象。(还有什么equals方法的判断我也就不列出来侮辱大家智商啦,那毫无意义)
1 2 3 4 5 | @Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); } |
③final s1+final s2
这是两个final修饰的String引用的拼接(这个我记忆中好多博客中都没有列举过),我们看如下代码的字节码
1 2 3 4 | final String s6="I am "; final String s7="guYue"; String s8="I am guYue"; String s9=s6+s7; |
很明显最后两个操作的字节码操作过程都是相同的,即直接从字符串常量池取。所以这里是true。
能看到这里的小伙伴,本人表示衷心的感谢!!!
Thanks you!