Java String 详解

Java String 详解

String 是 Java 中最常用的引用类型之一,用于表示不可变的字符序列。其设计特性(不可变性、常量池缓存)直接影响程序的性能和安全性,以下从核心特性、创建方式、常量池、常用方法、与其他字符串类的对比等方面全面解析。

一、核心特性:不可变性(Immutability)

1. 定义

String 被声明为 final class,且内部存储字符的数组 value 也是 final 的,这意味着:

  • String 对象创建后,其内容(字符序列)无法被修改

  • 任何看似 “修改” String 的操作(如拼接、替换、截取),本质都是创建新的 String 对象,原对象内容不变。

2. 底层实现(JDK 版本差异)

  • JDK 8 及之前:底层用 private final char[] value 存储字符(每个 char 占 2 字节,基于 UTF-16 编码);

  • JDK 9 及之后:底层改为private final byte[] value+ 编码标记(LATIN1 或 UTF16)

    • 若字符串仅包含 ASCII 字符(占 1 字节),使用 LATIN1 编码,节省 50% 内存;

    • 含非 ASCII 字符时,仍用 UTF16 编码(兼容旧版本)。

3. 不可变性的优缺点

优点:
  • 线程安全:不可变对象天然线程安全,无需加锁,可在多线程中直接共享;

  • 常量池复用:相同内容的字符串可复用常量池中的对象,减少内存占用;

  • 哈希值缓存:String 的 hashCode() 会缓存首次计算的哈希值(因内容不变,哈希值不会变),作为 HashMap 等集合的键时,查询效率更高。

缺点:
  • 频繁修改字符串(如循环拼接)会创建大量临时对象,导致内存开销大、效率低(需用 StringBuilder/StringBuffer 替代)。

二、String 的创建方式(2 种核心方式)

String 的创建方式决定了对象的存储位置(常量池 vs 堆),直接影响 == 比较结果。

1. 直接赋值(推荐)

String s1 = "abc"; // 优先从字符串常量池获取对象
String s2 = "abc"; // 复用常量池中的同一个对象
System.out.println(s1 == s2); // true(地址相同)
System.out.println(s1.equals(s2)); // true(内容相同)
  • 原理:JVM 会先检查字符串常量池(String Pool)中是否存在 "abc":

    • 若存在,直接返回常量池中的对象引用;

    • 若不存在,在常量池创建 "abc" 对象,再返回引用。

  • 特点:高效、节省内存(复用常量池对象)。

2. new String () 方式

String s3 = new String("abc"); // 堆中创建新对象,同时检查常量池
String s4 = new String("abc"); // 堆中创建另一个新对象
System.out.println(s3 == s4); // false(堆中不同对象,地址不同)
System.out.println(s3.equals(s4)); // true(内容相同)
System.out.println(s3 == s1); // false(堆对象 vs 常量池对象,地址不同)
  • 原理:

    1. 先检查常量池:若没有 "abc",则在常量池创建 "abc" 对象;

    2. 无论常量池是否存在,都会在堆内存中创建一个新的 String 对象,其 value 数组引用常量池中的字符序列。

  • 特点:创建 1 个或 2 个对象(取决于常量池是否已有目标字符串),内存开销大,不推荐直接使用。

3. 关键方法:intern ()

intern() 用于手动将字符串对象 “入池”,返回常量池中的对象引用(JDK 1.7+ 优化):

  • JDK 1.6 及之前:若常量池无该字符串,会复制堆中字符串的内容到常量池,返回常量池新对象;

  • JDK 1.7 及之后:若常量池无该字符串,不会复制内容,而是将堆中对象的引用存入常量池,返回堆对象的引用(节省内存)。

示例(JDK 8+):

String s5 = new String("a") + new String("b"); // 堆中创建 "ab" 对象(常量池无 "ab")
String s6 = s5.intern(); // 常量池存入 s5 的引用,返回 s5
String s7 = "ab"; // 直接获取常量池中的 s5 引用
System.out.println(s5 == s6); // true(s6 是 s5 的引用)
System.out.println(s5 == s7); // true(s7 复用常量池中的 s5 引用)

三、字符串常量池(String Pool)

1. 定义与作用

字符串常量池是 JVM 为 String 设计的缓存机制,本质是一个哈希表(HashTable),存储字符串常量的引用(JDK 1.7+)或内容(JDK 1.6-)。

  • 作用:避免重复创建相同内容的字符串,减少内存占用,提高访问效率。

