Java-day06

引入

字符串是 Java 开发中最常用的数据类型之一,但其背后的常量池机制、引用比较(==)与内容比较(equals)的区别、不可变性等知识点却容易被误解。本文将从字符串的底层存储、比较逻辑、不可变特性以及常用操作方法四个维度展开,结合代码示例与内存模型分析,帮助读者彻底厘清 Java 字符串的核心概念,为编写高效、无 Bug 的字符串操作代码奠定基础。

一、Java 字符串的底层存储:常量池与堆内存

Java 字符串的存储分为**字符串常量池**和**堆内存**两种场景,理解这一机制是掌握字符串引用比较(`==`)的关键。

1.1 字符串常量池:共享的字面量存储区

字符串常量池是 JVM 为了**减少字符串对象重复创建**而设计的内存区域,它独立于堆和栈,存储着所有以字面量形式创建的字符串(如 `String s = "123"`)。

**核心规则**:

- 当创建字符串字面量时,JVM 会先在常量池中查找该字符串是否已存在;

- 若存在,直接返回常量池中该字符串的引用;

- 若不存在,在常量池中创建该字符串对象,再返回引用。

**代码示例与内存分析**:

```java

public class StringPoolDemo {

    public static void main(String[] args) {

        // s 和 s1 都引用常量池中的 "123"

        String s = "123";

        String s1 = "123";

        System.out.println(s == s1); // 输出:true(地址相同)

    }

}

```

- 内存模型:`s` 和 `s1` 同时指向字符串常量池中的同一个 `"123"` 对象,因此 `==` 比较返回 `true`。

1.2 堆内存:new 关键字创建的新对象

使用 `new String("xxx")` 创建字符串时,JVM 会在**堆内存**中创建一个新的字符串对象,该对象与常量池中的字面量是相互独立的。

**代码示例与内存分析**:

```java

public class StringHeapDemo {

    public static void main(String[] args) {

        String s = "123"; // 常量池中的 "123"

        String s2 = new String("123"); // 堆中的新对象

        System.out.println(s == s2); // 输出:false(地址不同)

    }

}

```

- 内存模型:`s` 指向常量池的 `"123"`,`s2` 指向堆中新建的 `"123"` 对象,两者地址不同,因此 `==` 比较返回 `false`。

1.3 `new String("123")` 到底创建了几块内存?

当执行 `String s3 = new String("123")` 时,内存创建分为两步:

1. **第一步**:字符串字面量 `"123"` 会先在**常量池**中创建(若之前不存在);

2. **第二步**:`new String(...)` 会在**堆内存**中再创建一个新的字符串对象,该对象的内容与常量池中的 `"123"` 相同,但属于堆中的独立对象。

因此,这行代码总共在**常量池和堆**中各创建了一块内存空间,合计**两块**。

二、== 与 equals:引用比较与内容比较的本质区别

在 Java 中,`==` 和 `equals` 是两种完全不同的比较逻辑,尤其在字符串中容易混淆,需从“基本数据类型”和“引用数据类型”两个维度区分。

2.1 ==:基本数据类型比“值”,引用数据类型比“地址”

- **基本数据类型(int、float、char 等)**:`==` 比较的是值是否相同;

- **引用数据类型(String、Object 等)**:`==` 比较的是对象的**引用地址**是否相同,与内容无关。

**代码示例**:

```java

public class EqualsVsDoubleEqual {

    public static void main(String[] args) {

        // 基本数据类型比较(值相同)

        int a = 10;

        int b = 10;

        System.out.println(a == b); // 输出:true(值相同)

       

        // 引用数据类型比较(地址相同)

        String s = "123";

        String s1 = "123";

        System.out.println(s == s1); // 输出:true(地址相同,都指向常量池的"123")

       

        // 引用数据类型比较(地址不同)

        String s2 = new String("123");

        System.out.println(s == s2); // 输出:false(s 指向常量池,s2 指向堆)

    }

}

```

