杰瑞科技汇

Java字符串如何高效操作与优化?

目录

  1. String 的基本概念
  2. String 的不可变性
  3. String 的创建方式
  4. String 的常用方法
  5. StringStringBuilderStringBuffer 的区别
  6. String 的内存布局与 intern() 方法
  7. 最佳实践

String 的基本概念

在 Java 中,String (字符串) 是一个对象,而不是基本数据类型,它代表一个字符序列,"Hello, World!"

Java字符串如何高效操作与优化?-图1
(图片来源网络,侵删)

Java 语言为 String 提供了特殊的语法支持,可以直接用双引号 来创建字符串字面量,这在其他语言中是不常见的。

// 使用双引号创建字符串,这是最常见的方式
String str1 = "Hello";
String str2 = "World";
// 使用 new 关键字创建,不推荐(见下文)
String str3 = new String("Hello");

String 的不可变性

这是 String 最核心、最重要的特性。一旦一个 String 对象被创建,它的内容就不能被修改。

什么是不可变性?

  • 不能修改内容:你不能像修改数组那样,改变 String 对象中某个位置的字符。
  • 任何修改操作都是创建新对象:当你对一个 String 进行拼接、替换、截取等操作时,JVM 不会在原对象上进行修改,而是会创建一个新的 String 对象来存放结果,原对象保持不变。

为什么 String 要设计成不可变?

  1. 线程安全:由于 String 对象不可变,它在多个线程之间共享时是绝对安全的,不需要额外的同步机制来保证其一致性,这对于高并发应用至关重要。
  2. 哈希缓存String 对象的 hashCode() 在被创建时就会被计算并缓存,因为内容不会变,所以哈希值也永远不会变,这使得 String 非常适合用做 HashMapHashSet 等哈希表的键,性能更高。
  3. 字符串常量池String 的不可变性是实现字符串常量池的基础,由于内容不可变,所以多个字符串变量可以安全地引用同一个字符串字面量,从而节省内存。
  4. 安全性:在许多场景下,字符串被用于传递敏感信息(如密码、URL),如果字符串是可变的,那么它的内容可能在被传递的过程中被恶意修改,导致安全问题,不可变性保证了字符串内容的安全。

示例:验证不可变性

String s = "hello";
System.out.println("原始字符串: " + s); // 输出: hello
System.out.println("原始对象的内存地址: " + System.identityHashCode(s));
// concat() 方法看起来像是修改了字符串,但实际上它返回了一个新对象
s = s.concat(" world");
System.out.println("修改后的字符串: " + s); // 输出: hello world
System.out.println("新对象的内存地址: " + System.identityHashCode(s));

输出结果:

原始字符串: hello
原始对象的内存地址: 1163157884
修改后的字符串: hello world
新对象的内存地址: 1956725890

可以看到,两次打印的内存地址不同,证明 s 引用了一个全新的对象,而原来的 "hello" 对象并没有被改变。

Java字符串如何高效操作与优化?-图2
(图片来源网络,侵删)

String 的创建方式

Java 中创建 String 主要有两种方式,它们在内存中的行为有显著区别。

字符串字面量

String str1 = "hello";
String str2 = "hello";
  • 过程:当代码执行到 "hello" 时,JVM 会首先在字符串常量池 中查找是否存在值为 "hello" 的字符串。
  • 如果存在:直接将引用指向池中的对象。
  • 如果不存在:在常量池中创建 "hello" 对象,然后将引用指向它。
  • str1str2 指向的是同一个对象,可以用 来验证( 比较的是内存地址)。
System.out.println(str1 == str2); // 输出: true

new 关键字

String str3 = new String("hello");
String str4 = new String("hello");
  • 过程
    1. JVM 会在字符串常量池中检查 "hello" 是否存在,如果不存在,则先在常量池中创建一个。
    2. new 关键字会在堆内存 中创建一个新的 String 对象,并将常量池中的 "hello" 内容复制过来。
    3. 将引用 str3 指向堆中新创建的对象。
  • str3str4 指向的是堆中两个不同的对象,尽管它们的内容相同,它们都间接引用了常量池中的 "hello"
System.out.println(str3 == str4); // 输出: false
System.out.println(str3.equals(str4)); // 输出: true (equals() 比较的是内容)

使用字面量创建 String 可以复用常量池中的对象,更节省内存,除非有特殊需求(如需要一个独立的对象),否则强烈推荐使用字面量


String 的常用方法

String 类提供了非常丰富的方法来操作字符串。

