String类的声明

1
2
3
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc{

}
  1. final关键字修饰该类,因此不会再被子类继承。
    • 实现了字符串常量池
    • 保证线程安全
    • 保证hashcode的不可变性
  2. 实现了Serializable接口,保证String类可以进行序列化
  3. 实现了Comparable接口,可以不再使用==进行两个字符串是否相等,而是使用compareTo()方法进行比较
  4. String,StirngBuffer,StringBuilder都实现了CharSequence接口,由于String类不可变,所以如果遇到字符串拼接就可以使用StringBufferStringBuilder

String底层为什么要用byte数组而不是char数组

String从使用char数组改成byte数组是在Java 9中引入的一个重要优化。让我来详细解释这个变化的原因:

主要原因是为了节省内存空间。这里涉及到几个关键点:

  1. 字符编码的演变:
  • 在早期,char类型使用16位(2字节)来存储一个字符,这是基于Unicode的UCS-2编码设计的
  • 当时认为16位(可表示65536个字符)足够表示所有字符
  • 但随着Unicode的发展,出现了更多的字符,需要超过16位才能表示
  1. 内存使用效率:
    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%的内存
  1. 编码方案:
  • 新的实现使用一个coder字段来标识当前字符串使用的编码方式:
    • LATIN1(0):表示字符串只包含Latin-1字符,每个字符用1个字节存储
    • UTF16(1):表示字符串包含其他Unicode字符,每个字符用2个字节存储
  • 系统会根据字符串的内容自动选择最优的存储方式
  1. 性能影响:
    1
    2
    3
    4
    public int length() {
    // 根据coder返回正确的长度
    return value.length >> coder; // LATIN1时除以1,UTF16时除以2
    }
  • 虽然增加了一些判断逻辑,但内存使用的优化带来的好处远大于这些微小的性能开销
  • 对于大量使用字符串的应用来说,这种优化可以显著减少内存占用

这个改变是一个很好的例子,说明了如何在保持向后兼容性的同时,通过改进内部实现来优化性能。对于使用String的代码来说,这个变化是完全透明的,但却能带来显著的内存节省。

String为什么不可变

1
2
3
4
5
public final class String  
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
private final byte[] value;
}
  1. String类被final修饰,这就决定了不会再有子类重写Stirng类的方法,改变它的行为
  2. String类的数据存储在byte[]/char[]数组中,该数组也被final关键字修饰,保证了该字符串数组不会再被改变了
  3. 该数组被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
2
String str = "Hello";
str = str + " World"; // 创建了一个新的String对象

在这个例子中,原始的”Hello”对象仍然存在于内存中,而str变量现在指向了一个新的”Hello World”对象。

StringBuffer和StringBuilder的可变性
这两个类都是可变的,它们的操作会直接修改对象内部的字符数组,而不是创建新对象:

1
2
StringBuilder builder = new StringBuilder("Hello");
builder.append(" World"); // 直接修改原对象

线程安全性

String的线程安全
由于String是不可变的,所以它天然是线程安全的。多个线程可以同时读取同一个String对象而不会产生问题。

StringBuffer的线程安全
StringBuffer的方法都使用synchronized关键字进行同步,这使得它是线程安全的,但也因此带来了性能开销。适合在多线程环境下使用:

1
2
3
public synchronized StringBuffer append(String str) {
// 实现代码
}

StringBuilder的非线程安全
StringBuilder没有进行同步处理,因此在单线程环境下性能最好,但在多线程环境下不安全:

1
2
3
public StringBuilder append(String str) {
// 实现代码,没有synchronized关键字
}

性能比较

让我们通过一个简单的例子来比较它们的性能差异:

1
2
3
4
5
6
7
8
9
10
11
// String拼接
String str = "";
for(int i = 0; i < 1000; i++) {
str += "a"; // 每次循环都创建新对象
}

// StringBuilder拼接
StringBuilder builder = new StringBuilder();
for(int i = 0; i < 1000; i++) {
builder.append("a"); // 直接修改原对象
}

在这个例子中:

  • String的方式会创建大量临时对象,性能最差
  • StringBuilder因为没有同步开销,性能最好
  • StringBuffer因为有同步开销,性能介于两者之间

共同点

虽然这三个类有很多区别,但它们也有一些重要的共同点:

  1. 都实现了CharSequence接口,这意味着它们都可以存储和操作字符序列:

    1
    2
    3
    CharSequence cs1 = "Hello";  // String
    CharSequence cs2 = new StringBuilder("Hello"); // StringBuilder
    CharSequence cs3 = new StringBuffer("Hello"); // StringBuffer
  2. 都提供了相似的基本操作方法,如字符串拼接、子字符串获取、长度查询等:

    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
2
3
String s1 = "hello";  // 创建一个字符串字面量
String s2 = "hello"; // 重用常量池中的对象
System.out.println(s1 == s2); // 输出true,因为是同一个对象

当我们创建字符串字面量时,Java会首先检查字符串常量池中是否已经存在相同的字符串:

  1. 如果存在,直接返回常量池中的引用
  2. 如果不存在,则在常量池中创建一个新的字符串对象

这种机制的好处是:

  • 减少内存占用:相同的字符串只存储一份
  • 提升性能:字符串比较可以直接比较引用
  • 优化资源利用:避免创建重复的对象

2. 适配器模式(Adapter Pattern)

String类实现了多个接口(如Comparable, CharSequence等),使其能够适配不同的使用场景。这是适配器模式的一个体现:

1
2
3
4
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 实现代码
}

通过实现这些接口,String类能够:

  • 通过Comparable接口支持字符串比较和排序
  • 通过CharSequence接口支持字符序列操作
  • 通过Serializable接口支持序列化

这样的设计让String类能够:

  1. 在不同的上下文中使用,比如集合排序
  2. 与其他字符序列类型(如StringBuilder、StringBuffer)进行互操作
  3. 在网络传输和持久化场景中使用

3. 不可变模式(Immutable Pattern)

虽然不是GoF定义的23种设计模式之一,但不可变模式也是String类的一个重要设计模式:

1
2
3
4
5
public final class String {
private final char[] value; // 使用final修饰字符数组

// 构造方法和其他方法
}

不可变模式的实现特点:
4. 类被声明为final,防止继承
5. 所有字段都是private和final的
6. 不提供修改内部状态的方法
7. 确保所有方法都不会修改对象状态

这种设计带来的好处前面我们已经详细讨论过,包括线程安全、缓存优化等。