当前位置: 首页 > news >正文

Java String 全面解析:从源码到常量池,再到面试高频题

目录

一、源码分析(JDK8)

成员变量

构造函数

常见方法

二、String常量池

String 常量池到底是什么?

什么字符串会进常量池?

常量池的核心规则

new String() 为什么不会进池?

JDK 7 以后常量池的重大变化

intern() 到底做了什么?

字符串拼接进不进池?

总结

三、常见面试点

String 核心

创建 String 的两种方式

== 和 equals 的区别

字符串常量池重点

字符串拼接重点

String 常用方法核心特点

String 为什么不可变?

StringBuilder 与 StringBuffer 区别

String拼接原理

String常量池、运行时常量池、Class常量池区别

四、常见面试题

1、创建对象 & 常量池

2、字符串拼接

一、源码分析(JDK8)

成员变量

public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; // Default to 0 }
  • public final class String

    • public:任何地方都能用
    • finalString 不能被继承,不能写个子类继承它
    • class String:字符串类
  • implements

    • 实现了三个接口:
      • Serializable:可以序列化(网络传输 / 存文件)
      • Comparable<String>:可以比较大小(a.compareTo (b))
      • CharSequence:字符串的标准接口
private final char value[];
  • private:外部不能访问、不能修改
  • final数组地址一旦赋值就不能改
  • char value[]:真正存字符串内容的字符数组
private int hash; // Default to 0
  • 缓存哈希值
  • 第一次调用hashCode()时计算一次,之后直接用,不用重复算
  • 提升 HashMap 性能

构造函数

非常的多,我们之说下面的常见几种

1. 空参构造:public String()

public String() { this.value = "".value; }
  • 作用:创建一个空字符串""
  • 底层:直接复用常量池里空字符串的char[],不新建数组
  • 使用String s = new String();→ 等价于String s = ""
  • 注意:直接写""更高效

2. 字符串参数构造:public String(String original)

public String(String original) { this.value = original.value; this.hash = original.hash; }
  • 作用:根据已有字符串创建一个新 String 对象
  • 底层直接复用原字符串的 char 数组(不复制),只新建 String 外壳
  • 使用String s = new String("abc");
  • 重点
    • 常量池字符串 +new创建两个对象(常量池 1 个 + 堆 1 个)
    • 日常开发不要这么写,直接String s = "abc";最优

3. char 数组完整构造:public String(char value[])

public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
  • 作用:把整个 char 数组转成字符串
  • 底层复制一份新数组(保护原数组不被修改)
  • 使用
    char[] arr = {'a','b','c'}; String s = new String(arr);

4. char 数组截取构造:public String(char value[], int offset, int count)

public String(char value[], int offset, int count) { // 边界校验:offset、count 不能为负、不能越界 this.value = Arrays.copyOfRange(value, offset, offset+count); }
  • 作用:从 char 数组中截取一段生成字符串
  • 参数
    • offset:起始下标
    • count:截取长度
  • 底层:复制截取范围的数组,严格校验越界
  • 使用
    char[] arr = {'a','b','c','d'}; String s = new String(arr, 1, 2); // 从下标1开始,取2个 → "bc"

5. StringBuilder 构造:public String(StringBuilder builder)

public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }
  • 作用:把StringBuilder转成 String(最常用
  • 底层:无锁,直接复制数组,效率高
  • 使用场景:拼接字符串后转 String
    StringBuilder sb = new StringBuilder(); sb.append("a").append("b"); String s = new String(sb); // → "ab"

对比

构造方法核心用途特点
new String()空字符串等价 ""
new String(String)复制字符串会创建新对象
new String(char[])char 数组转字符串复制数组,安全
new String(char[],off,len)截取 char 数组常用
new String(StringBuilder)拼接后转字符串常用

常见方法

基础信息获取方法

这些方法直接操作value字符数组,最简单高效

// 返回字符串长度 = 字符数组长度 public int length() { return value.length; } // 判断是否为空:长度为0 public boolean isEmpty() { return value.length == 0; } // 获取指定索引的字符,越界抛异常 public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }

字符 / 字节拷贝方法

底层用System.arraycopynative 本地方法,速度极快)实现数据拷贝

// 拷贝字符串到目标字符数组 public void getChars(...) { System.arraycopy(value, 源起始, 目标数组, 目标起始, 长度); } // 字符串转字节数组(支持编码) public byte[] getBytes(String charsetName) { return StringCoding.encode(...); }

字符串比较方法

1. equals ()

判断两个字符串内容是否完全相同

  1. 先判断引用地址是否相同(==),相同直接返回 true
  2. 再判断类型是否是 String
  3. 最后逐字符比较