2. 存储位置变化

  • JDK 1.6 及之前:位于方法区(永久代)

  • JDK 1.7 及之后:移至堆内存(因永久代空间有限,易导致 OOM,堆空间更大且可动态扩展)。

3. 常量池触发时机

  • 直接赋值字符串(如 "abc"):编译期将字符串存入常量池(.class 文件的 Constant Pool 表),类加载时加载到 JVM 常量池;

  • 字符串字面量拼接(如 "a" + "b"):编译期优化为 "ab",直接存入常量池;

  • 动态拼接(如 s1 + "b",s1 是变量):编译期无法优化,运行时通过 StringBuilder 拼接,结果存入堆(需手动 intern() 入池)。

四、String 常用方法(按功能分类)

String 提供了大量操作字符序列的方法,以下是高频使用场景:

1. 字符串比较

方法功能注意事项
equals(Object obj)比较字符串内容是否相同(区分大小写)重写自 Object,核心方法
equalsIgnoreCase(String str)比较内容(不区分大小写)如 "ABC".equalsIgnoreCase ("abc") → true
compareTo(String str)按字典序比较(返回 int)相等返回 0;前小后大返回负数;反之正数
==比较对象地址是否相同切勿用于内容比较(仅直接赋值的相同字符串返回 true)

示例:

String a = "abc";
String b = new String("abc");
System.out.println(a.equals(b)); // true(内容)
System.out.println(a == b); // false(地址)
System.out.println("a".compareTo("b")); // -1(字典序 a < b)

2. 查找与判断

方法功能
indexOf(String str)查找 str 首次出现的索引(无则返回 -1)
lastIndexOf(String str)查找 str 最后出现的索引(无则返回 -1)
contains(CharSequence s)判断是否包含指定字符序列
startsWith(String prefix)判断是否以指定前缀开头
endsWith(String suffix)判断是否以指定后缀结尾
isEmpty()判断长度是否为 0(JDK 6+)
isBlank()判断是否全为空白字符(JDK 11+,含空格、制表符等)

示例:

String str = "hello world";
System.out.println(str.indexOf("o")); // 4
System.out.println(str.contains("world")); // true
System.out.println(str.startsWith("he")); // true
System.out.println("  ".isBlank()); // true(JDK 11+)

3. 截取与分割

