这个特性是JDK9放出来的,主要是为了节约String占用的内存。
众所周知,在大多数Java程序的堆里,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节/字符,它也要按照2字节/字符进行分配,浪费了一半的内存空间。
JDK9是怎么解决这个问题的呢?一个字符串出来的时候判断,它是不是只有Latin-1字符,如果是,就按照1字节/字符的规格进行分配内存,如果不是,就按照2字节/字符的规格进行分配(UTF-16编码),提高了内存使用率。
举个类似的例子说就是,工厂生产的有多种型号的零件,一批零件必须装入同一种型号的包装发货,零件有大号和小号两种,而且绝大多数都是小号,之前包装的时候不管一批零件是大是小全部放到大号的包装里,造成了空间浪费,现在如果一批里面有大号就全装大号,如果没有大号就全装小号,提升了空间利用率。
这种做法带来的好处是显而易见的:
- 原本一个仓库装不下的零件,现在可以装下了(用更少的内存跑更大的应用)
- 原本仓库一天往外运一次,现在可以一天半甚至两天运一次(减少GC次数)
为什么用UTF-16而不用UTF-8呢,这就要从这两个字符集的设计说起了。
UTF-8实际上是对空间利用效率最高的编码集,它是不定长的,可以最大限度利用内存和网络。它是小包装装得下就用小包装,小包装装就看看能不能用大包装装,大包装都装不下就用超大包装。但是这种编码集只适用于传输和存储,并不适合拿来做String的底层实现。这是为什么呢?
因为String有随机访问的方法,所谓随机访问,就是charAt、subString这种方法,随便指定一个数字,String要能给出结果。如果字符串中的每个字符占用的内存是不定长的,那么进行随机访问的时候,就需要从头开始数每个字符的长度,才能找到你想要的字符。试想,如果大小包装混装,你想拿到第N个零件,你必须一个一个数包装盒,数到N,才能找到你要的零件。而如果包装是一样的大小,你就可以通过简单的计算知道你要找的零件距离你有多少远,直接过去拿就行了。
但是又有人会问了,UTF-16也是变长的啊,一个字符可能在UTF-16里面占用4个字节咧。是的,是的,UTF-16是变长的,但这是在现实世界里是这样。在java的世界里,一个字符(char)就是2个字节,从\u0000到\uFFFF,占4个字节的字符,在java里是用两个char来存储的,而String的各种操作,都是以java的字符(char)为单位的,charAt是取得第几个char,subString取的也是第几个到第几个char组成的子串,甚至length返回的都是char的个数,从来没有哪个方法可以让你“通过下标取出字符串中第几个'现实意义'中的字符”,所以UTF-16在java的世界里,就可以视为一个定长的编码。
还是工厂的例子:如果哪天出现了一个超大的零件咋办?简单,我这就只有大号包装,一个大号的包装都装不下的时候怎么办呢,把超大号的零件切成两份就行了,用两个大号包装去装,出厂也视为两个零件。
关于这种超大零件,可以试着跑以下下面的代码体会
public class StringTest { public static void main(String[] args) { System.out.println("🀎".length()); System.out.println("🀎".charAt(0)); System.out.println("🀎".charAt(1)); } }