跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java String 源码深度解析

深入解析 Java String 类的源码设计与核心特性。首先阐述了 String 的不可变性及其带来的线程安全、哈希缓存和常量池复用等优势。其次介绍了 JDK 1.9 将底层存储从 char[] 优化为 byte[] 的演变过程及编码标识机制。接着详细分析了 equals、hashCode、substring 和 indexOf 等常用方法的源码实现逻辑,包括 JDK 版本间的差异。最后总结了开发中的注意事项,如避免频繁拼接、合理使用 intern 方法及区分空字符串与 null,帮助开发者更好地理解和使用 String 类。

RustyLab发布于 2026/3/24更新于 2026/7/2049 浏览
Java String 源码深度解析

在 Java 开发中,String 类是日常使用频率极高的类之一。从简单的字符串拼接到复杂的文本处理,都离不开它的身影。但很多时候,我们只是停留在'会用'的层面,对其底层源码的设计逻辑和特性了解甚少。

一、String 类的核心特性:不可变性

打开 JDK 源码,我们会发现 String 类被 final 关键字修饰,这意味着 String 类不能被继承。同时,其内部存储字符串的成员变量 value(在 JDK 1.8 及之前为 char[] 类型,JDK 1.9 之后改为 byte[] 类型)也被 private final 修饰。这两个关键设计,共同决定了 String 类的核心特性——不可变性。

// JDK 1.8 String 类核心成员变量
private final char value[];

// JDK 1.9 String 类核心成员变量
private final byte[] value;
private final byte coder; // 用于标识编码格式,0 表示 Latin-1,1 表示 UTF-16

不可变性意味着一旦 String 对象被创建,其内部的字符序列就无法被修改。比如执行 String str = "abc"; str += "d"; 时,看似是修改了 str 的值,实则是创建了一个新的 String 对象 "abcd",并将 str 的引用指向了这个新对象,原对象 "abc" 依然存在于内存中(后续会被垃圾回收机制处理)。

这种设计带来了诸多好处:

  1. 线程安全:由于对象不可修改,多线程环境下无需担心线程安全问题,无需额外加锁。
  2. 可缓存哈希值:String 类的 hashCode() 方法会根据 value 计算哈希值,由于 value 不可变,哈希值只需计算一次并缓存起来,后续调用 hashCode() 可直接返回,提升效率(这也是 HashMap 等集合中常用 String 作为键的重要原因)。
  3. 常量池复用:Java 中的字符串常量池(String Pool)可以复用相同内容的 String 对象,减少内存占用。比如 String a = "abc"; String b = "abc";,a 和 b 会指向常量池中同一个对象。

二、底层存储的演变:从 char[] 到 byte[]

在 JDK 1.8 及之前,String 类使用 char[] 存储字符,每个 char 占 2 个字节(UTF-16 编码)。但在实际开发中,大部分字符串是由 Latin-1 字符(如英文字母、数字、常见符号)组成,这类字符只需 1 个字节就能存储。使用 char[] 存储会造成大量内存浪费。

为了解决这个问题,JDK 1.9 对 String 的底层存储进行了优化,将 char[] 改为 byte[],并新增了 coder 成员变量来标识编码格式:

  • 当 coder = 0 时,使用 Latin-1 编码,每个字符占 1 个字节,适用于存储英文字符等。
  • 当 coder = 1 时,使用 UTF-16 编码,每个字符占 2 个字节,适用于存储中文、特殊符号等。

这种自适应编码的设计,在保证功能不受影响的前提下,大幅减少了内存占用,尤其在处理大量英文字符串的场景下,优化效果显著。

三、常用方法的源码解析

1. equals() 方法:判断字符串内容是否相等

String 类重写了 Object 类的 equals() 方法,其核心逻辑是逐字符比较两个字符串的内容是否一致,具体步骤如下:

  1. 先判断两个对象的引用是否相同,若相同直接返回 true。
  2. 判断传入的对象是否为 String 类型,若不是直接返回 false。
  3. 比较两个字符串的长度和编码格式(JDK 1.9 及之后),若不一致返回 false。
  4. 逐字符比较两个字符串的 value 数组,若所有字符都相同则返回 true,否则返回 false。
// JDK 1.9 String 类 equals() 方法核心代码
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String) anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

2. hashCode() 方法:计算字符串的哈希值

String 类的 hashCode() 方法根据 value 数组的内容计算哈希值,计算公式为:hash = 31 * hash + val[i](其中 val[i] 是字符串第 i 个字符的 ASCII 值)。选择 31 作为乘数,是因为 31 是一个质数,且 31 * x 可以被优化为 (x << 5) - x(移位运算比乘法运算效率更高),同时 31 能有效减少哈希冲突。

// JDK 1.8 String 类 hashCode() 方法
private int hash; // 缓存字符串的哈希码
public int hashCode() {
    int h = hash; // 从缓存中获取哈希码
    // 如果哈希码未被计算过(即为 0)且字符串不为空,则计算哈希码
    if (h == 0 && value.length > 0) {
        char val[] = value; // 获取字符串的字符数组
        // 遍历字符串的每个字符来计算哈希码
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; // 使用 31 作为乘法因子
        }
        hash = h; // 缓存计算后的哈希码
    }
    return h; // 返回哈希码
}

3. substring() 方法:截取字符串(JDK 1.7 前后差异)

在 JDK 1.7 之前,substring() 方法通过创建新的 String 对象,并共享原对象的 value 数组,只修改新对象的 offset(偏移量)和 count(长度)来实现截取。这种设计虽然高效,但会导致原对象的 value 数组无法被垃圾回收(即使原对象不再使用,只要截取后的对象存在,value 数组就会被引用),容易造成内存泄漏。

