杰瑞科技汇

Java字符串怎么用?

目录

  1. 什么是 String
  2. String 的不可变性
  3. String 的创建方式
    • 字符串字面量
    • new 关键字
  4. String 常用方法
  5. StringStringBuilderStringBuffer 的区别
  6. String 的内存布局(常量池)
  7. 最佳实践

什么是 String

在 Java 中,String 表示一个字符串,它不是一个基本数据类型(如 int, char),而是一个对象,这个对象内部使用一个 finalchar 数组来存储字符序列。

Java字符串怎么用?-图1
(图片来源网络,侵删)
// String 类的内部结构简化版
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[]; // 核心:一个不可变的字符数组
    // ... 其他成员变量和方法
}

从上面的定义可以看出 String 有两个重要特性:

  • finalString 类不能被继承。
  • value 数组是 final 的:这个数组一旦被创建,其引用就不能再指向其他数组。

String 的不可变性

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

为什么需要不可变性?

  1. 线程安全不可变,String 对象在多线程环境下是安全的,无需额外的同步机制。
  2. 安全性:在 Java 中,String 被广泛用于加载类、文件路径、网络地址等,如果字符串内容可以被修改,可能会带来严重的安全风险(修改了加载类的路径)。
  3. 性能优化:由于不可变,Java 虚拟机可以对 String 进行优化,比如字符串常量池

如何“修改”字符串?

Java字符串怎么用?-图2
(图片来源网络,侵删)

当你看起来在修改一个字符串时,JVM 会创建一个新的 String 对象,原来的对象保持不变。

String s1 = "hello";
String s2 = s1; // s2 和 s1 指向同一个对象
// s1.concat(" world") 并没有改变 s1 指向的对象
// 它返回了一个新的 "hello world" 字符串对象
s1 = s1.concat(" world"); 
System.out.println(s1); // 输出: hello world
System.out.println(s2); // 输出: hello (s2 仍然指向原来的 "hello" 对象)

String 的创建方式

Java 中创建字符串主要有两种方式,它们的内存行为完全不同。

字符串字面量

String s1 = "Hello";
String s2 = "Hello";

这种方式创建的字符串,JVM 会将其放入一个叫做字符串常量池 的全局内存区域中。

  • 当执行 String s1 = "Hello"; 时,JVM 会在常量池中查找是否存在 "Hello"。
  • 如果不存在,就创建一个 "Hello" 对象并存入常量池,然后将 s1 引用指向它。
  • 如果存在,s1 就直接指向这个已存在的对象。
  • s1 == s2 的结果是 true,因为它们指向同一个对象。
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // 输出: true (比较的是引用地址)
System.out.println(s1.equals(s2)); // 输出: true (比较的是内容)

new 关键字

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

这种方式创建的字符串,总是在堆内存 中创建一个新的对象,而不会检查常量池。

Java字符串怎么用?-图3
(图片来源网络,侵删)
  • new String("Hello") 的过程是:
    1. "Hello" 会被作为字面量放入常量池(如果还没有的话)。
    2. new 关键字会在堆内存中创建一个新的 String 对象,并将常量池中的 "Hello" 内容复制到这个新对象中。
    3. s3 引用指向这个堆内存中的新对象。

s3 == s4 的结果是 false,因为它们是堆中两个不同的对象。

String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // 输出: false (比较的是引用地址)
System.out.println(s3.equals(s4)); // 输出: true (比较的是内容)

String 常用方法

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

方法 描述 示例
length() 返回字符串的长度。 "abc".length() -> 3
charAt(int index) 返回指定索引处的字符。 "abc".charAt(1) -> 'b'
substring(int beginIndex) 返回从 beginIndex 开始到末尾的子字符串。 "abcde".substring(2) -> "cde"
substring(int begin, int end) 返回从 beginend-1 的子字符串。 "abcde".substring(1, 3) -> "bc"
indexOf(String str) 返回子字符串第一次出现的索引,找不到返回 -1。 "hello world".indexOf("world") -> 6
lastIndexOf(String str) 返回子字符串最后一次出现的索引。 "hello world".lastIndexOf("o") -> 7
toUpperCase() / toLowerCase() 将字符串转换为大写/小写。 "Hello".toLowerCase() -> "hello"
trim() 去除字符串首尾的空白字符。 " hello ".trim() -> "hello"
replace(char old, char new) 替换所有 old 字符为 new 字符。 "hello".replace('l', 'p') -> "heppo"
split(String regex) 根据正则表达式分割字符串,返回字符串数组。 "a,b,c".split(",") -> ["a", "b", "c"]
equals(Object obj) 比较两个字符串的内容是否相等(区分大小写)。 "abc".equals("ABC") -> false
equalsIgnoreCase(String another) 比较两个字符串内容是否相等(不区分大小写)。 "abc".equalsIgnoreCase("ABC") -> true
compareTo(String another) 按字典顺序比较两个字符串。 "abc".compareTo("abd") -> -1 (因为 'c' < 'd')

StringStringBuilderStringBuffer 的区别

当需要进行大量的字符串拼接、修改操作时,由于 String 的不可变性,每次操作都会创建新对象,导致性能低下,这时应该使用 StringBuilderStringBuffer

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 线程安全(因为不可变) 非线程安全 线程安全
性能 拼接等操作性能差 性能最高 性能比 StringBuilder 稍低
使用场景 少量、固定的字符串 单线程环境下的字符串操作 多线程环境下的字符串操作

示例:性能对比

// String 拼接 (性能差)
String s = "";
for (int i = 0; i < 10000; i++) {
    s = s + "a"; // 每次循环都创建新对象
}
// StringBuilder 拼接 (性能好)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append("a"); // 在原有对象上修改,效率高
}
String result = sb.toString();

String 的内存布局(常量池)

字符串常量池是 JVM 的一块特殊内存区域,用于存储所有字符串字面量,它的主要目的是复用字符串对象,减少内存开销。

JDK 7 的重要变化: 在 JDK 7 之前,字符串常量池位于永久代,从 JDK 7 开始,它被移到了堆内存中,这意味着字符串常量池中的对象可以被垃圾回收器回收。

示例:intern() 方法 intern() 方法是一个手动将字符串对象“入池”的方法。

String s1 = new String("Hello"); // "Hello" 在常量池,s1 在堆
String s2 = s1.intern(); // 将 s1 的内容 "Hello" 放入常量池,如果池中已有,则直接返回池中的引用
String s3 = "Hello"; // s3 直接从常量池获取
System.out.println(s1 == s2); // false, s1是堆对象,s2是常量池对象
System.out.println(s2 == s3); // true, s2和s3都指向常量池中的 "Hello"

最佳实践

  1. 优先使用字面量:对于已经确定且不会改变的字符串,直接使用字面量(String s = "hello";),让 JVM 自动管理常量池。
  2. 避免不必要的 拼接:在循环或频繁修改字符串的场景中,绝对不要使用 或 来拼接字符串,这会导致大量临时对象的创建,造成性能问题,务必使用 StringBuilderStringBuffer
  3. 使用 equals() 而不是 比较内容: 比较的是两个对象的内存地址,而 equals() 比较的是对象的内容,绝大多数情况下,我们关心的是内容是否相同。
  4. 明确线程需求:如果确定代码只在单线程中运行,优先使用性能更高的 StringBuilder,如果涉及多线程共享和修改,则必须使用 StringBuffer

希望这份详细的讲解能帮助你全面理解 Java 中的 String

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