Java的String.substring() 详细讲解

前言

在 Java 日常开发中,字符串截取是最常用的操作之一。

java.lang.String 类提供的 substring() 方法是实现这一功能的核心。

一、两种常见使用方法

substring() 方法的作用是从当前字符串中提取出一个子字符串,并将其作为一个全新的 String 对象返回。

原字符串本身不会被修改(因为 Java 中的 String 是不可变的)。

它提供了两个重载版本:

1.1 单参数版本

substring(int beginIndex)

  • 功能:从指定的 beginIndex 位置开始,一直截取到字符串的末尾。
  • 示例
1
2
3
String str = "HelloWorld";
String result = str.substring(5);
System.out.println(result); // 输出: World

1.2 双参数版本

substring(int beginIndex, int endIndex)

  • 功能:截取从 beginIndex 开始,到 endIndex 结束的子字符串。
  • 核心记忆点左闭右开区间 [beginIndex, endIndex)。即包含 beginIndex 位置的字符,但不包含 endIndex 位置的字符。
  • 示例
1
2
3
4
String str = "HelloWorld";
// 截取索引 0 到 4 的字符 (即前 5 个字符)
String result = str.substring(0, 5);
System.out.println(result); // 输出: Hello

技巧:截取出来的子字符串的长度,刚好等于 endIndex - beginIndex


二、3 个规则

为了避免在生产环境中抛出异常,使用该方法时需要特别注意以下规则:

  1. 索引从 0 开始:Java 字符串的第一个字符索引是 0,最后一个字符索引是 length() - 1
  2. 允许等于 length()
    • 在双参数版本中,endIndex 最大可以等于 str.length()。这表示截取到原字符串的末尾。
    • 如果 beginIndexendIndex 相等,将返回一个空字符串 ""
  3. 越界会抛异常:如果违反规则,会触发运行时异常 StringIndexOutOfBoundsException
    • beginIndex < 0
    • endIndex < 0
    • beginIndex > endIndex
    • beginIndexendIndex > str.length()

三、 底层原理

substring() 的底层实现在 JDK 6 和 JDK 7+ 之间发生过一次重大变更:

  • JDK 6 及以前(内存泄漏风险)

    调用 substring() 产生的新字符串,底层依然共享着原字符串的 char[] 数组,只是改变了 offsetcount 属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class String {
    char[] value;
    int offset;
    int count;
    }
    // substring 本质:
    new String(value, offset, count)

    // 内存发生场景
    String big = new String(new char[1000000]); // 100万字符
    String small = big.substring(0, 10);
    // 你以为:small 只占 10 个字符
    // 但实际:small 引用整个 100万 char[] small → char[1000000]
    // 即使:big = null; small 还活着, small 指向原始 char[] GC 认为 char[] 还在用
    // 整个 100万字符数组无法释放
    // substring 只是“视图切片”,不是“数据复制”
    • 风险
      • 如果你有一个极其巨大的字符串,只是用 substring 截取了一小段并长期持有引用,
      • 会导致那个巨大的 char[] 无法被垃圾回收(GC),从而引发内存泄漏。
  • JDK 7 及以后(现行机制)

    为了解决上述问题,现在的 substring() 会在堆内存中创建一个全新的 char[] 数组(或 byte[] 数组,JDK 9 之后 String 底层改为了 byte[]),并将需要截取的内容复制过去。原字符串如果不再被引用,就可以被正常 GC 回收。

    1
    2
    3
    new String(Arrays.copyOfRange(value, begin, end));
    // 结果 small → char[10],big → char[1000000]
    // 当big = null; GC 可以:回收 100万 char[], small 不受影响

    image-20260506205304835


四、 实战踩坑提示

在实际业务中,我们经常需要根据某个特定字符来截取。

为了安全起见,通常需要搭配 indexOf() 方法使用,并做好边界检查。

这里使用获取文件路径中的文件名来举例:文件路径usr/local/bin/note.md获取的文件名:note.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public String getFileName(String filePath) {
// 【第 1 步:安全拦截】
// 检查传入的字符串是不是 null,或者是不是空字符串 ""
// 如果是,直接返回空字符串,防止后面调用方法时报 NullPointerException(空指针异常)。
if (filePath == null || filePath.isEmpty()) {
return "";
}

// 【第 2 步:定位最后一个斜杠】
// filePath = "images/2023/photo.jpg"
// 倒着找,最后一个 '/' 的索引是 11。所以 lastSlashIndex = 11。
int lastSlashIndex = filePath.lastIndexOf("/");

// 【第 3 步:边界情况处理(又是防御性编程)】
// 情况 A:lastSlashIndex == -1
// 说明路径里根本没有斜杠(比如传入的是 "photo.jpg"),那它本身就是文件名,直接返回。
// 情况 B:lastSlashIndex == filePath.length() - 1
// 说明斜杠是整个字符串的最后一个字符(比如传入的是 "images/2023/"),这其实是个目录,后面没有文件名。安全起见,原样返回(或者你可以改成返回 "",看具体业务需求)。
if (lastSlashIndex == -1 || lastSlashIndex == filePath.length() - 1) {
return filePath;
}

// 【第 4 步:核心截取】
// 此时,lastSlashIndex 是 11。
// 我们要的是 '/' 后面的内容,所以从 lastSlashIndex + 1(也就是索引 12)开始截取,一直截取到末尾。
// 结果完美拿到 "photo.jpg"
return filePath.substring(lastSlashIndex + 1);
}

总结

  1. subString使用:

    • substring() 用于截取字符串,返回的是新对象,原字符串不变(String 不可变)。

    • 核心规则:左闭右开 [begin, end),长度 = end - begin

    • 使用时要注意索引边界,否则会抛 StringIndexOutOfBoundsException

  2. 底层关键点

    • JDK 6 及以前:共享原 char[] → 只是“切片视图”,可能导致内存泄漏

    • JDK 7+:复制新数组 → 真正“独立字符串”,避免内存占用问题

  3. 实战建议

    • 涉及截取位置时,优先配合 indexOf / lastIndexOf 使用

    • 一定要做边界判断(null / 空字符串 / 越界)

    • 牢记:现在的 substring 是安全的,但旧版本存在坑