JDK 1.7 及之后,substring() 方法进行了优化,不再共享原对象的 value 数组,而是通过复制原数组的指定部分来创建新的 value 数组,从根本上解决了内存泄漏问题。

// JDK 1.8 String 类 substring() 方法
public String substring(int beginIndex) {
    // 检查起始索引是否小于 0,如果是,则抛出 StringIndexOutOfBoundsException 异常
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    // 计算子字符串的长度
    int subLen = value.length - beginIndex;
    // 检查子字符串长度是否为负数,如果是,则抛出 StringIndexOutOfBoundsException 异常
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    // 如果起始索引为 0,则返回原字符串;否则,创建并返回新的字符串
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

4. indexOf() 方法:查找子串在原字符串内的索引

indexOf 方法用于查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引。来看该方法的源码:

/*
 * 查找字符数组 target 在字符数组 source 中第一次出现的位置。
 * sourceOffset 和 sourceCount 参数指定 source 数组中要搜索的范围,
 * targetOffset 和 targetCount 参数指定 target 数组中要搜索的范围,
 * fromIndex 参数指定开始搜索的位置。
 * 如果找到了 target 数组,则返回它在 source 数组中的位置索引(从 0 开始),
 * 否则返回 -1。
 */
static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) {
    // 如果开始搜索的位置已经超出 source 数组的范围,则直接返回 -1(如果 target 数组为空,则返回 sourceCount)
    if (fromIndex >= sourceCount) {
        return (targetCount == 0 ? sourceCount : -1);
    }
    // 如果开始搜索的位置小于 0,则从 0 开始搜索
    if (fromIndex < 0) { fromIndex = 0; }
    // 如果 target 数组为空,则直接返回开始搜索的位置
    if (targetCount == 0) { return fromIndex; }
    // 查找 target 数组的第一个字符在 source 数组中的位置
    char first = target[targetOffset];
    int max = sourceOffset + (sourceCount - targetCount);
    // 循环查找 target 数组在 source 数组中的位置
    for (int i = sourceOffset + fromIndex; i <= max; i++) {
        /* Look for first character.
         */
        // 如果 source 数组中当前位置的字符不是 target 数组的第一个字符,则在 source 数组中继续查找 target 数组的第一个字符
        if (source[i] != first) {
            while (++i <= max && source[i] != first);
        }
        /* Found first character, now look at the rest of v2
         */
        // 如果在 source 数组中找到了 target 数组的第一个字符,则继续查找 target 数组的剩余部分是否匹配
        if (i <= max) {
            int j = i + 1;
            int end = j + targetCount - 1;
            for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++);
            // 如果 target 数组全部匹配,则返回在 source 数组中的位置索引
            if (j == end) {
                /* Found whole string.
                 */
                return i - sourceOffset;
            }
        }
    }
    // 没有找到 target 数组,则返回 -1
    return -1;
}

四、开发中的注意事项

  1. 避免频繁字符串拼接:由于 String 的不可变性,使用 + 进行频繁字符串拼接会创建大量临时对象,导致内存浪费和性能下降。此时应使用 StringBuilder(非线程安全,效率高)或 StringBuffer(线程安全,效率较低)。
  2. 合理使用 intern() 方法:intern() 方法会将字符串对象加入常量池(若常量池中不存在该对象),并返回常量池中的引用。合理使用 intern() 可以减少重复对象的创建,但过度使用会增加常量池的负担,需根据实际场景权衡。
  3. 区分空字符串与 null:"" 是一个长度为 0 的 String 对象,其 value 数组长度为 0;而 null 表示没有引用任何对象。在使用 String 对象前,需注意判断是否为 null,避免空指针异常。

目录

  1. 一、String 类的核心特性:不可变性
  2. 二、底层存储的演变:从 char[] 到 byte[]
  3. 三、常用方法的源码解析
  4. 1. equals() 方法:判断字符串内容是否相等
  5. 2. hashCode() 方法:计算字符串的哈希值
  6. 3. substring() 方法:截取字符串(JDK 1.7 前后差异)
  7. 4. indexOf() 方法:查找子串在原字符串内的索引
  8. 四、开发中的注意事项
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 前端数据库 IndexedDB 详解:构建离线 Web 应用
  • Sora 2 技术解析:AI 视频生成的核心突破与应用场景
  • 深入解析 C++ STL list:双向链表原理与迭代器实战
  • LIBERO 数据集:终身机器人学习与知识迁移基准
  • ERNIESage 结合预训练大模型的图神经网络链接预测应用
  • ASP.NET 4.7 微服务化实践:Windows Docker 环境搭建
  • SQL Server 错误 18456 用户 sa 登录失败排查与解决
  • 深入理解 Linux 基础 IO:从 C 库到系统调用
  • CSS 背景样式详解:颜色、图片与复合属性
  • 6 层高速 PCB 设计实战:逻辑派 FPGA-G1 开发板布局布线详解
  • AIGC 产品经理面试 100 道高频问题及答案解析
  • AI 深度观察:GTC 开幕、Agent 工程化与具身智能新进展
  • 分布式文件系统 HDFS 存储原理
  • 使用 CSS 实现毛玻璃模糊背景效果
  • 手写 STL 红黑树:封装实现 map 与 set 容器
  • TRAE 与 VSCode 中的 Git 版本管理操作指南
  • 位运算算法专题:判断字符唯一、丢失数字与两数之和等
  • 数据结构基础:顺序表详解与动态实现
  • 二分查找算法:山脉数组的峰顶索引与寻找峰值
  • C/C++ 算法入门:动态规划多状态问题 - 打家劫舍与股票买卖

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online