String不可变好处分析

499 阅读6分钟

我正在参加「掘金·启航计划」

前言

String类是Java编程中使用最多的类了,最先想到的可能就是不可变,安全等等,再或者就是面试中new String("abc")创建了几个对象,new String("abc").intern() == "abc" 是否相等,二话不说直接上源码!

Java 8源码及注释

public final class String implements java.io.Serializable, 
Comparable<String>, CharSequence { 
/** 该值用于字符存储。 */
private final char value[]; 
/** 缓存字符串的哈希码 */ 
private int hash; // Default to 0
}

Java 17 源码及注释(JDK 8之后)

public final class String implements java.io.Serializable, 
Comparable<String>, CharSequence, Constable, ConstantDesc { 
//该值用于字符存储。
//实现说明: //该字段受 VM 信任,如果 String 实例为常量,则该字段受常量折叠的影响。
//构建后覆盖该字段会导致问题。此外,它被标记为Stable以信任数组的内容。 
//JDK 中没有其他工具提供此功能(目前)。 
// Stable在这里是安全的,因为值永远不会为空 @Stable private final byte[] value; //用于对value中的字节进行编码的编码标识符。此实现中支持的值为 LATIN1 UTF16 
//实现说明:
//该字段受 VM 信任,如果 String 实例为常量,则该字段受常量折叠的影响。构建后覆盖该字段会导致问题。
private final byte coder; 
//缓存字符串的哈希码 
private int hash; 
// Default to 0 
//如果哈希值被计算为实际上为零,则缓存,使我们能够避免重新计算它。 
private boolean hashIsZero; // Default to false;
  1. String的优化改进

到了JDK9,String中字符串的存储不再用char数组了,改用byte数组。

如源码中所示,还增加了一个coder成员变量,在程序中,绝大多数字符串只包含英文字母数字等字符,使用Latin-1编码,一个字符占用一个byte。如果使用char,一个char要占用两个byte,会占用双倍的内存空间。

但是,如果字符串中使用了中文等超出Latin-1表示范围的字符,使用Latin-1就没办法表示了。这时JDK会使用UTF-16编码,那么占用的空间和旧版(使用char[])是一样的。

coder变量代表编码的格式,目前String支持两种编码格式Latin-1和UTF-16。

Latin-1需要用一个字节来存储,而UTF-16需要使用2个字节或者4个字节来存储。

优化的好处,首先如果项目中使用Latin-1字符集居多,内存的占用大幅度减少,同样的硬件配置可以支撑更多的业务。当内存减少之后,进一步导致减少GC次数, 同样会提升系统的性能。

  1. String的不可变性 final

String在 Java 中是不可变的。不可变类只是一个实例不能被修改的类。实例中的所有信息都是在创建实例时初始化的,不能修改信息。不可变类有很多优点如下。

2.1 字符串池的要求

字符串池(String intern pool)是方法区中一个特殊的存储区。当一个字符串被创建并且该字符串已经存在于池中时,将返回现有字符串的引用,而不是创建一个新对象。如果一个字符串是可变的,用一个引用改变字符串会导致其他引用的值错误。

2.2 缓存哈希值

字符串的哈希码在 Java 中经常使用。例如,在 HashMap 或 HashSet 中。不可变保证哈希码始终相同,因此无需担心更改即可实现。这意味着每次使用哈希码时都无需计算。这样更有效率。

2.3 安全性考虑

String 被广泛用作许多 java 类的参数,例如网络连接、打开文件等。如果 String 不是不可变的,则连接或文件将被更改,这可能会导致严重的安全威胁。

2.4 线程安全问题

因为不可变对象不能改变,所以可以在多个线程之间自由共享。这消除了进行同步的要求。

总之,String出于效率和安全原因,它被设计为不可变的。这也是为什么在许多情况下通常首选不可变类的原因。

  1. String的equals的方法

简单来说,就是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。对比JDK 8 和JDK17区别如下:

JDK8 版本

public boolean equals(Object anObject) { 
// 判断是否是统一引用 
    if (this == anObject) {
        return true; 
    } 
    // 判断是否是String 类型 
        if (anObject instanceof String){ 
            String anotherString = (String)anObject; 
            int n = value.length; 
            //获取他们值的长度,判断长度是否相同 
            if (n == anotherString.value.length) { 
            //将值转为char数组,进行比较只要有一个不想等就返回false 
            char v1[] = value; 
            char v2[] = anotherString.value; 
            int i = 0; 
                while (n-- != 0) {
                    if (v1[i] != v2[i]) 
                        return false; 
                    i++; 
                } 
            return true;
        } 
    } 
 return false;
}

JDK 17版本

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
// 长度是否相同
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
        //进行比较只要有一个不想等就返回false
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
  1. String类中intern() 方法

先看官方源码说明及注释

返回字符串对象的规范表示。 
一个字符串池,最初是空的,由String类私下维护。
当调用 intern 方法时,如果池中已经包含一个等于该String对象的字符串,该字符串由equals(Object)方法确定, 
则返回池中的字符串。否则,将此String对象添加到池中并返回对该String对象的引用。 
因此,对于任何两个字符串s和t ,当且仅当s.equals(t)为true时, s.intern() == t.intern()才为true 。 
所有文字字符串和字符串值的常量表达式都是实习的。
字符串文字在Java 语言规范的 @jls 3.10.5 部分中定义。 
返回值: 与此字符串具有相同内容的字符串,但保证来自唯一字符串池。 public native String intern();

整体的一个意思就是 通过常量池来节省内存空间,上面也已提到不可变的一个原因之一就是字符串常量池的一个需要。

来个题目试试:

在字符串常量池中,默认会将对象放入常量池;在字符串变量中,对象是会在堆中创建,同时也会在常量池中创建一个字符串对象,String 对象中的 char 数组将会引用常量池中的 char 数组,并返回堆内存对象引用。

如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,在 JDK1.6 版本中去复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。

在 JDK1.7 版本以后,由于常量池合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池的字符串引用。

具体分析还要了解JVM内存结构。这里暂不详细解释

总结

  1. JDK对String的优化,char[]编程byte[],以节约内存。
  2. String不可变的好处,安全等。
  3. String重写equals()方法,引用改成的值比较。
  4. String.intern() 方法,字符串常量池。