不可变性
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
复制代码