方法类别 方法名 描述 示例
获取信息 length() 返回字符串长度 "abc".length() -> 3
charAt(int index) 返回指定索引处的字符 "abc".charAt(1) -> 'b'
substring(int beginIndex) 截取从 beginIndex 开始到结尾的子串 "abcdef".substring(2) -> "cdef"
substring(int beginIndex, int endIndex) 截取从 beginIndexendIndex-1 的子串 "abcdef".substring(2, 4) -> "cd"
indexOf(String str) 查找子串首次出现的索引,找不到返回-1 "hello world".indexOf("world") -> 6
lastIndexOf(String str) 查找子串最后一次出现的索引 "hello world".lastIndexOf("o") -> 7
转换 toLowerCase() 转换为小写 "Hello".toLowerCase() -> "hello"
toUpperCase() 转换为大写 "Hello".toUpperCase() -> "HELLO"
trim() 去除首尾空白字符 " hello ".trim() -> "hello"
split(String regex) 按正则表达式分割字符串,返回字符串数组 "a,b,c".split(",") -> ["a", "b", "c"]
replace(char old, char new) / replace(CharSequence old, CharSequence new) 替换所有匹配的字符或子串 "hello".replace('l', 'p') -> "heppo"
valueOf(...) 静态方法,将各种数据类型转换为字符串 String.valueOf(123) -> "123"
比较 equals(Object obj) 是否相等 "hello".equals("Hello") -> false
equalsIgnoreCase(String another) 忽略大小写比较内容 "hello".equalsIgnoreCase("Hello") -> true
compareTo(String another) 按字典序比较大小,返回差值 "a".compareTo("b") -> -1
contains(CharSequence s) 判断是否包含指定子串 "hello".contains("ell") -> true
startsWith(String prefix) / endsWith(String suffix) 判断是否以指定前缀/后缀开头/结尾 "hello".startsWith("he") -> true
其他 format(String format, Object... args) 格式化字符串 String.format("我叫%s,今年%d岁", "张三", 18) -> "我叫张三,今年18岁"

StringStringBuilderStringBuffer 的区别

当需要进行大量字符串拼接操作时,由于 String 的不可变性,频繁创建新对象会带来严重的性能问题,这时就需要使用 StringBuilderStringBuffer

Java字符串如何高效操作与优化?-图3
(图片来源网络,侵删)
特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 线程安全 非线程安全 线程安全
性能 差(频繁修改时) 较低(因为有同步开销)
使用场景 少量字符串定义,或作为不可变的键 单线程环境下的字符串拼接 多线程环境下的字符串拼接

何时使用?

  • 使用 String

    • 不需要改变。
    • 作为 Map 的键。
    • 少量的字符串操作。
  • 使用 StringBuilder

    • 在单线程环境中进行大量字符串拼接、修改、删除等操作,这是最常用的场景,性能最佳。
  • 使用 StringBuffer

    在多线程环境中进行字符串操作,需要保证线程安全,在 Servlet 环境中构建响应。

示例:性能对比

// String 性能差
String s = "";
for (int i = 0; i < 10000; i++) {
    s += "a"; // 每次循环都创建新对象
}
// StringBuilder 性能好
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append("a"); // 在原对象上操作,不创建新对象
}
String result = sb.toString();

String 的内存布局与 intern() 方法

字符串常量池

字符串常量池是 JVM 的一块特殊内存区域,用于存储字符串字面量,它的主要目的是减少内存占用提高性能

intern() 方法

intern() 方法是一个有趣且强大的方法,它可以将字符串放入常量池。

  • 对于字面量s.intern() 会返回常量池中该字符串的引用,如果常量池中没有,则先将字符串内容复制到常量池,再返回引用。
  • 对于 new 创建的字符串s.intern() 会检查常量池中是否存在与 s 内容相同的字符串。
    • 如果存在,则返回常量池中那个字符串的引用。
    • 如果不存在,则将该 s 的内容复制到常量池,并返回常量池中这个新字符串的引用。

示例:深入理解 intern()

// 情况一:字面量
String s1 = "hello";
String s2 = s1.intern(); // s1 已经在常量池,s2 == s1
System.out.println(s1 == s2); // true
// 情况二:new 创建的字符串
String s3 = new String("hello"); // s3 在堆中
String s4 = s3.intern(); // s3.intern() 会返回常量池中 "hello" 的引用
System.out.println(s3 == s4); // false,因为 s3 在堆,s4 指向常量池
// 情况三:常量池中没有的情况
String s5 = new String("world"); // "world" 不在常量池
String s6 = s5.intern(); // s5.intern() 会将 "world" 放入常量池,并返回其引用
String s7 = "world"; // 字面量,直接指向常量池中的 "world"
System.out.println(s5 == s6); // false
System.out.println(s6 == s7); // true

intern() 方法在处理大量重复且较长的字符串时,可以有效节省内存,但在现代 JVM 中,其性能影响需要谨慎评估。


最佳实践

  1. 优先使用字面量:除非有特殊需求,否则始终使用 String str = "hello"; 来创建字符串。
  2. 避免不必要的字符串拼接:在循环中,不要使用 或 来拼接字符串,这会产生大量临时对象,应使用 StringBuilder
  3. 使用 StringBuilder 处理大量修改:在需要频繁修改字符串的场景(如循环拼接、格式化),StringBuilder 是不二之选。
  4. equals() 比较内容, 比较地址:永远记住, 用于判断两个引用是否指向同一个对象,而 equals() 用于判断两个对象的内容是否相同,对于 String时一定要用 equals()
  5. 当心 null:调用 null 的任何方法都会抛出 NullPointerException,在不确定字符串是否为 null 时,使用 Objects.equals(str1, str2) 或进行 null 检查。

希望这份详细的讲解能帮助你全面掌握 Java String 字符串!

分享:
扫描分享到社交APP
上一篇
下一篇