2.2 equals:默认比“地址”,String 重写后比“内容”

- `equals` 是 `Object` 类的方法,**默认实现是用 `==` 比较对象地址**;

- `String` 类**重写了 `equals` 方法**,逻辑是:先判断地址是否相同,若相同直接返回 `true`;若不同,再逐字符比较内容是否相同。

**String 类 equals 方法源码解析**:

```java

public boolean equals(Object anObject) {

    if (this == anObject) {

        return true; // 地址相同,直接返回true

    }

    if (anObject instanceof String) {

        String anotherString = (String)anObject;

        int n = value.length;

        if (n == anotherString.value.length) {

            char v1[] = value;

            char v2[] = anotherString.value;

            int i = 0;

            while (n-- != 0) { // 逐字符比较内容

                if (v1[i] != v2[i])

                    return false;

                i++;

            }

            return true;

        }

    }

    return false;

}

```

**代码示例**:

```java

public class StringEqualsDemo {

    public static void main(String[] args) {

        String s = "123";

        String s1 = "123";

        String s2 = new String("123");

        String s3 = new String("123");

       

        System.out.println(s.equals(s1)); // 输出:true(内容相同)

        System.out.println(s.equals(s2)); // 输出:true(内容相同)

        System.out.println(s2.equals(s3)); // 输出:true(内容相同)

        System.out.println(s == s2); // 输出:false(地址不同)

    }

}

```

三、Java 字符串的不可变性:为什么 String 不可变?

Java 字符串是**不可变的**,即一旦创建,其内容就无法修改。这一特性是由字符串的底层存储和设计哲学决定的。

3.1 不可变性的底层实现

String 类的核心存储是一个 `final char[] value` 数组(JDK 9 后改为 `byte[]`,但不可变性逻辑一致):

- `final` 修饰的数组引用无法被重新赋值;

- 数组本身的元素也没有对外暴露修改的方法,因此字符串内容一旦创建就无法改变。

**代码示例与原理**:

```java

public class StringImmutableDemo {

    public static void main(String[] args) {

        String s = "Hello";

        // 看似修改了字符串,实则创建了新的字符串对象

        s = s + " World";

        System.out.println(s); // 输出:Hello World

    }

}

```

- 原理:`s + " World"` 会创建新的字符串对象 `"Hello World"`,并将 `s` 的引用指向该新对象,原 `"Hello"` 对象仍存在于常量池或堆中(后续被 GC 回收)。

3.2 不可变性的优势

- **线程安全**:不可变对象天生线程安全,多线程环境下无需加锁;

- **常量池优化**:可在常量池中共享相同内容的字符串,减少内存消耗;

- **哈希值稳定**:字符串的哈希值在创建时确定,可安全地作为 HashMap 的键。

3.3 字符串拼接的优化:StringBuilder 与 append 方法

由于 String 不可变,直接使用 `+` 拼接字符串会频繁创建新对象,性能低下。Java 推荐使用 `StringBuilder`(单线程)或 `StringBuffer`(多线程)的 `append` 方法优化拼接。

`append` 方法的核心是**在原有数组基础上扩容**(新建数组,复制原内容后追加新内容),而非每次创建新对象,因此性能远高于直接拼接。

**代码示例与字节码分析**:

```java

public class StringConcatDemo {

    public static void main(String[] args) {

        String a = "123";

        String b = "456";

        String c = a + b; // 底层调用StringBuilder的append方法

        String d = "123456";

        System.out.println(c == d); // 输出:false(c 指向堆中拼接的对象,d 指向常量池)

    }

}

```

- 字节码分析:`a + b` 会被编译为 `new StringBuilder().append(a).append(b).toString()`,`toString()` 方法会在堆中创建新的 String 对象,因此 `c` 与常量池中的 `d` 地址不同。

四、Java 字符串的常用操作方法

