Java String类详解
String类的声明
1 | public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc{ |
- 有
final
关键字修饰该类,因此不会再被子类继承。- 实现了字符串常量池
- 保证线程安全
- 保证
hashcode
的不可变性
- 实现了
Serializable
接口,保证String
类可以进行序列化 - 实现了
Comparable
接口,可以不再使用==
进行两个字符串是否相等,而是使用compareTo()
方法进行比较 String
,StirngBuffer
,StringBuilder
都实现了CharSequence
接口,由于String
类不可变,所以如果遇到字符串拼接就可以使用StringBuffer
或StringBuilder
String底层为什么要用byte数组而不是char数组
String从使用char数组改成byte数组是在Java 9中引入的一个重要优化。让我来详细解释这个变化的原因:
主要原因是为了节省内存空间。这里涉及到几个关键点:
- 字符编码的演变:
- 在早期,char类型使用16位(2字节)来存储一个字符,这是基于Unicode的UCS-2编码设计的
- 当时认为16位(可表示65536个字符)足够表示所有字符
- 但随着Unicode的发展,出现了更多的字符,需要超过16位才能表示
- 内存使用效率:
1
2
3
4
5
6// 旧版实现
private final char[] value; // 每个字符占2字节
// 新版实现
private final byte[] value; // 每个字符可能占1字节或2字节
private final byte coder; // 用来标识编码方式
- 在大多数情况下,字符串只包含Latin-1字符(如英文字母、数字等),只需要1个字节就能表示
- 使用char[]时,即使存储普通的ASCII字符也要占用2个字节
- 改用byte[]后,对于Latin-1字符串只需要1个字节/字符,可以节省50%的内存
- 编码方案:
- 新的实现使用一个coder字段来标识当前字符串使用的编码方式:
- LATIN1(0):表示字符串只包含Latin-1字符,每个字符用1个字节存储
- UTF16(1):表示字符串包含其他Unicode字符,每个字符用2个字节存储
- 系统会根据字符串的内容自动选择最优的存储方式
- 性能影响:
1
2
3
4public int length() {
// 根据coder返回正确的长度
return value.length >> coder; // LATIN1时除以1,UTF16时除以2
}
- 虽然增加了一些判断逻辑,但内存使用的优化带来的好处远大于这些微小的性能开销
- 对于大量使用字符串的应用来说,这种优化可以显著减少内存占用
这个改变是一个很好的例子,说明了如何在保持向后兼容性的同时,通过改进内部实现来优化性能。对于使用String的代码来说,这个变化是完全透明的,但却能带来显著的内存节省。
String为什么不可变
1 | public final class String |
- String类被
final
修饰,这就决定了不会再有子类重写Stirng
类的方法,改变它的行为 - String类的数据存储在
byte[]/char[]
数组中,该数组也被final
关键字修饰,保证了该字符串数组不会再被改变了 - 该数组被
private
修饰并且不提供set
方法,保证了Stirng
类外也无法对String
进行修改
还有一个比较重要的点就是不论存该数组被private
修饰并且不提供set
方法,保证了Stirng
类外也无法对String
进行修改储的是char[]
还是byte[]
,String类都没有提供操作这个数组的api,因此保证了不可变性
String不可变有什么好处
数据安全
String
经常用来存储用户名密码等敏感信息,将String
类设计成不可变就可以防止代码创建修改这些String
值线程安全
多个线程可以同时安全访问同一个String
对象,不需要再用锁机制去防止多线程使用的安全性可以通过常量池技术节省内存空间
String
类可以算得上Java
中最常用的引用数据类型。使用常量池,当创建字符串字面量可以安全地重用常量池中已有地String
对象。通过避免重复创建节省内存HashMap 缓存
String经常被用作HashMap等基于哈希的集合中的键。由于String是不可变的,它的哈希码只需要计算一次就可以被缓存起来,这样可以提高后续基于哈希的操作的性能。如果String是可变的,每次修改都需要重新计算哈希码。
String, StringBuilder, StringBuffer有什么区别
让我从不同角度详细解释String、StringBuffer和StringBuilder这三个类的异同:
可变性的区别
String的不可变性:
String是不可变的,这意味着每次对String的修改实际上都会创建一个新的String对象。例如:
1 | String str = "Hello"; |
在这个例子中,原始的”Hello”对象仍然存在于内存中,而str变量现在指向了一个新的”Hello World”对象。
StringBuffer和StringBuilder的可变性:
这两个类都是可变的,它们的操作会直接修改对象内部的字符数组,而不是创建新对象:
1 | StringBuilder builder = new StringBuilder("Hello"); |
线程安全性
String的线程安全:
由于String是不可变的,所以它天然是线程安全的。多个线程可以同时读取同一个String对象而不会产生问题。
StringBuffer的线程安全:
StringBuffer的方法都使用synchronized关键字进行同步,这使得它是线程安全的,但也因此带来了性能开销。适合在多线程环境下使用:
1 | public synchronized StringBuffer append(String str) { |
StringBuilder的非线程安全:
StringBuilder没有进行同步处理,因此在单线程环境下性能最好,但在多线程环境下不安全:
1 | public StringBuilder append(String str) { |
性能比较
让我们通过一个简单的例子来比较它们的性能差异:
1 | // String拼接 |
在这个例子中:
- String的方式会创建大量临时对象,性能最差
- StringBuilder因为没有同步开销,性能最好
- StringBuffer因为有同步开销,性能介于两者之间
共同点
虽然这三个类有很多区别,但它们也有一些重要的共同点:
都实现了CharSequence接口,这意味着它们都可以存储和操作字符序列:
1
2
3CharSequence cs1 = "Hello"; // String
CharSequence cs2 = new StringBuilder("Hello"); // StringBuilder
CharSequence cs3 = new StringBuffer("Hello"); // StringBuffer都提供了相似的基本操作方法,如字符串拼接、子字符串获取、长度查询等:
1
2
3
4// 这些操作在三个类中都可以实现,只是实现方式和性能特征不同
str.substring(0, 5);
builder.substring(0, 5);
buffer.substring(0, 5);
使用建议
基于以上分析,我建议:
- 如果字符串是固定的,使用String
- 如果是单线程环境下的可变字符串,使用StringBuilder
- 如果是多线程环境下的可变字符串,使用StringBuffer
String类使用了哪些设计模式
String类的实现主要用到了两个重要的设计模式:享元模式(Flyweight Pattern)和适配器模式(Adapter Pattern)。让我来详细解释这两种模式在String类中的应用:
1. 享元模式(Flyweight Pattern)
享元模式的核心思想是通过共享来支持大量细粒度对象的复用,从而减少内存使用。在String类中,这个模式主要体现在字符串常量池的实现上:
1 | String s1 = "hello"; // 创建一个字符串字面量 |
当我们创建字符串字面量时,Java会首先检查字符串常量池中是否已经存在相同的字符串:
- 如果存在,直接返回常量池中的引用
- 如果不存在,则在常量池中创建一个新的字符串对象
这种机制的好处是:
- 减少内存占用:相同的字符串只存储一份
- 提升性能:字符串比较可以直接比较引用
- 优化资源利用:避免创建重复的对象
2. 适配器模式(Adapter Pattern)
String类实现了多个接口(如Comparable, CharSequence等),使其能够适配不同的使用场景。这是适配器模式的一个体现:
1 | public final class String |
通过实现这些接口,String类能够:
- 通过Comparable接口支持字符串比较和排序
- 通过CharSequence接口支持字符序列操作
- 通过Serializable接口支持序列化
这样的设计让String类能够:
- 在不同的上下文中使用,比如集合排序
- 与其他字符序列类型(如StringBuilder、StringBuffer)进行互操作
- 在网络传输和持久化场景中使用
3. 不可变模式(Immutable Pattern)
虽然不是GoF定义的23种设计模式之一,但不可变模式也是String类的一个重要设计模式:
1 | public final class String { |
不可变模式的实现特点:
4. 类被声明为final,防止继承
5. 所有字段都是private和final的
6. 不提供修改内部状态的方法
7. 确保所有方法都不会修改对象状态
这种设计带来的好处前面我们已经详细讨论过,包括线程安全、缓存优化等。