(十六)Java String类全面解析
一、String类概述
1.1 String的本质
在Java中,String类可能是使用最频繁的类之一,但它也是最容易被误解的类之一。从本质上讲,String代表的是一个不可变的Unicode字符序列。这种不可变性(immutability)是String类设计的核心特性。
java
String str = "Hello";
str = str + " World"; // 实际上是创建了一个新的String对象
当我们在Java中创建一个String对象时,它会被存储在Java内存的特定区域——字符串常量池(String Pool)中。这种设计带来了性能和内存使用的优化。
1.2 String在内存中的存储
理解String在内存中的存储方式对于编写高效Java程序至关重要:
-
字符串常量池:位于方法区(Method Area)的一部分,用于存储字符串字面量
-
堆内存:通过
new
关键字创建的String对象存储在堆中 -
intern机制:可以手动将字符串放入常量池
java
String s1 = "Java"; // 存储在字符串常量池
String s2 = new String("Java"); // 存储在堆内存
String s3 = s2.intern(); // 返回常量池中的引用
1.3 String的不可变性
String的不可变性体现在以下几个方面:
-
类设计:String类被声明为
final
,防止被继承和修改 -
内部存储:内部使用
final char[]
(JDK9后改为byte[]
)存储数据 -
方法行为:所有看似修改字符串的方法都返回新对象
不可变性的优点:
-
线程安全
-
可以缓存hash值
-
便于实现字符串池
-
安全性考虑(如网络连接、文件路径等)
二、String的创建方式
2.1 字面量创建
最直接的方式是使用双引号创建字符串字面量:
java
String str1 = "Java";
String str2 = "Java"; // 重用常量池中的相同对象
System.out.println(str1 == str2); // true,引用相同对象
2.2 new关键字创建
使用构造器创建String对象会在堆中创建新实例:
java
String str3 = new String("Java");
String str4 = new String("Java");
System.out.println(str3 == str4); // false,不同对象
2.3 字符数组创建
可以从字符数组创建String对象:
java
char[] charArray = {'J', 'a', 'v', 'a'};
String str5 = new String(charArray);
2.4 字节数组创建
处理字节流时常用此方式,可以指定字符编码:
java
byte[] byteArray = {74, 97, 118, 97};
String str6 = new String(byteArray, StandardCharsets.UTF_8);
2.5 StringBuffer/StringBuilder创建
从可变字符串类构建:
java
StringBuffer buffer = new StringBuffer("Java");
StringBuilder builder = new StringBuilder("Java");
String str7 = new String(buffer);
String str8 = new String(builder);
三、String常用API详解
3.1 字符串基本信息
3.1.1 length() - 获取长度
返回字符串的Unicode字符数:
java
String str = "Java编程";
System.out.println(str.length()); //
注意与数组的length
属性区分,数组是属性,字符串是方法。
3.1.2 isEmpty() - 判断空串
检查字符串是否为空(长度为0):
java
"".isEmpty(); // true
" ".isEmpty(); // false
null.isEmpty(); // NullPointerException
3.1.3 isBlank() - JDK11新增
检查字符串是否为空或仅包含空白字符:
java
"".isBlank(); // true
" ".isBlank(); // true
"\t\n".isBlank(); // true
3.2 字符串比较
3.2.1 equals() - 内容相等
比较字符串内容是否相同:
java
String s1 = "Java";
String s2 = new String("Java");
s1.equals(s2); // true
3.2.2 equalsIgnoreCase() - 忽略大小写
不区分大小写的比较:
java
"Java".equalsIgnoreCase("JAVA"); // true
3.2.3 compareTo() - 字典序比较
按Unicode值比较字符串,返回差值:
java
"apple".compareTo("banana"); // 负值(a<b)
"zoo".compareTo("apple"); // 正值(z>a)
"java".compareTo("java"); // 0
3.2.4 contentEquals() - 与CharSequence比较
比较与任何CharSequence(如StringBuilder)的内容:
java
String str = "Java";
StringBuilder sb = new StringBuilder("Java");
str.contentEquals(sb); // true
3.3 字符串查找
3.3.1 indexOf() - 查找字符/子串
返回第一次出现的索引:
java
"Java".indexOf('a'); // 1
"Java".indexOf("va"); // 2
"Java".indexOf('a', 2); // 从索引2开始找,返回3
3.3.2 lastIndexOf() - 反向查找
返回最后一次出现的索引:
java
"Java".lastIndexOf('a'); // 3
"Java".lastIndexOf("va"); // 2
3.3.3 contains() - 包含检查
检查是否包含指定字符序列:
java
"Java".contains("av"); // true
3.3.4 startsWith()/endsWith() - 前缀/后缀检查
java
"Java".startsWith("Ja"); // true
"Java".endsWith("va"); // true
3.3.5 matches() - 正则匹配
使用正则表达式匹配整个字符串:
java
"Java8".matches("Java\\d"); // true
3.4 字符串操作
3.4.1 substring() - 子串提取
提取子字符串:
java
"Hello World".substring(6); // "World"
"Hello World".substring(0, 5); // "Hello"
注意:JDK7u6之前substring共享原char数组可能导致内存泄漏。
3.4.2 concat() - 字符串连接
连接字符串(不如+
或StringBuilder常用):
java
"Hello".concat(" World"); // "Hello World"
3.4.3 replace() - 字符/子串替换
替换字符或子串:
java
"Java".replace('a', 'o'); // "Jovo"
"Java".replace("va", "vaEE"); // "JavaEE"
3.4.4 replaceAll()/replaceFirst() - 正则替换
使用正则表达式替换:
java
"a1b2c3".replaceAll("\\d", "-"); // "a-b-c-"
"a1b2c3".replaceFirst("\\d", "-"); // "a-b2c3"
3.4.5 split() - 字符串分割
按正则表达式分割字符串:
java
"a,b,c".split(","); // ["a", "b", "c"]
"a..b.c".split("\\.+"); // ["a", "b", "c"]
3.4.6 join() - 字符串拼接
JDK8新增,用分隔符连接字符串:
java
String.join("-", "Java", "is", "cool"); // "Java-is-cool"
3.4.7 repeat() - JDK11新增
重复字符串指定次数:
java
"Java".repeat(3); // "JavaJavaJava"
3.4.8 toLowerCase()/toUpperCase() - 大小写转换
java
"Java".toLowerCase(); // "java"
"Java".toUpperCase(); // "JAVA"
注意:在土耳其等地区可能需要指定Locale。
3.5 字符串转换
3.5.1 toCharArray() - 转为字符数组
java
char[] chars = "Java".toCharArray();
3.5.2 getBytes() - 转为字节数组
可以指定字符编码:
java
byte[] bytes1 = "Java".getBytes();
byte[] bytes2 = "Java".getBytes(StandardCharsets.UTF_8);
3.5.3 valueOf() - 其他类型转String
静态方法,将各种类型转为String:
java
String.valueOf(123); // "123"
String.valueOf(true); // "true"
String.valueOf(3.14); // "3.14"
String.valueOf(new Object()); // 调用toString()
3.5.4 format() - 格式化字符串
类似C的printf:
java
String.format("Hi, %s! You have %d messages.", "Alice", 5);
3.6 字符串空白处理
3.6.1 trim() - 去除两端空白
去除ASCII空白字符(<=U+0020):
java
" Java ".trim(); // "Java"
3.6.2 strip() - JDK11新增
去除Unicode空白字符:
java
"\u2000Java\u2000".strip(); // "Java"
3.6.3 stripLeading()/stripTrailing() - JDK11新增
仅去除前导或尾部空白:
java
" Java ".stripLeading(); // "Java "
" Java ".stripTrailing(); // " Java"
3.7 其他实用方法
3.7.1 intern() - 字符串池化
将字符串放入常量池或返回已有引用:
java
String s1 = new String("Java").intern();
String s2 = "Java";
s1 == s2; // true
3.7.2 chars() - JDK8流式处理
返回IntStream,可用于函数式处理:
java
"Java".chars().forEach(System.out::println);
3.7.3 codePoints() - 处理Unicode代码点
正确处理补充字符(如emoji):
java
"😊Java".codePoints().forEach(System.out::println);
3.7.4 lines() - JDK11按行分割
按行终止符分割字符串:
java
"Line1\nLine2\rLine3".lines().forEach(System.out::println);
四、String性能优化
4.1 字符串拼接的陷阱
字符串拼接是性能问题的常见来源:
java
// 低效写法 - 产生多个中间String对象
String result = "";
for (int i = 0; i < 100; i++) {result += i;
}// 高效写法 - 使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {sb.append(i);
}
String result = sb.toString();
4.2 字符串常量池的利用
合理利用字符串常量池可以减少内存使用:
java
// 推荐 - 使用字面量
String s1 = "Java";// 不推荐 - 除非必要,避免new String
String s2 = new String("Java");// 大量重复字符串可考虑intern()
String s3 = new String(charArray).intern();
4.3 大字符串处理
处理大文本时:
-
考虑使用
StringReader
和StringWriter
-
避免在内存中保存整个大字符串
-
使用
substring
时注意老版本的内存泄漏问题
4.4 正则表达式优化
复杂正则可能成为性能瓶颈:
java
// 预编译正则表达式
Pattern pattern = Pattern.compile("your_regex");
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {// 处理匹配
}
五、String相关类比较
5.1 String vs StringBuffer vs StringBuilder
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是(因为不可变) | 是(同步方法) | 否 |
性能 | - | 比StringBuilder慢 | 最快 |
使用场景 | 常量字符串 | 多线程环境字符串操作 | 单线程环境字符串操作 |
5.2 CharSequence接口
String、StringBuilder、StringBuffer都实现了CharSequence接口,定义了对字符序列的基本操作。当只需要读取字符序列时,使用CharSequence作为参数类型更通用。
六、Java版本演进中的String变化
6.1 JDK6及之前
-
字符串常量池位于方法区(永久代)
-
substring共享原char数组
6.2 JDK7
-
字符串常量池移到堆内存
-
substring不再共享数组,解决潜在内存泄漏
6.3 JDK8
-
引入String.join()方法
-
新增chars()和codePoints()方法
6.4 JDK9
-
String内部存储改为byte[]+编码标记字段
-
紧凑字符串(Compact Strings)优化,Latin-1字符使用单字节存储
6.5 JDK11
-
新增isBlank()、lines()、repeat()等方法
-
新增strip()系列方法处理Unicode空白
七、String在实际开发中的应用
7.1 字符串缓存
利用String不可变性实现缓存:
java
public class StringCache {private static final Map<String, String> CACHE = new HashMap<>();public static String getCachedString(String input) {return CACHE.computeIfAbsent(input, String::new);}
}
7.2 字符串作为Map的Key
String是理想的Map键类型:
-
不可变性保证哈希值不变
-
实现了正确的equals和hashCode
-
有高效的比较性能
java
Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("Java", wordCount.getOrDefault("Java", 0) + 1);
7.3 字符串与I/O操作
文件读写时的字符串处理:
java
// 读取文件内容为字符串
String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);// 写入字符串到文件
Files.write(path, content.getBytes(StandardCharsets.UTF_8));
7.4 字符串与网络通信
网络协议处理中的字符串应用:
java
// HTTP请求处理
String requestLine = "GET /index.html HTTP/1.1";
String[] parts = requestLine.split(" ");
String method = parts[0];
String path = parts[1];
String protocol = parts[2];
八、常见面试问题解析
8.1 String为什么设计为不可变?
-
安全性:防止意外修改,如网络连接、文件路径等
-
线程安全:无需同步即可在多线程中使用
-
缓存哈希:作为HashMap键时哈希值只需计算一次
-
字符串池:实现字符串常量池优化
-
类加载机制:类名等字符串需要稳定性
8.2 String、StringBuffer和StringBuilder的区别?
-
可变性:String不可变,后两者可变
-
线程安全:StringBuffer同步方法保证线程安全
-
性能:StringBuilder在单线程下性能最佳
-
使用场景:少量拼接用String,多线程用StringBuffer,单线程大量操作用StringBuilder
8.3 String的intern()方法有什么作用?
将字符串对象添加到字符串常量池(如果池中不存在),并返回池中引用。可以用于减少重复字符串的内存占用,但需谨慎使用,因为:
-
常量池大小有限
-
不当使用可能增加GC负担
-
JDK7后常量池位于堆内存,不再受永久代大小限制
8.4 如何高效拼接多个字符串?
-
少量拼接:直接使用
+
(编译器会优化为StringBuilder) -
循环内拼接:显式使用StringBuilder
-
JDK8+:使用String.join()或StringJoiner
-
大量数据:考虑使用字符流或直接操作char数组
8.5 String的hashCode()如何计算?
String的哈希计算采用多项式哈希:
java
// JDK中的实现
public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;
}
选择31的原因:
-
奇质数,减少哈希冲突
-
31 * i可以被优化为(i << 5) - i
-
经验证明对英文字符分布效果良好
九、String的最佳实践
9.1 编码规范建议
-
字符串比较总是使用equals()而非==
-
优先使用字符串字面量而非new String()
-
拼接路径使用Path类而非字符串拼接
-
处理用户输入时显式指定字符编码
-
敏感信息(如密码)避免用String,使用char[]
9.2 性能优化建议
-
避免在循环中使用
+
拼接字符串 -
预编译频繁使用的正则表达式
-
考虑重用StringBuilder实例(需权衡线程安全)
-
大文本处理使用流式API
-
合理使用intern()但不要过度
9.3 安全性建议
-
SQL查询使用PreparedStatement而非字符串拼接
-
处理HTML/XML时使用专用转义工具
-
日志输出前过滤敏感信息
-
比较敏感字符串(如密码)使用Arrays.equals(char[], char[])
-
注意Unicode欺骗攻击(如视觉相似字符)
十、String的未来发展
10.1 Valhalla项目的影响
Java Valhalla项目(值类型和内联类)可能带来的改变:
-
可能引入更高效不可变字符串实现
-
减少对象头开销
-
更好的内存局部性
10.2 字符串压缩技术
未来可能进一步优化字符串存储:
-
更智能的编码检测和转换
-
对特定领域(如DNA序列)的特殊优化
-
与硬件特性(如SIMD)结合的加速处理
10.3 多语言增强
改进对多语言文本的处理:
-
更好的emoji支持
-
更完善的正则表达式Unicode属性支持
-
更高效的国际化字符串处理
结语
String作为Java中最基础也最复杂的类之一,其设计理念和实现细节体现了Java语言的许多核心思想。深入理解String类不仅有助于编写高效、安全的Java代码,也是理解Java内存模型、不可变对象设计等高级主题的良好起点。随着Java语言的不断发展,String类也在持续优化和改进,但它的核心概念——不可变的Unicode字符序列——将始终保持不变。