String 类详解

不可变性

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 数组,offsetString 对象实际的起始位置,而 count 是所占的个数。在 JDK7 中,value 中的所有字符都属于 String 对象。

valueoffsetcount 这三个变量都是 private final 的,并且没有 setter 方法来修改,所以 String 类外部无法修改 String。所以,一旦初始化后就不能修改,String 对象也就是不可变的。

真的不可变吗

String 中的 char 数组 valueprivate 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 被设计为不可变的,在 SecurityCacheThread Safe 方面都有很多优点:

  • 安全性。String 被广泛地使用在其他 Java 类中充当参数。例如 网络连接、IO 操作、数据库连接等,如果字符串可变,那么可能会导致安全问题。
  • 字符串常量池。String 类维护了一个运行时常量池,会对创建的字符串进行缓存,如此在使用时更加高效。而这就建立在不可变的基础上,不用担心数据冲突问题。
  • 缓存 hashcodeJava 中经常用到字符串的哈希值,字符串的不可变能保证其 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 等)用 StringBuilderappend() 方法替代,最后调用 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 是一个比较常用的方法,而且在 jdk6jdk7 中的实现不同。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 offsetint 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 是一个线程安全的可变字符序列。它解决了由于拼接产生太多中间对象的问题,可以用 appendadd 方法,把字符串添加到字符串的末尾或指定位置。

它虽然保证了线程安全,也带来了额外的性能开销,所以除非有线程安全的需要,否则还是推荐使用 StringBuilder

StringBulider

StringBuilder 在能力与 StringBuffer 没有本质区别,但不保证同步,有效减小了开销。如果可能,在字符串拼接时建议优先使用。

为了能实现可修改的目的,StringBufferStringBuilder 底层都是可修改的数组,二者都继承了 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,完成构造,其 coderLATIN1

而如果存在一个大于 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 11String 类增加了一系列的字符串处理方法:

// 判断字符串是否为空白
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
复制代码

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值