我正在参加「掘金·启航计划」
前言
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;
- 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次数, 同样会提升系统的性能。
- String的不可变性 final
String在 Java 中是不可变的。不可变类只是一个实例不能被修改的类。实例中的所有信息都是在创建实例时初始化的,不能修改信息。不可变类有很多优点如下。
2.1 字符串池的要求
字符串池(String intern pool)是方法区中一个特殊的存储区。当一个字符串被创建并且该字符串已经存在于池中时,将返回现有字符串的引用,而不是创建一个新对象。如果一个字符串是可变的,用一个引用改变字符串会导致其他引用的值错误。
2.2 缓存哈希值
字符串的哈希码在 Java 中经常使用。例如,在 HashMap 或 HashSet 中。不可变保证哈希码始终相同,因此无需担心更改即可实现。这意味着每次使用哈希码时都无需计算。这样更有效率。
2.3 安全性考虑
String 被广泛用作许多 java 类的参数,例如网络连接、打开文件等。如果 String 不是不可变的,则连接或文件将被更改,这可能会导致严重的安全威胁。
2.4 线程安全问题
因为不可变对象不能改变,所以可以在多个线程之间自由共享。这消除了进行同步的要求。
总之,String出于效率和安全原因,它被设计为不可变的。这也是为什么在许多情况下通常首选不可变类的原因。
- 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;
- 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内存结构。这里暂不详细解释
总结
- JDK对String的优化,char[]编程byte[],以节约内存。
- String不可变的好处,安全等。
- String重写equals()方法,引用改成的值比较。
- String.intern() 方法,字符串常量池。