public boolean equals(Object anObject) { if (this == anObject) return true; // 地址相同,直接相等 if (anObject instanceof String) { String another = (String) anObject; int n = value.length; if (n == another.value.length) { // 长度不同,直接不等 char[] v1 = value; char[] v2 = another.value; int i = 0; while (n-- != 0) { // 逐字符比较 if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
2. compareTo()

字典序比较:逐字符比较 ASCII 码值,返回差值

3. 其他比较
  • equalsIgnoreCase():忽略大小写比较
  • startsWith()/endsWith():判断开头 / 结尾
  • regionMatches():比较指定区域字符

hashCode () 方法

String 的哈希算法:31 倍哈希法(经典高效)

public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; // 公式:h = 31 * h + val[i] for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

为什么用 31?

  1. 31 是质数,减少哈希冲突
  2. 31 * i = (i << 5) - i,JVM 会自动优化,运算极快

查找字符 / 子串方法

1. indexOf ()

朴素字符串匹配算法:先找首字符,再匹配剩余字符,效率满足日常使用

static int indexOf(...) { char first = 目标首字符; // 遍历源数组,找到首字符后逐位匹配 }
2. lastIndexOf()

反向查找,逻辑和 indexOf 一致

截取、拼接、替换方法

1. substring()

不修改原字符串,创建新 String 对象返回:

public String substring(int begin, int end) { return new String(value, begin, 长度); }
2. concat()

字符串拼接:拷贝数组 → 追加内容 → 返回新字符串

3. replace()

替换字符:先找到第一个匹配字符,再统一替换,返回新字符串

大小写、去空格方法

  1. toLowerCase()/toUpperCase():考虑地区、特殊字符(如希腊字母)的大小写转换
  2. trim()删除首尾 <= 空格的字符(空格、制表符、换行符)

valueOf 系列

静态方法,将任意类型转为字符串:

public static String valueOf(int i) { return Integer.toString(i); } public static String valueOf(Object obj) { return obj == null ? "null" : obj.toString(); }

安全转换,避免空指针

intern () 本地方法

public native String intern();

字符串常量池核心方法:

  • 调用时,将字符串放入常量池
  • 返回常量池中的引用 → 用于节省内存,实现字符串复用

二、String常量池

String 常量池到底是什么?

一句话:String 常量池 = 一块专门放字符串的缓存区域目的:复用字符串,少创建对象,省内存,提高效率

它是JVM 专门给 String 做的优化

什么字符串会进常量池?

双引号括起来的字符串字面量,会自动进常量池

String s = "abc"; // 自动进池

下面这些绝对不会自动进池

  • new String("abc")
  • 字符串拼接a + b
  • 从文件、配置、数据库、网络读取
  • 方法返回的字符串

这些都在里,不进池

常量池的核心规则

  1. 创建字符串前,先去池里找有没有相同内容
  2. 有 → 直接复用池里对象,不新建
  3. 没有 → 创建后放进池里

所以:

String s1 = "abc"; String s2 = "abc"; s1 == s2 → true

因为复用了同一个对象

new String()为什么不会进池?

String s = new String("abc");

执行过程:

  1. "abc"→ 进常量池
  2. new String(...)在堆里创建一个新对象
  3. 堆对象 ≠ 池对象
"abc" == new String("abc") → false

JDK 7 以后常量池的重大变化

JDK 6:

  • 常量池在永久代(PermGen)
  • 空间小,容易 OOM
  • intern()会把字符串复制到常量池

JDK 7+(包括 8、11、17):

  • 常量池移到堆(Heap)
  • intern()不再复制对象
  • 池里存的是堆对象的引用

这就是为什么:

String s = new String("1") + new String("1"); s.intern(); String s2 = "11"; s == s2 → true(JDK8)

intern()到底做了什么?

s.intern();

作用:把当前字符串手动加入常量池

规则:

  1. 池中有相同内容 →返回池中的对象
  2. 池中没有 →把当前对象存入池,返回自己

intern () 就是让堆字符串也能享受常量池复用

字符串拼接进不进池?

1. 纯常量拼接(进池)

String s = "a" + "b" + "c";

编译器优化成"abc"进池

2. 变量拼接(不进池)

String s = a + b;

底层new StringBuilder()堆对象,不进池

总结

"abc" → 常量池
new String("abc") → 堆
a + b → 堆
读取文件/配置/DB → 堆
常量池对象 == 常量池对象 → true
堆对象 == 常量池对象 → false
堆对象 == 堆对象 → false
  1. 常量池 = 字符串缓存,用来复用对象
  2. 双引号字面量自动进池
  3. new / 拼接 / 读取 → 不进池
  4. intern () 手动把堆字符串丢进池
  5. == 比地址,equals 比内容
  6. JDK7+ 常量池在堆里,intern 不复制,存引用

三、常见面试点

String 核心

  1. String 是不可变类(Immutable)
  2. 底层是private final char[] value(JDK9 是 byte [])
  3. 所有字符串操作(截取、替换、拼接)都不会修改原字符串,只会返回新字符串
  4. 不可变 = 线程安全 = 可以安全缓存 = 可以常量池共享

String 一旦创建,内容永远不能改,改了就是新对象

创建 String 的两种方式

1. 字面量创建

String s = "abc";

特点:

  • 自动进入字符串常量池
  • 重复创建会复用池里对象,不新建
  • 内存最省、最快

2. new 创建

String s = new String("abc");

特点:

  • 一定在堆里创建新对象
  • 不会自动入池
  • 即使内容一样,==也不相等

== 和 equals 的区别

  • ==:比较地址
  • equals():比较内容

规则:

  • 字面量之间 == 可以用
  • 只要有一个是堆对象(new / 读取 / 拼接),== 大概率 false
  • 比较字符串内容永远用 equals
"abc" == "abc" → true new String("abc") == "abc" → false

字符串常量池重点

作用

复用字符串对象,减少内存,提高速度

什么时候自动入池?

只有代码里写的双引号字面量会自动入池

什么不会自动入池?

  • new String()
  • 字符串拼接
  • 配置文件读取
  • 数据库读取
  • 网络读取
  • 文件读取

手动入池:intern ()

  • 把堆字符串丢进常量池
  • 内容相同则复用池里对象
  • 目的:省内存 + 让 == 可以用

字符串拼接重点

String s = "a" + "b" + "c";
  • 编译期优化 → 直接变成"abc"→ 入池
String s = a + b;
  • 运行期拼接
  • 底层 new StringBuilder ()
  • 结果一定是堆对象,不入池

String 常用方法核心特点

  • substring()→ 不修改原串,返回新串
  • replace()→ 不修改原串,返回新串
  • trim()→ 不修改原串,返回新串
  • toLowerCase()→ 不修改原串,返回新串
  • hashCode()→ 使用 31 倍哈希算法,缓存起来不重复计算

String 为什么不可变?

好处:

  1. 线程安全
  2. 可以缓存 hashCode
  3. 可以安全用作 HashMap key
  4. 可以进常量池共享,省内存
  5. 安全(防止被意外篡改)

StringBuilder 与 StringBuffer 区别

String

s += "a";

本质 创建新对象 性能差

StringBuilder

可变

StringBuilder sb = new StringBuilder();

源码:char[] value;

append:直接修改数组

无锁,线程不安全,性能最高

StringBuffer

public synchronized

大量同步锁,虽然保证了线程安全,但是性能低于Builder

单线程 使用StringBuilder,多线程使用StringBuffer

String拼接原理

String s = a + b + c;

编译后结果

new StringBuilder() .append(a) .append(b) .append(c) .toString();

反编译结果

StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); sb.append(c); return sb.toString();

String常量池、运行时常量池、Class常量池区别

名称存储内容
Class常量池编译后的字面量和符号引用
运行时常量池JVM加载类后产生
String常量池专门缓存字符串对象

四、常见面试题

1、创建对象 & 常量池

String s1 = "java"; String s2 = "java"; String s3 = new String("java"); String s4 = new String("java");

1.1、s1 == s2s1 == s3s3 == s4结果是true/false?

s1 == s2
System.out.println(s1 == s2);

结果:true

原因:

String s1 = "java"; String s2 = "java";

字符串字面量:

"java"

会进入字符串常量池

执行 s1 时:

常量池不存在 "java"

创建 "java"

s1 指向常量池对象

执行 s2 时:

发现常量池已有 "java"

直接复用

s2 指向同一个对象

s1 == s2

s1 == s2比较的是地址:所以结果是true

s1 == s3
System.out.println(s1 == s3);

结果:false

分析如下

String s3 = new String("java");

执行过程:

第一步

检查常量池:"java"已经存在

第二步

new String()在堆中创建新的 String 对象

源码:

public String(String original) { this.value = original.value; this.hash = original.hash; }

会创建一个新的 String 外壳对象
虽然s1.equals(s3)为true,因为比较的是值

但是s1 == s3比较的是引用地址,两个对象,所以结果为false

s3 == s4
System.out.println(s3 == s4);

结果:false

执行:

String s3 = new String("java"); String s4 = new String("java");

每次new String(...)都会创建新的堆对象

因此s3 == s4

比较的是:StringA的地址和StringB的地址

所以结果是false

1.2、一共创建了几个对象?

答案:3个对象

对象1:常量池中

"java"

对象2:产生的堆对象(s3)

new String("java")

对象3:产生的堆对象(s4)

new String("java")
String Pool : 1个 Heap : s3对应对象 1个 s4对应对象 1个 总计: 3个对象

2、字符串拼接

// 代码片段1 String a = "ab" + "cd"; // 代码片段2 String x = "ab"; String y = "cd"; String b = x + y;

两段代码最终ab是否进入常量池?a == "abcd"b == "abcd"结果分别是什么?

解释:+拼接字符串,编译期常量变量拼接底层实现有什么区别?

String a = "ab" + "cd";

结果

a == "abcd" // true

编译期发生了什么

因为:"ab"、"cd"都是字面量常量

编译器在编译阶段直接优化为

String a = "abcd";

这叫:常量折叠(Constant Folding)

相当于字节码:

LDC "abcd"

直接从常量池加载

a == "abcd"

比较的是同一个常量池对象

结果是true

String x = "ab"; String y = "cd"; String b = x + y;

结果:

b == "abcd" // false

为什么?

虽然:x 内容是"ab"

但是x是变量

编译器无法保证运行时一定还是:"ab"

所以不能做常量折叠

编译器会生成:

String b = new StringBuilder() .append(x) .append(y) .toString();

JDK8 反编译基本类似

StringBuilder sb = new StringBuilder(); sb.append(x); sb.append(y); String b = sb.toString();

执行时:

String Pool "ab" "cd" "abcd"
Heap new StringBuilder() ↓ new String("abcd")

注意:

StringBuilder.toString()

返回的是:new String(...)新的堆对象,不会自动进入常量池

因此:

b == "abcd"

变成了堆对象==常量池对象

结果是false

http://www.jsqmd.com/news/931582/

相关文章:

  • 基于MQTT与Docker的物联网数据采集与可视化实战
  • Photoshop AI插件SD-PPP:在Photoshop中直接使用AI绘图
  • social-auto-upload Webhook集成:事件驱动自动化工作流终极指南
  • 从零开始:B站缓存视频合并工具的完整使用旅程 [特殊字符]
  • 重新定义AI换脸工作流:ComfyUI Reactor Node的技术突破与应用革命
  • Rusted PackFile Manager终极指南:3个核心场景教你快速上手《全面战争》模组制作
  • 91.开源跨平台刷机Bash脚本!自动识别设备+固件校验+分区刷写全自动化
  • Arduino红外传感与舵机控制:打造万圣节自动糖果分发器
  • 武汉圣擎航空:蒙特哥贝机票全攻略与GEO营销实战 - 土星买买买
  • KMS智能激活工具:3分钟完成Windows和Office永久激活的完整指南
  • 牛客小白月赛133
  • 抖音无水印下载终极指南:3个超简单步骤搞定视频批量保存
  • UI-TARS桌面应用深度部署指南:构建企业级视觉智能体系统
  • 巧用 okbiye 论文优化工具:轻松攻克学术查重与 AI 内容筛查难题
  • 物理层 → 数据链路层 → 网络层 → 传输层 → 会话层 → 表示层 → 应用层
  • Sora 2汽车设计展示,深度拆解其在GB/T 39786-2021数字孪生认证中的6项关键通过证据
  • 2026-2027年度超声波流量计源头厂家推荐榜:国产十大品牌深度测评与权威指南 - 仪表品牌排行榜
  • Tailwind CSS 的核心哲学:从“组件优先”到“功能优先”
  • Java课程
  • 应急响应——Web漏洞:命令执行+SSRF+弱口令
  • 当小程序不只是“工具”:为什么畔游科技是企业“懂成长的伙伴”? - 新闻快传
  • 学术文稿优化新思路:借助 okbiye 实现论文精准降重与 AI 痕迹淡化
  • Linux CIFSwitch 内核新漏洞允许攻击者获得 root 权限
  • 计算机二级备考资料合集:刷题、知识点与考前整理思路
  • 这款工具让图片悬浮在手机屏幕之上
  • 当AI开始驱动工作:从落地到实践的完整思考
  • 92.手机系统故障深度修复:软砖/硬砖/分区损坏一站式刷机解决方案
  • 告别 “格式焦虑”!paperxie 智能排版,让毕业论文格式一步对齐 4000 + 高校规范
  • 别再死磕论文飘红和 AI 检测!okbiye 多方案降重 + 降 AIGC,一键适配知网 / 维普 / Turnitin
  • 上海小程序开发服务商综合能力排行:帮你找到对的外包技术团队 - 新闻快传