不可变性
String 对象是不可变(Immutable)的,也就是一旦 String 类实例被创建后,就不能改变其值。这里的不可变指的是引用既不能指向其他对象,而且引用指向的对象的值也不能改变。
为什么不可变
在 JDK1.6 中,String 类中的成员变量如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
在 JDK1.7 中,String 类主要改变了 substring 方法的实现,成员变量剩下了两个:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
可以看出,String 就是字符数组的封装。在 JDK6 中,value 是一个 char 数组,offset 是 String 对象实际的起始位置,而 count 是所占的个数。在 JDK7 中,value 中的所有字符都属于 String 对象。
value、offset 和 count 这三个变量都是 private final 的,并且没有 setter 方法来修改,所以 String 类外部无法修改 String。所以,一旦初始化后就不能修改,String 对象也就是不可变的。
真的不可变吗
String 中的 char 数组 value 是 private final 的,被 final 修饰 虽然不能指向其他数组对象,但却可以通过反射修改其指向的数组。
使用反射可以得到 String 类的 value 属性,修改访问权限,然后就可以对数组内容进行修改。
public static void main(String[] args) throws Exception {
String s = "abc";
System.out.println(s);
// 获取 value 字段
Field field = String.class.getDeclaredField("value");
// 修改 value 字段访问权限
field.setAccessible(true);
// 获取 s 对象上 value 属性的值
char[] value = (char[]) field.get(s);
value[1] = 'd';
System.out.println(s);
}
// abc
// adc
可以看到,通过反射是可以修改 "不可变" 对象的。
不可变的优点
String 被设计为不可变的,在 Security、Cache、Thread Safe 方面都有很多优点:
- 安全性。
String被广泛地使用在其他Java类中充当参数。例如 网络连接、IO操作、数据库连接等,如果字符串可变,那么可能会导致安全问题。 - 字符串常量池。
String类维护了一个运行时常量池,会对创建的字符串进行缓存,如此在使用时更加高效。而这就建立在不可变的基础上,不用担心数据冲突问题。 - 缓存
hashcode。Java中经常用到字符串的哈希值,字符串的不可变能保证其hashcode永远保持一致,这样在每次使用一个字符串的hashcode时,就不用重新计算一次,也更加高效。 - 线程安全性。由于
String对象不能被改变,所以同一个字符串实例可以被多个线程共享,而不用因为线程安全问题使用同步。
不可变的缺点
当然,设计为不可变也会出现一些缺点,例如在类似拼接、裁剪等操作时,都会创建新的 String 对象,如果程序设计不当,便会产生大量无用的字符串对象,耗费时间空间。
"+" 的实现
1、对于两个编译期常量(编译期可知),例如 String s = "a" + "b",编译器会进行常量折叠,即变成 String s = "ab":
/**
* String s1 = "ab";
* String s2 = "a1";
*/
String s1 = "a" + "b";
String s2 = "a" + 1;
2、对于能够进行优化的(例如 String s = "a" + s1 等)用 StringBuilder 的 append() 方法替代,最后调用 toString() 方法
/**
* String s3 = (new StringBuilder()).append("a").append(s1).toString();
* String s4 = (new StringBuilder()).append(s2).append(s3).toString();
*/
String s3 = "a" + s1;
String s4 = s2 + s3;
/**
* String s6 =
* for (int i = 0; i < 2; i++) {
* s6 = (new StringBuilder()).append(s6).append(i).toString();
* }
*/
String s6 = "";
for (int i = 0; i < 2; i++) {
s6 += i;
}
substring 在 jdk6 和 7 的区别
substring 是一个比较常用的方法,而且在 jdk6 和 jdk7 中的实现不同。substring(int beginIndex, in endIndex) 方法的作用是截取字符串并返回其 [beginIndex, endIndex - 1] 范围内的内容。
String str = "abcdef";
String substring = str.substring(2, 4);
System.out.println(substring);
输出结果为:cd。
JDK6 中的 substring
前面说过,在 JDK 6 中,String 类的三个成员变量:char value[],int offset,int count,三个变量决定了 String 存储的真正的字符数组。
String 中主要相关源码如下:
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
// 检查边界
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
当调用 substring 方法时,会创建一个 String 对象,但 value 引用仍然指向堆中的同一个字符数组。它的内存变化:
如果字符串很长,在使用 substring 进行切割时只需要很短的一段,就可能导致性能问题.。因为只需要一小段字符串,但是却引用了整个字符串,这个很长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露。
JDK7 中的 substring
在 JDK7 中,主要剩下一个 value 变量,它的主要源码如下;
public String(char value[], int offset, int count) {
// 检查边界
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String substring(int beginIndex) {
// 检查边界
int subLen = value.length - beginIndex;
// 检查边界
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
可以看到,JDK 7 中的 subString 方法,使用 new String 创建了一个新字符串,避免对老字符串的引用,从而解决了内存泄露问题。它的内存变化如下:
StringBuffer、StringBuilder
String
String 是不可变对象,被声明为 final class,所有属性也都是 final 的。由于其不可变性,类似拼接、裁剪等操作,都会产生一个新的 String 对象,然后指针指向新的 String 对象,如果操作不当,可能会产生大量临时字符串。
在字符串内容不经常变化的业务场景优先使用 String 类。例如:常量声明、少量的字符串拼接等。
StringBuffer
StringBuffer 是一个线程安全的可变字符序列。它解决了由于拼接产生太多中间对象的问题,可以用 append 或 add 方法,把字符串添加到字符串的末尾或指定位置。
它虽然保证了线程安全,也带来了额外的性能开销,所以除非有线程安全的需要,否则还是推荐使用 StringBuilder。
StringBulider
StringBuilder 在能力与 StringBuffer 没有本质区别,但不保证同步,有效减小了开销。如果可能,在字符串拼接时建议优先使用。
为了能实现可修改的目的,StringBuffer 和 StringBuilder 底层都是可修改的数组,二者都继承了 AbstarctStringBuilder,包含了基本操作,区别仅在于最终的方法是否加了 synchronized。
JDK 9 改进
在 JDK9 之前,String 类内部使用 char 数组来存储数据,但 char 是两个字节大小,这样就造成了一定的浪费。
在 JDK9 中,引入了 Compact Strings 的设计,对字符串进行改进,将 char 数组改变为 byte 数组加上一个标识编码的 coder,并且对相关字符串操作进行修改。
成员变量变化如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
改进之后,在存储数据时,如果传入 byte 数组,直接赋值就好,如果传入 char 数组,其源码如下:
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) { // COMPACT_STRINGS 默认初始化为 true
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
其中,StringUTF16.compress 方法实现如下:
public static byte[] compress(char[] val, int off, int len) {
byte[] ret = new byte[len];
if (compress(val, off, ret, 0, len) == len) {
return ret;
}
return null;
}
···
@HotSpotIntrinsicCandidate
public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) {
for (int i = 0; i < len; i++) {
char c = src[srcOff];
if (c > 0xFF) {
len = 0;
break;
}
dst[dstOff] = (byte)c;
srcOff++;
dstOff++;
}
return len;
}
在 for 循环中,如果 char 数组中每一个字符都小于等于 0xFF,那么将 char 转换为 byte,完成构造,其 coder 为 LATIN1。
而如果存在一个大于 0xFF 的字符,就会跳出循环,最终 StringUTF6.compress 方法返回 null,通过 StringUTF16.toBytes 方法:
@HotSpotIntrinsicCandidate
public static byte[] toBytes(char[] value, int off, int len) {
byte[] val = newBytesFor(len);
for (int i = 0; i < len; i++) {
putChar(val, i, value[off]);
off++;
}
return val;
}
public static byte[] newBytesFor(int len) {
// check bound
return new byte[len << 1];
}
@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
static void putChar(byte[] val, int index, int c) {
assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
index <<= 1;
val[index++] = (byte)(c >> HI_BYTE_SHIFT);
val[index] = (byte)(c >> LO_BYTE_SHIFT);
}
通过 newBytesFor 方法 new 一个两倍长度的 byte 数组,在 for 循环中,通过 putChar 方法来填充 byte 数组,将 char 字符分为两部分,存储两个相邻的 byte 数组中。
String 类中方法基本都重新实现了一遍,但对外提供的接口没有改变。重构后,在字符串中所有字符小于 0xFF 时,可以节省一半的内存。
JDK 11 新特性
JDK 11 中 String 类增加了一系列的字符串处理方法:
// 判断字符串是否为空白
System.out.println(" ".isBlank()); // true
// 去除首尾空格
System.out.println(" Timber ".strip()); // Timber
// 去除首部空格
System.out.println(" Timber".stripLeading()); // Timber
// 去除尾部空格
System.out.println("Timber ".stripTrailing()); // Timber
// 重复字符串
System.out.println("Timber".repeat(2)); // TimberTimber
// 获取字符串中的行数
System.out.println("A\nB\nC".lines().count()); // 3