--- title: "01-String详解" created: 2025-11-25 tags: - Java --- # String详解 String是Java中最常用的类,具有**不可变性(Immutable)**特性。理解String的底层实现、常量池机制和性能优化,是Java开发的基础。 --- ## 一、String的不可变性 ### 1.1 底层实现演进 ```java // JDK 8及之前:char数组 private final char value[]; // JDK 9及之后:byte数组 + 编码标识 private final byte[] value; private final byte coder; // 0=LATIN1, 1=UTF16 ``` **为什么改为byte数组?** - 大部分字符串是Latin-1字符(ASCII),用1字节足够 - 节省内存:Latin-1字符串内存减少50% - coder标识编码方式,按需选择1字节或2字节存储 ### 1.2 不可变性的实现 ```java public final class String { // ① final类,不可继承 private final byte[] value; // ② final数组,引用不可变 private final byte coder; // ③ 编码标识不可变 private int hash; // ④ 缓存hashCode // 没有提供修改value的方法 // ⑤ 无修改方法 } ``` **三重保障:** 1. **final类**:防止子类破坏不可变性 2. **final数组**:引用不可变(但数组内容理论上可变) 3. **无修改方法**:不暴露修改内部数据的接口 ### 1.3 为什么设计为不可变? | 原因 | 说明 | | --- | --- | | **线程安全** | 不可变对象天然线程安全,无需同步 | | **缓存hashCode** | 只需计算一次,HashMap性能提升 | | **字符串常量池** | 不可变才能安全共享,节省内存 | | **安全性** | 防止敏感信息(如密码)被篡改 | | **类加载安全** | 类名不可变,防止恶意篡改类加载 | ### 1.4 "修改"String的真相 ```java String s = "hello"; s = s + " world"; // 不是修改原对象,而是创建新对象 // 等价于 String s = "hello"; String temp = new StringBuilder(s).append(" world").toString(); s = temp; // s指向新对象,原"hello"对象不变 ``` --- ## 二、String常量池 ### 2.1 什么是字符串常量池? 字符串常量池(String Pool)是JVM维护的一块特殊内存区域,用于存储字符串字面量,实现字符串复用。 **位置变化:** - JDK 6及之前:永久代(PermGen) - JDK 7及之后:堆内存(Heap) ### 2.2 字面量 vs new ```java // 方式1:字面量(推荐) String s1 = "hello"; // 先查常量池,有则复用,无则创建并入池 String s2 = "hello"; // 复用常量池中的对象 System.out.println(s1 == s2); // true(同一对象) // 方式2:new关键字 String s3 = new String("hello"); // 堆中创建新对象 String s4 = new String("hello"); // 又创建一个新对象 System.out.println(s3 == s4); // false(不同对象) System.out.println(s1 == s3); // false(常量池 vs 堆) ``` ### 2.3 new String("abc") 创建了几个对象? ```java String s = new String("abc"); ``` **答案:1个或2个** 1. **常量池中的"abc"**:如果常量池中没有"abc",则创建一个 2. **堆中的String对象**:new关键字必定在堆中创建一个新对象 ```java // 验证 String s1 = "abc"; // 常量池创建"abc" String s2 = new String("abc"); // 仅在堆中创建,常量池已有 System.out.println(s1 == s2); // false System.out.println(s1 == s2.intern()); // true ``` ### 2.4 intern()方法 ```java public native String intern(); ``` **作用:** 将字符串放入常量池,并返回常量池中的引用 ```java String s1 = new String("hello"); // 堆中对象 String s2 = s1.intern(); // 返回常量池中的"hello" String s3 = "hello"; // 常量池中的"hello" System.out.println(s1 == s2); // false(堆 vs 常量池) System.out.println(s2 == s3); // true(都是常量池) ``` ### 2.5 JDK 6 vs JDK 7+ 的intern()差异 ```java String s = new String("1") + new String("1"); // 堆中创建"11" s.intern(); // 将"11"放入常量池 String s2 = "11"; // JDK 6: s == s2 → false // JDK 7+: s == s2 → true ``` **原因:** - JDK 6:intern()在永久代创建新对象 - JDK 7+:intern()直接将堆中对象的引用放入常量池(不复制) ### 2.6 字符串拼接的常量池行为 ```java // 编译期确定 String s1 = "a" + "b" + "c"; // 编译优化为 "abc" String s2 = "abc"; System.out.println(s1 == s2); // true // 运行期确定 String a = "a"; String s3 = a + "bc"; // 运行时拼接,堆中创建新对象 System.out.println(s2 == s3); // false // final修饰 final String b = "a"; String s4 = b + "bc"; // 编译期确定,等价于 "abc" System.out.println(s2 == s4); // true ``` --- ## 三、String常用方法 ### 3.1 比较方法 ```java // equals():比较内容 "hello".equals("hello"); // true "hello".equals("Hello"); // false // equalsIgnoreCase():忽略大小写 "hello".equalsIgnoreCase("Hello"); // true // compareTo():字典序比较 "abc".compareTo("abd"); // -1(c < d) "abc".compareTo("abc"); // 0 "abd".compareTo("abc"); // 1 ``` ### 3.2 查找方法 ```java String s = "hello world"; s.indexOf("o"); // 4(首次出现位置) s.lastIndexOf("o"); // 7(最后出现位置) s.contains("world"); // true s.startsWith("he"); // true s.endsWith("ld"); // true ``` ### 3.3 截取与替换 ```java String s = "hello world"; s.substring(0, 5); // "hello"(左闭右开) s.replace("l", "L"); // "heLLo worLd" s.replaceAll("\\s", "-"); // "hello-world"(正则) s.trim(); // 去除首尾空白 s.strip(); // JDK 11+,去除首尾空白(支持Unicode空白) ``` ### 3.4 分割与连接 ```java // 分割 "a,b,c".split(","); // ["a", "b", "c"] // 连接(JDK 8+) String.join("-", "a", "b", "c"); // "a-b-c" String.join("-", Arrays.asList("a", "b", "c")); // "a-b-c" ``` --- ## 四、高频面试题 ### Q1: String为什么设计成不可变的? **答:** 1. **线程安全**:不可变对象天然线程安全 2. **字符串常量池**:不可变才能安全共享 3. **缓存hashCode**:只需计算一次,提升HashMap性能 4. **安全性**:防止敏感信息被篡改 5. **类加载安全**:类名不可变,防止恶意篡改 ### Q2: String、StringBuilder、StringBuffer的区别? | 特性 | String | StringBuilder | StringBuffer | | --- | --- | --- | --- | | 可变性 | 不可变 | 可变 | 可变 | | 线程安全 | 是(不可变) | 否 | 是(synchronized) | | 性能 | 拼接慢 | 最快 | 较快 | | 使用场景 | 少量操作 | 单线程拼接 | 多线程拼接 | ### Q3: String s = new String("abc") 创建了几个对象? **答:** 1个或2个 - 如果常量池没有"abc":2个(常量池1个 + 堆1个) - 如果常量池已有"abc":1个(仅堆中1个) ### Q4: String的intern()方法有什么作用? **答:** - 将字符串放入常量池 - 如果常量池已存在,返回常量池中的引用 - JDK 7+:如果堆中有而常量池没有,直接将堆对象引用放入常量池 ### Q5: 为什么JDK 9将String底层从char[]改为byte[]? **答:** - 大部分字符串是Latin-1字符,用1字节足够 - 节省内存:Latin-1字符串内存减少50% - 通过coder字段标识编码,按需选择存储方式 ### Q6: "a" + "b" + "c" 会创建几个对象? **答:** 1个 - 编译器优化为 "abc" - 直接在常量池中创建(如果不存在) --- ## 五、最佳实践 ### ✅ 推荐做法 ```java // 1. 优先使用字面量 String s = "hello"; // 而非 new String("hello") // 2. 字符串比较用equals() if (s.equals("hello")) { } // 而非 s == "hello" // 3. 常量放前面,避免NPE if ("hello".equals(s)) { } // s为null也不会报错 // 4. 多次拼接用StringBuilder StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100; i++) { sb.append(i); } String result = sb.toString(); ``` ### ❌ 避免做法 ```java // 1. 循环中拼接字符串 String s = ""; for (int i = 0; i < 100; i++) { s += i; // 每次创建新对象,性能差 } // 2. 用 == 比较字符串内容 if (s == "hello") { } // 比较的是引用,不是内容 // 3. 不必要的new String s = new String("hello"); // 浪费内存 ``` --- ## 相关链接 - [[02-StringBuilder系列|02-StringBuilder系列]] - 可变字符串的使用 - [[03-BigDecimal|03-BigDecimal]] - 精确数值计算 --- ## 记忆口诀 ``` String不可变,三重保障全 final类和数组,无修改方法添 常量池省内存,字面量优先选 拼接用Builder,循环莫用加 ``` --- ⬅️ [[04-类型转换规则|类型转换规则]] 🏠 [[00-Java|00-Java]] ➡️ [[02-StringBuilder系列|StringBuilder系列]]