关于字符串的两个问题

关于字符串的两个问题

如何计算一个字符串所占空间大小

这里只讨论磁盘中存储的字符串,而非程序语言中的字符串。
常用的字符编码有ASCII(1byte)、 Unicode、UTF-8(1-4byte)、GBK

ASCII:
ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
ASCII码的问题:
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。

Unicode(字符集规则,其字符集实现统称为Unicode字符集):
如它的名字所示,它容纳了几乎所有符号,现在的规模可以容纳100多万个符号。
问题:
1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。

UTF-8:
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
特点:
是一种单字符长度可变的编码方式,范围是1-6字节。
编码规则:
1) 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2) 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
3)根据上述两条规则,UTF-8中可以用来表示字符编码的实际位数最多有31位。

Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
——————–+———————————————
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,还是以汉字“严”为例,演示如何实现UTF-8编码。
已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
在字符映射表中,占3个字节的汉字52156个,占4个字节的汉字64029个。

在java中计算字符串所占磁盘空间:

这里得提一下不同编译器对处理的同种数据类型的长度是不同的,在一些没有操作系统的嵌入式计算机系统上,int的长度与处理器字长一致;有操作系统时,操作系统的字长与处理器的字长不一定一致,此时编译器根据操作系统的字长来定义int字长。但是,Java的基本数据类型的字长是与平台无关的,int型字长为32。

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 对应的utf-8编码:\u4f60\u597d\u5e08\u59d0\u002e
String s = "你好世界.";
char[] chars = s.toCharArray();
for (char b : chars
) {
System.out.println(Integer.toBinaryString(b));
}
assert s.getBytes("UTF-8")==13;
/*
转换后的Unicode编码
0100 1111 0110 0000 u4f60 你
0101 1001 0111 1101 u597d 好
0100 1110 0001 0110 u5e08 世
0111 0101 0100 1100 u59d0 界
0010 1110 u002e .

对应的UTF-8编码
11100100 10111101 10100000: e4bda0 你
.
.
.
*/

根据上述内容,字符串”你好师姐的长度应该为2+2+2+2+1=9”,但实际上却是13。

猜测:直接从代码看到的是unicode的编码,但实际进行储存的是UTF-8的编码,前4个汉字每个占了3个字节,而ASCII编码的.只占了一个字节。

所以判断一个字符串所占磁盘空间的大小的方法是:

  • 1.确认其编码字符集
  • 2.到对应字符集码表中查找对应的字符,并确认其所占空间大小
  • 3.加总

或者直接

1
int len=s.getBytes(CharSet).length;

各JDK版本中,String类有什么区别?

jdk1.7与jdk1.8/java7u40

原先的String类中有4个非静态变量:

  • char[] value用于存储字符串。

  • int offset用于记录字符串首字母在value数组中对应的下标。

  • int count用于记录字符串的长度。

  • int hash用于缓存该字符串的哈希值。

String.substring创建的String对象将和原String对象共享同一个内部变量char[] value,这样设计的好处是:
通过共享字符串节省内存开销。
String.substring方法的时间复杂度为O(1)。

问题:
这样的设计有可能会导致内存泄露:如果你从一个长度很长的String对象中提取出一个很短的子串,当这个String对象不再需要时(该对象静候GC回收),你的子串中还保持着这个String对象中存储着完整字符串的char[] value数组的引用。

而在jdk1.8中String类中不再有offset和count变量,也意味着char[] value不会被共享,String.substring现在是线性级的时间复杂度,不再是常数级的时间复杂度。
这种变化的好处是String对象占用的内存稍微少了一些(比以前少8个字节),同时确保String.substring方法不会导致内存泄漏。

jdk1.6与jdk1.7

jdk1.6中,intern()方法会把首次遇到的字符串实例复制到方法区中,然后返回方法区中这个字符串实例的引用。
jdk1.7中,intern()方法不再复制实例,只是在常量池中记录首次出现的实例的引用,因此如果该字符串是首次出现的,那么intern()返回的结果和StringBuilder创建的是同一实例。

其它版本过于久远不做考究

inner方法解读:

如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”.

大体实现结构:

JAVA 使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。

要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:

-XX:StringTableSize=99991