Java 提供了丰富的字符串操作方法,以下是最常用的几个场景及示例。

4.1 基本属性与查询方法

- `length()`:获取字符串长度;

- `charAt(int index)`:获取指定下标处的字符;

- `indexOf(String str)`:查找子串第一次出现的下标;

- `lastIndexOf(String str)`:查找子串最后一次出现的下标;

- `contains(CharSequence s)`:判断是否包含子串。

**代码示例**:

```java

public class StringBasicMethods {

    public static void main(String[] args) {

        String aString = " Hello World ";

        System.out.println(aString.length()); // 输出:13(包含两端空格)

        System.out.println(aString.charAt(1)); // 输出:'H'(下标从0开始)

        System.out.println(aString.indexOf("l")); // 输出:3

        System.out.println(aString.lastIndexOf("l")); // 输出:9

        System.out.println(aString.contains("ll")); // 输出:true

    }

}

```

4.2 内容修改与转换方法

- `substring(int beginIndex)`:从指定下标开始截取子串;

- `substring(int beginIndex, int endIndex)`:截取 [beginIndex, endIndex) 区间的子串;

- `toLowerCase()`:转换为小写;

- `toUpperCase()`:转换为大写;

- `trim()`:去除两端空格;

- `replace(CharSequence old, CharSequence new)`:替换子串;

- `concat(String str)`:拼接字符串(性能低于 StringBuilder)。

**代码示例**:

```java

public class StringModifyMethods {

    public static void main(String[] args) {

        String aString = " Hello World ";

        System.out.println(aString.substring(3)); // 输出:"lo World "

        System.out.println(aString.substring(3, 6)); // 输出:"lo "

        System.out.println(aString.toLowerCase()); // 输出:" hello world "

        System.out.println(aString.toUpperCase()); // 输出:" HELLO WORLD "

        System.out.println(aString.trim()); // 输出:"Hello World"

        System.out.println(aString.replace("l", "L")); // 输出:" HeLLo WorLd "

        System.out.println(aString.concat("Java")); // 输出:" Hello World Java"

    }

}

```

4.3 比较方法

- `equals(Object obj)`:比较内容是否相同(区分大小写);

- `equalsIgnoreCase(String anotherString)`:比较内容是否相同(不区分大小写);

- `compareTo(String anotherString)`:按字典序比较,返回差值(0 表示相等,正数表示当前字符串大,负数表示参数字符串大)。

**代码示例**:

```java

public class StringCompareMethods {

    public static void main(String[] args) {

        String s = "Hello";

        String s1 = "hello";

        System.out.println(s.equals(s1)); // 输出:false(区分大小写)

        System.out.println(s.equalsIgnoreCase(s1)); // 输出:true(不区分大小写)

        System.out.println(s.compareTo(s1)); // 输出:-32('H' 与 'h' 的 ASCII 差值)

    }

}

```

五、总结:字符串操作的最佳实践

1. **优先使用字面量创建字符串**:`String s = "123"` 可利用常量池优化,减少对象创建;

2. **避免频繁使用 `+` 拼接字符串**:循环或高频拼接场景下,使用 `StringBuilder.append()` 提升性能;

3. **明确 `==` 与 `equals` 的区别**:比较内容用 `equals`,比较地址用 `==`;

4. **利用不可变性优势**:在多线程环境下直接使用字符串,无需加锁;

5. **熟悉常用操作方法**:根据场景选择 `substring`、`replace`、`trim` 等方法,避免重复造轮子。

通过本文的学习,相信你已对 Java 字符串的常量池机制、`==` 与 `equals` 的区别、不可变性以及常用操作有了全面理解。在实际开发中,合理运用这些知识可让字符串操作更高效、更安全,避免因底层机制误解导致的 Bug。建议在日常编码中多思考字符串的内存模型,逐步培养对字符串操作的敏感度,为解决复杂业务场景中的字符串问题积累经验。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