String
1. 概念
String 类代表字符串,Java程序中所有的字符串文本都是这个类的实例,String是一个常量,它的值在定义后
不能更改
。
上图可以看出,String是被final
修饰的,不能被继承,可以被序列化,可以进行比较大小。
2. String在jdk9中的变更
2.1 源码对比
以下是JDK8的代码:
以下是JDK9的代码:
2.2 更改原因
JDK8中的String存储在char数组
中,使用的是UTF-8
(两个字节)进行编码的,但是字符串是堆使用率的重要组成部分,而且大部分的字符串对象都只是包含Latin-1字符
(一个字符),因此会导致存储的内部会有一半的闲置控制,导致浪费。
2.3 新版本实现
JDK9中的String是以byte数组
加上编码标志字段
来实现的,会根据字符串的具体内容,以ISO-8859-1/Latin-1
编码(一个字节)或者UTF-16
编码(两个字节),编码标志(coder字段
)将指明具体使用哪种编码方式编码——coder值为0时表示一个字节,coder值为1时表示两个字节。
2.4 优点
内存占用大幅度减少,也会较少GC的次数,也可以减少"Stop the Word"的频数,增加吞吐量,提升系统的性能。
3. 使用String
在Java代码中,String使用的频率是非常高的,我们一般有以下几种使用方式:
3.1 字面量
使用字面量的方法我们常用的使用String的方式,这种方式会将字符串中的值放在字符串常量池中,然后给变量赋值。
String name = "jack";
0 ldc #2 <jack> // 字符串常量池中取jack
2 astore_1
3 return
3.2 字面量拼接
字面量拼接的方式使用String,会先将拼接后的值放在字符串常量池中,然后再给变量赋值。
String name = "abc" + "bcd";
0 ldc #2 <abcbcd> // 字符串常量池中取abcbcd
2 astore_1
3 return
3.3 含有变量拼接
当字符串拼接时,有一个值是变量,则会使用
StringBuilder
对象类进行拼接,然后调用toString方法
将值保存在堆空间(此处是为了和字符串常量池区分开)中,因为toString方法
是使用数组进行创建的对象,所以常量池中不存在字符串的值。
String a = "123";
String b = "abc" + a;
0 ldc #2 <123> // 字符串常量池中的123
2 astore_1
3 new #3 <java/lang/StringBuilder> // 创建StringBuilder对象
6 dup
7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
10 ldc #5 <abc> // 字符串常量池中的abc
12 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> // apend方法
15 aload_1
16 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> // apend方法
19 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> // toString方法
22 astore_2
23 return
3.4 使用构造器创建
构造器创建对象在开发中是非常常见的,String对象也可以使用构造器进行创建。使用构造器创建String对象,首先会在堆内存中创建一个String的对象,然后具体的值也会放在字符串常量池中。
String name = new String("jack");
0 new #2 <java/lang/String> // 创建一个String对象
3 dup
4 ldc #3 <jack> // 字符串常量池中的jack
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return
3.5 构造器创建String拼接
如果是构造器创建的String对象进行拼接,首先会创建一个StringBuilder的对象,然后将两个具体的值拼接,然后调用
toString方法
。
String b = new String("123") + new String("abc");
0 new #2 <java/lang/StringBuilder> // StringBuilder对象
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String> // String 对象
10 dup
11 ldc #5 <123> // 字符串常量池中的123
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> // apend方法
19 new #4 <java/lang/String> // String 对象
22 dup
23 ldc #8 <abc> // 字符串常量池中的abc
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> // apend方法
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> // toString方法
34 astore_1
35 return
4. 字符串常量池
4.1 字符串常量池位置变化
-
在JDK6及以前,字符串常量池是在
永久代
中; -
在JDK7时,字符串常量池放在
堆空间
中; -
在JDK8中,
元空间
取代永久代
,但是字符串常量池还是保存在堆空间
中;
4.2 字符串常量池概述
-
字符串常量池是一个
HashTable
; -
可以使用
-XX:StringTableSize
来设置HashTable
的长度; -
在JDK6中,
HashTable
的长度是固定的,为1009
; -
在JDK7中,
HashTable
的默认长度是60013
; -
在JDK8及以后,
HashTable
的默认长度60013,长度可以设置的最小值为1009
;
5. intern()方法
我们看到在3.5节中的使用构造器创建然后进行String拼接的方式,字符串常量池是没有拼接后的字符串。如果我们也想把拼接后的字符串放在字符串常量池中,可以调用intern()
方法来完成。
jdk6中:将这个字符串对象尝试放入串池
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
jdk7以后:将这个字符串对象尝试放入串池。
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,则会把堆中对象的引用地址复制一份,放入串池,并返回串池中的引用地址
查看以下示例:
JDK6:
public static void main(String[] args) {
String b = new String("123") + new String("abc");
b.intern();
System.out.println(b == "123abc"); // false b对象是堆中的对象,123abc字符串是在字符串常量池中的对象地址
System.out.println(d == "123abc"); // true 调用intern方法尝试将123abc存放在字符串常量池中,然后将对象地址返回
}
JDK8:
public static void main(String[] args) {
String b = new String("123") + new String("abc");
String d = b.intern();
System.out.println(b == "123abc"); // true b是堆中的对象,调用intern()对象之后,将引用地址复制一份,放在字符串常量池中
System.out.println(d == "123abc"); // true d是字符串中指向堆空间的引用
}
讨论intern()方法的几种情况:
public static void main(String[] args) {
String a = new String("123"); // 1. 字符串常量池中存放123对象; 2. a指向堆内存中的String对象地址;
String b = a.intern(); // 3. 查看字符串常量池中是否存在123对象,此时是存在的,返回123字符串常量池中的对象地址;
System.out.println(a == b); // 4. a是堆内存中的地址 b是123字符串常量池中的对象地址 不一样 返回false
}
public static void main(String[] args) {
String a = new String("12") + new String("3"); // 1. 字符串常量池中不存在123对象地址; 2. a指向堆内存中的String对象地址;
String b = a.intern(); // 3. 因为123对象不存在,就将a的引用在字符串常量池中复制一份
System.out.println(a == b); // 4. a是堆内存中的地址 b是堆内存中对象地址中的引用地址 一样 返回true
}