方法功能注意事项
substring(int beginIndex)从 beginIndex 截取到末尾
substring(int beginIndex, int endIndex)截取 [beginIndex, endIndex)(左闭右开)JDK 6 复制字符数组,JDK 7+ 复用原数组偏移量(可能内存泄漏)
split(String regex)按正则表达式分割,返回字符串数组正则需转义(如分割 "." 需用 \\.
split(String regex, int limit)限制分割次数(limit 为分割后数组长度)

示例:

String str = "a.b.c";
String[] arr1 = str.split("\\."); // ["a", "b", "c"](正则转义)
String[] arr2 = str.split("\\.", 2); // ["a", "b.c"](限制分割 1 次)
​
String sub = str.substring(2, 4); // "b."(索引 2 是 'b',4 是 '.')

4. 替换

方法功能注意事项
replace(char oldChar, char newChar)替换所有指定字符非正则
replace(CharSequence oldSeq, CharSequence newSeq)替换所有指定字符序列非正则
replaceAll(String regex, String replacement)按正则替换所有匹配内容正则(如替换空格用 \\s+
replaceFirst(String regex, String replacement)按正则替换首次匹配内容正则

示例:

String str = "hello 123 world 456";
System.out.println(str.replace('l', 'x')); // "hexxo 123 worxd 456"
System.out.println(str.replaceAll("\\d+", "num")); // "hello num world num"(替换所有数字)

5. 转换与格式化

方法功能
toUpperCase()转为大写(受默认 Locale 影响)
toLowerCase()转为小写(受默认 Locale 影响)
toCharArray()转为 char 数组
valueOf(任意类型)静态方法,将其他类型转为 String(推荐,避免空指针)
trim()去除首尾空白字符(仅 ASCII 空格,JDK 6+)
strip()去除首尾空白字符(支持 Unicode,JDK 11+)
format(String format, Object... args)静态方法,格式化字符串(如 String.format("年龄:%d", 20)

示例:

String numStr = String.valueOf(123); // "123"(避免 new String(123) 错误)
String str = "  Hello  ";
System.out.println(str.trim()); // "Hello"(仅去除 ASCII 空格)
System.out.println(str.strip()); // "Hello"(支持 Unicode 空白)
String fmt = String.format("%.2f", 3.1415); // "3.14"(保留 2 位小数)

6. 其他常用方法

  • length():返回字符串长度(字符数,非字节数);

  • charAt(int index):返回指定索引的字符(索引从 0 开始);

  • join(CharSequence delimiter, CharSequence... elements):静态方法,用分隔符拼接字符串数组(JDK 8+)。

示例:

String[] arr = {"a", "b", "c"};
String joined = String.join("-", arr); // "a-b-c"
System.out.println(joined.charAt(1)); // '-'(索引 1)
System.out.println(joined.length()); // 5

五、String 与 StringBuilder、StringBuffer 的对比

因 String 不可变,频繁修改(如循环拼接)需用可变字符串类,三者核心区别如下:

特性StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全天然安全(不可变)非线程安全(无同步锁)线程安全(方法加 synchronized
效率低(频繁修改创建新对象)高(直接操作字符数组)中(同步开销)
底层实现byte[](JDK9+)/ char[](JDK8-)char [](可扩容)char [](可扩容)
适用场景字符串不修改(如常量、配置)单线程下频繁修改(如拼接、替换)多线程下频繁修改(如日志拼接)

关键注意点:

  • 字符串拼接效率:StringBuilder > StringBuffer > String(String 循环拼接等价于 new StringBuilder().append().toString(),但每次循环创建新对象);

  • StringBuilder 扩容机制:默认初始容量 16,扩容时按 oldCapacity * 2 + 2 计算,不足则直接用目标长度,扩容会复制数组,建议初始化时指定容量(如 new StringBuilder(1024))减少扩容次数。

示例(高效拼接):

// 推荐:单线程下指定容量的 StringBuilder
StringBuilder sb = new StringBuilder(100);
for (int i = 0; i < 100; i++) {
    sb.append(i).append(",");
}
String result = sb.toString(); // 仅创建 1 个 StringBuilder 和 1 个 String

六、常见问题与面试题

1. ==equals() 的区别?

  • ==:比较对象的内存地址(基本类型比较值,引用类型比较地址);

  • equals():String 重写后比较内容,默认 Object 类的 equals() 等价于 ==

2. 为什么 String 是 final 的?

  • 保证不可变性:避免被继承后修改 value 数组,破坏常量池复用和线程安全;

  • 安全:作为 HashMap 等集合的键时,不可变性确保哈希值不变,避免查找失效;

  • 高效:常量池复用依赖不可变性,否则缓存的字符串可能被篡改。

3. 以下代码创建了几个对象?(JDK 8+)

String s1 = "abc";
String s2 = new String("abc");
String s3 = new String("a") + new String("b");
  • 分析:

    1. s1:常量池无 "abc",创建 1 个常量池对象;

    2. s2:常量池已有 "abc",仅创建 1 个堆对象;

    3. s3new String("a") 创建 1 个堆对象(常量池创建 "a"),new String("b") 创建 1 个堆对象(常量池创建 "b"),拼接时创建 StringBuilder 对象,最终 toString() 创建 1 个堆对象("ab",常量池无),共 5 个对象(常量池 2 个:"a"、"b";堆 3 个:"a"、"b"、"ab" + StringBuilder)。

4. JDK 9 为什么把 String 底层从 char [] 改为 byte []?

  • 节省内存:大部分字符串是 ASCII 字符(占 1 字节),char [] 每个字符占 2 字节,byte [] + LATIN1 编码可减少 50% 内存占用;

  • 兼容旧版本:非 ASCII 字符仍用 UTF16 编码,通过 coder 标记区分编码方式。

七、总结

String 的核心是不可变性,基于常量池实现高效复用,适用于字符串不修改的场景;若需频繁修改,优先使用 StringBuilder(单线程)或 StringBuffer(多线程)。

关键要点:

  1. 直接赋值复用常量池,new String() 创建堆对象;

  2. equals()== 更安全(比较内容);

  3. 频繁修改用可变字符串类,初始化指定容量提升效率;

  4. 注意 JDK 版本差异(底层存储、intern()substring() 等)。

掌握 String 的设计原理和使用场景,能有效优化程序性能,避免常见坑(如内存泄漏、正则转义错误)。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值