编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

在 JDK 6 与 JDK 7 这两个版本中,substring(int beginIndex, int endIndex) 方法的底层实现存在显著差异。了解这两个版本间的区别,有助于开发者更好地理解字符串内存管理机制。为简便起见,下文统一以 substring() 指代 substring(int beginIndex, int endIndex) 方法。

1. substring() 方法简介

String 对象的 substring(int beginIndex, int endIndex) 方法返回此对象的一个子串,范围从 beginIndex 开始,一直到 endIndex - 1 结束,共包含 (endIndex - beginIndex) 个字符。

新手提示:

  1. String 的索引与数组一样,都是从 0 开始。
  2. 注意方法名字是 substring(),全小写。
  3. 存在一个重载方法 substring(int beginIndex),表示从 beginIndex 索引处开始直到字符串末尾。

示例代码:

String x = "abcdef";
int begin = 1;
int end = 3;
x = x.substring(begin, end);
System.out.println(x);

执行结果(包含索引为 begin 直到 end - 1 的字符):

bc

2. substring() 调用机制

众所周知,String 是不可变的(Immutable)。当执行 x = x.substring(begin, end) 时,实际上 x 指向了一个全新的字符串对象,如下图所示:

JDK 字符串引用示意图

图 1

然而,这幅图并未完全揭示堆内存中真正发生的情况。那么,在 JDK 6 和 JDK 7 之间,substring() 的调用到底有哪些区别呢?

3. JDK 6 中的实现细节

在 JDK 6 中,String 实际上是一个字符数组的封装。String 对象主要包含 3 个属性域:

private final char value[];
private final int offset;
private final int count;

它们分别用于存储实际的字符数组、数组的起始索引偏移量,以及 String 的字符个数。

当调用 substring() 方法时,虽然创建了一个新的 String 对象,但新对象的 value[] 属性域仍然指向堆内存中原来的那个字符数组。区别仅在于两个对象的 countoffset 值不同。如下图所示:

JDK6 substring 共享数组示意图

图 2

为解释这个问题,下面是最关键部分的源码:

// JDK 6, 包级私有构造,共享 value 数组以提升速度
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    // ... 检查边界的代码
    // 如果范围和自己一模一样,则返回自身,否则用 value 字符数组构造一个新的对象
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

4. JDK 6 中的内存隐患

如果有一个非常长的字符串,但每次使用 substring() 时只想要很小的一部分,那么在 JDK 6 中将会引起性能问题:虽然你只需要很小的一部分字符,但新字符串对象持有了整个原始 value[] 的引用,从而导致大量内存被占用无法释放。

要解决这个问题,在 JDK 6 中可以让其指向一个真正的子字符串(强制复制数组),示例代码:

x = x.substring(begin, end) + "";

5. JDK 7 中的改进

在 JDK 7 中,这个问题得到了改进。substring() 方法真实地在堆内存中创建了另一个字符数组,不再共享底层数组。

JDK7 substring 复制数组示意图

图 3

相关源码如下:

// JDK 7, 权限变为 public
public String(char value[], int offset, int count) {
    // ... 检查边界..
    // value 数组拷贝
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    // ... 检查边界..
    int subLen = endIndex - beginIndex;
    // 如果和自身一样,那就返回自身,否则返回构造的新对象
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
}

参考资料

  1. Changes to substring
  2. Java 6 vs Java 7 when implementation matters

相关阅读

  1. Top 10 questions about Java String
  2. Java method for spliting a camelcase string
  3. Java: Convert File to Char Array
  4. Count Number of Statements in a Java Method By Using Eclipse JDT ASTParser

原文链接:The substring() Method in JDK 6 and JDK 7


说明:本文主要对比 JDK 6 与 JDK 7 早期版本的实现差异。自 JDK 7 Update 6 起,substring() 行为已改为复制数组(同 JDK 7 描述),后续版本(JDK 8+)均沿用此机制。JDK 6 已停止维护,生产环境建议使用受支持的 LTS 版本。