一、String底层结构改变--为节约内存
//JDK8及之前
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
...
}
//JDK9及之后
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte value[];
...
}
- JDK8及之前,String类的当前实现是将字符存储在char数组中,每个字符使用两个字节(16位)。
- 实践发现,字符串是堆的主要组成部分(占比25%)+大多数字符串对象只包含拉丁字符(Latin-1),这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费,因此JDK9及以后改为byte数组。
- 之前 String 类使用 UTF-16 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标识存储。该编码表示如果你的字符是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集,比如UTF-8,仍然用两个字节存。
- 同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改。
String:不可变字符串
StringBuilder:可变字符串
StringBuffer:可变字符串+线程安全(各方法添加synchronized)
1.1 一个面试题:jdk8中,一个字符串包含了汉字,计算该字符串占用多少个字节?
在Java中:
1字符=2字节,1字节=8位
英文和数字占一个字节,中文占2个字节。
如果直接使用str.length()计算字符串占用多少个字节,得出的长度往往是不准确的,例如:
public static void main(String[] args) {
String str= "Great大中国";
int length = str.length();
System.out.println(length);
}
计算结果为8,是错误的。
正确计算方法如下:
/**
* 计算字符串占用了多少个字节
* 1字符=2字节,1字节=8位
* 英文和数字占一个字节,中文占2个字节。
*/
public static int getStrlength(String str) {
int strLength = 0;
//chinese表示常见的汉字【一-龥】
String chinese = "[\u4e00-\u9fa5]";
/* 获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 */
for (int i = 0; i < str.length(); i++) {
/* 从字符串中获取一个字符或汉字 */
String temp = str.substring(i, i + 1);
/* 判断是否为中文字符 */
if (temp.matches(chinese)) {
/* 中文字符长度为2 */
strLength += 2;
} else {
/* 其他字符长度为1 */
strLength += 1;
}
}
return strLength;
}
二、String长度限制
- 编译期的限制:字符串的长度不能超过65534。
- 运行时限制:字符串的长度不能超过2^31-1。
长度:指String.length()的值。String底层不管是byte数组,还是char数组,都不会影响长度,会影响的是占用的内存空间。
public static void main(String[] args) { String str="abcde"; System.out.println(str.length());//5 }
2.1 编译期限制说明
public static void main(String[] args) {
String str="abcde";
System.out.println(str);
}
如上定义的字符串常量“abcde”会被放入方法区的常量池中,编译期Stirng 长度之所以会受限制,是因JVM规范对常量池有所限制。常量池中的每一种数据项都有自己的类型,Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示,CONSTANT_Utf8的数据结构如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
u1 bytes[length]这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大长度。length 的类型是u2,u2是无符号的16位整数,2^16-1=65535,所以上面byte数组的最大长度是65535。
2.1 运行期限制说明
public String(char value[], int offset, int count) {
...
}
运行时的限制主要体现在 String 的构造函数上,count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1,所以在运行时,String 的最大长度是2^31-1。
2^31-1对应占用的内存大小:
JDK8中LATIN1字符占用的内存=(2^31-1)*16/8/1024/1024/1024 = 4GB【长度*每个长度对应的位数/8/1024/1024/1024】
JDK9中LATIN1字符占用的内存=(2^31-1)*8/8/1024/1024/1024=2GB
三、对String不可变性的理解--【String代表不可变的字符序列,简称:不可变性】
1、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
2、当对字符串重新赋值、replace()方法替换、拼接等任何形式的修改时,需要重新分配内存区域赋值,不能使用原有的value进行赋值。
举例1:
@Test
public void test(){
String str1="abc";//字面量定义的方式,"abc"存储在字符串常量池中
String str2=new String("abc");
String str3=new String("cde");//共创建2个对象:在常量池中创建字符串cde+在堆中创建一个String对象str3
System.out.println(str1==str2);//false
}
例1图分析
举例2:
@Test
public void test(){
String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);// false
System.out.println(s1);//hello
System.out.println(s2);//abc
}
例2图示分析
举例3:
public class StrDemo {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StrDemo ex = new StrDemo();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);//good
System.out.println(ex.ch);//best
}
}
例3图示分析
四、字符串拼接
字符串拼接结论:
- 常量与常量的拼接结果在常量池,原理是编译期优化。
- 拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
4.1 字符串拼接的底层原理
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的s1 + s2 的执行细节:(变量s是我临时定义的)
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 约等于 new String("ab"),但不等价,原因是:存在字符串字面量时,才会在常量池生成
补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
*/
String s4 = s1 + s2;//
System.out.println(s3 == s4);//false
}
图解
4.2 举例
举例1--new String(“a”) + new String(“b”) 会创建几个对象?
* 对象1:new StringBuilder() //字符串拼接原理是StringBuilder,所以需要该对象
* 对象2: new String("a")
* 对象3: 常量池中的"a"
* 对象4: new String("b")
* 对象5: 常量池中的"b"
* 对象6-str: StringBuilder的toString()会new一个String对象
*
* 强调一下,toString()的调用,在字符串常量池中,没有生成"ab",“ab”在堆中
*
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}
举例2--常量拼接
@Test
public void test1(){
String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
/*
* java编译成.class再执行.class,编译后的结果如下:
* String s1 = "abc";
* String s2 = "abc"
*/
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
举例3--常量拼接
- 在 Java 中使用 final 关键字来修饰常量。
- 字符串字面量也在常量池中。
@Test
public void test4(){
final String s1 = "a";//使用final修饰的s1为常量
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
举例3的汇编代码
0 ldc #14 <a>
2 astore_1
3 ldc #15 <b>
5 astore_2
6 ldc #16 <ab>
8 astore_3
9 ldc #16 <ab> //s4在编译后直接优化为使用ab字面量赋值
11 astore 4
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 return
举例4--字符串拼接
@Test
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//编译期优化
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
}
结合jvm一起学习:JVM-01-JVM基础-03-运行时常量池