深度理解字符串String
# 深度理解字符串String
# 1 介绍
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
...
}
2
3
4
- 实现了
Serializable
接口,表示字符串支持序列化 - 实现了
Comparable
接口,表示字符串可以比较大小 - 定义
final char value[]
,代表不可变的字符序列 - 用
final
修饰 class,代表类不可以被继承
# 2 创建对象
# String的不可变性
通过字面量创建对象,此时的字符串值声明在字符串常量池中。
String s1 = "abc";
String s2 = "abc";
s2 = "def";
String s3 = s1 + "123";
String s4 = s1.replace('a', 'c');
2
3
4
5
6
注意:为了简化叙述,上图没有将「堆」画出来。
字符串常量池中不会存储相同内容的字符串。
- 当对字符串重新赋值时,不会修改字符串常量池中原有的字面量。
- 当对字符串进行连接操作时,也不会修改字符串常量池中原有的字面量。
- 当调用 String 的
replace()
方法修改指定的字符或字符串时,也不会修改字符串常量池中原有的字面量。
# String对象的创建
两种方式:
- 通过字面量定义:字面量声明在方法区的字符串常量池中;
- 通过构造器:对象在堆中开辟空间,字面量声明在方法区的字符串常量池中。
测试代码:
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s3 == s4); // false
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true
System.out.println(s3.equals(s4)); // true
2
3
4
5
6
7
8
9
10
11
==
:比较的是引用对象的地址值。
equals
:String 重写了 equals()
方法,比较的是字符串的值。
所以,通过 String s3 = new String("abc")
方式创建了几个对象呢?
当字符串常量池中存在“abc”字面量时,创建一个对象,堆中的字符串对象。
当字符串常量池中不存在"abc"字面量时,创建两个对象,一个是堆中的字符串对象,另一个是 char[] value
对应的常量池中的字面量。
# String的拼接
测试代码
String s1 = "abc";
String s2 = "123";
String s3 = "abc123";
String s4 = "abc" + "123";
String s5 = s1 + "123";
String s6 = "abc" + s2;
String s7 = s1 + s2;
String s8 = s7.intern();
System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s3 == s8); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当常量与常量拼接的时候结果还是在常量池,且常量池不会存在相同内容的常量;
当其中一个是变量,相当于自动调用了构造器的方法创建对象,拼接的结果将在堆中;
如果拼接的结果调用 intern()
方法,返回值就是在常量池中。
# 字符串常量池
字符串常量池是 JVM 为了提升性能和减少内存消耗,针对于字符串专门开辟的一块区域,主要是为了避免字符串被重复创建。
JDK1.6:字符串常量池在方法区中
JDK1.7:字符串常量池在堆中
JDK1.8:字符串常量池在方法区中,但是此时方法区的实现为元空间。
# 3. 常用方法
# intern()
String::intern()
是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中整个字符串的 String 对象的引用;否则,会将此String对象包含的字符串添加到常量池,并且返回此 String 对象的引用。
String s1 = new StringBuffer("re").append("dis").toString();
System.out.println(s1 == s1.intern()); // true
String s2 = new StringBuffer("ja").append("va").toString();
System.out.println(s2 == s2.intern()); // false
2
3
4
5
为什么 Java 不同呢?因为有一个初始化的 Java字符串(JDK自带),在加载 sun.misc.Version
这个类的时候进入常量池。
System 类中有 initializeSystemClass
方法,该方法调用 sun.misc.Version.init()
,Version 这类中有常量 Java。
# equals()
equals()
是 Object 类的方法,它用来比较对象的内存地址。
Object 类是一切类的父类,所以 String 内也包含equals()
方法并且重写了这个方法,用于比较两个字符串的字符是否相同。
# 4. 不可变性
⭐ 不可变性的定义
对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。
这就意味着,一旦我们将一个对象分配给一个变量,就无法再通过任何方式更改对象的状态了。
⭐ 不可变的原因
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
2
3
4
5
6
- 用 final 修饰 String 这个类,不可被继承,防止被他人继承后修改
- 用 final 修饰 char 数组,数组的引用无法被修改,并且内部没有提供修改 char 数组的方法。
- 在源码里避免修改 char 数组,并且 char 数组,对 String 的修改操作都会创建一个新的 String 对象。
⭐ 设计为不可变的原因
- 设计为不可变才能使用字符串常量池。如果可变的话,当一个字符串的内容被修改后,其他相同的字符串都会被修改,因为他们引用了字符串常量池中的同一个字面量。
- 安全问题,String 是最常用的数据类型,String 被许多 Java 类库用来作为参数,如果 String 不是固定不变的,将会引起各种安全隐患。
# 4 常见问题
# String、StringBuffer、StringBuilder对比
StringBuffer
与 StringBuilder
都是继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中使用字符数组保存字符串,但是没有用 final
和 private
关键字修饰,最关键的是 AbstractStringBuilder
类还提供了很多修改字符串的方法,比如append方法。
StringBuffer
与 StringBuilder
的构造方法都是调用父类构造方法,也就是 AbstractStringBuilder
实现的。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
2
3
4
5
6
7
8
线程安全性:
- String:对象是不可变的,线程安全。
- StringBuffer:对方法加了同步锁或调用的方法加了同步锁,所以是线程安全的。
- StringBuffer:并没有对方法进行同步加锁,所以是非线程安全的。
性能:
- String:每次改变String都会创建一个新的String对象,然后指针指向新的String对象。
- StringBuffer:每次对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
- StringBuilder:相比StringBuffer的性能提升10%-15%,但线程不安全。
总结:
- 操作少量数据:String
- 单线程操作字符串缓冲区下大量数据:StringBuilder
- 多线程操作字符串缓冲区下大量数据:StringBuffer
# 字符串拼接
+
拼接方法,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个String对象。在循环中使用 +
进行字符串的拼接的时候,编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
所以,在循环中拼接对象的时候一定要在循环开始前创建一个 StringBuilder
对象。