String & StringBuilder & StringBuffer

最熟悉的一集

Author: Corissa

Date: Dec.29 2023

重点需要掌握🏁:

理解 字符串 的设计,分类和实现

String 相关类的演进

JVM 对象缓存机制及相关优化

通过 String 学习基本的线程安全设计与实现

1 通用分析


String:

​ 是 Java 中非常重要的类,提供了 构造和管理字符的基本逻辑

​ String 是 Immutable 类,其类和属性都被声明为 final

保存字符串的数组被 final 修饰且为private,并且String 类没有提供/暴露修改这个字符串的方法。

String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

​ 由于其不可变性,在对 String 对象实例做拼接、裁剪等操作时都会产生新的 String 对象。当需要大量对字符串进行修改操作时,往往会影响到应用性能。

StringBuffer:

​ 是字符串的可变类,且是线程安全的 (通过 synchronized 关键字实现)。本质上是一个线程安全的可修改字符串序列。

StringBuilder:

​ 也是字符串的可变类,但是去掉了线程安全的部分,效率比StringBuffer高。

​ StringBuffer 和 StringBuilder 底层采用的都是 char[] (JDK以后是byte[])。二者都继承了 AbustractStringBuilder 类。

​ StringBuilder 和 StringBuffer 的初始长度都是初始字符串长度+16 (str.length() + 16)

2 拓展


2.1 String#equals() & Object#equals()

String 重写了 Object 中的 equals() 方法。在 Object 中,equals() 对于基本数据类型比较的是值,对于引用数据类型比较的是对象的内存地址。而 String 重写了 Object 中的 equals() 方法,比较的是 String 字符串的值。

2.2 字符串缓存

避免重复创建字符串,降低内存消耗和对象创建开销

  1. Java 6

    提供 intern() 方法,提示 JVM 把相应的字符串放到缓存当中去。当创建字符串对象并调用 intern() 方法时,如果已经有缓存,则返回缓存里的实例。

    问题在于,Java 6 的字符串使唤存在 PermGen (永久代) 里的,空间有限,且不会被 FullGC 之外的垃圾收集照顾到,使用不当会产生 OOM 的问题。

    且 intern() 是一种显示地排重机制,需要开发者显示地调用,且很难保证效率。

  2. Java 8

    将缓存放置在堆中,避免了永久占满的问题。永久代在 JDK 8 中也被 MetaSpace (元数据区)替代了。

    在 JDK 8 中字符排重是 G1GC 下的字符排重,是使相同数据的字符串指向同一份数据,这是 JVM 底层的改变。

    创建字符串常量池中没有的字符串过程是:

    1. 在堆中创建字符串对象
    2. 将该字符串对象的引用保存在字符串常量池中,以后有相同字符串直接返回
    1
    2
    3
    4
    5
    6
    // 在堆中创建字符串对象”ab“
    // 将字符串对象”ab“的引用保存在字符串常量池中
    String aa = "ab";
    // 直接返回字符串常量池中字符串对象”ab“的引用
    String bb = "ab";
    System.out.println(aa==bb);// true
2.3 String 演变史

底层 char[] -> byte[] + 编码coder

引入 Compact Strings 的设计

String 其实支持两个编码方案,Latin-1 和 UTF-16。大部分拉丁文用char类型会造成一定的空间浪费,用byte就能表示。对于一串字符串,如果其中的字符没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。byte 相较 char 节省一半的内存空间。

避免了一定程度的内存浪费提高了操作速度。

但是,在极端情况下,同样数组长度下存储能力退化了一倍。(相同长度的数组char是两字节)

3 Object 中的 hashcode() 与 equals()


hashCode() 定义在 Object 类中,也就是说任何类都包含有 hashCode() 函数。

另外需要注意的是:ObjecthashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的。

hashCode 可以使容器的查找是否存在某元素的效率更高,比如HashMap 在查找key时,是对比 hashCode 来判断的。

当然,两个相等的hashCode对象可能不同(发生了哈希碰撞)

重写equals()必须重写hashCode()