前言:

        在C语言中,处理字符串往往需要借助字符数组或指针,再配合标准库函数(strcpy,strlen)。这种方法将数据与操作方法分离开来,不太符合面向对象的思想。而字符串再日常开发中无处不在,因此java专门设计了String类,将字符串相关的数据和操作封装在一起。

        本文将带你全面了解String的内部原理,常用方法,不可变性,常量池机制,以及它的“好兄弟”StringBuilder和StringBuffer。

目录

前言:

一.String的构造方法

二.String的内存布局与常量池

2.1String内部结构(基于JDK17)

2.2字符串常量池

2.3两种赋值方法的区别

三.字符串的比较

3.1 ==:比较引用地址

3.2equals():比较内容(字典序)

3.3compareTo():按字典序返回差值

四.常用方法速览

4.1拆分注意事项

4.2intern()方法示例

五.字符串的不可变性

5.1为什么String是不可变的?

5.2不可变的好处

5.3误区提醒

六.字符串拼接的性能陷阱

七.StringBilder与StringBuffer

7.1常用方法

7.2String与StringBuilder互转

八.总结


一.String的构造方法

        String提供了多种构造方法,最常用的三种:

public class StringDemo {
    public static void main(String[] args) {
        // 1. 直接赋值(字符串常量)
        String s1 = "hello bit";
        System.out.println(s1);

        // 2. 通过 new 关键字创建对象
        String s2 = new String("hello bit");
        System.out.println(s2);

        // 3. 使用字符数组构造
        char[] array = {'h', 'e', 'l', 'l', 'o'};
        String s3 = new String(array);
        System.out.println(s3);

        // 4. 使用字节数组构造(可将 ASCII 码转为字符串)
        byte[] bytes = {97, 98, 99, 100};
        String s4 = new String(bytes);
        System.out.println(s4); // 输出 "abcd"
    }
}

注意:直接赋值与new创建的对象在内存布局上有很大的区别,下文会详细分析。

二.String的内存布局与常量池

2.1String内部结构(基于JDK17)

        打开String源码,你会看到:

public final class String
    implements Serializable, Comparable<String>, CharSequence {

    @Stable
    private final byte[] value;   // 存储字符串的字节数组

    private final byte coder;     // 编码标识:LATIN1 或 UTF16

    private int hash;             // 缓存哈希值,默认 0
    // ...
}

value是一个final的字节数组,真正存储字符数据。

coder 表示编码方式,JDK9+引入了紧凑字符串特性,当字符串只包含Latin-1字符时用1字节,否则用2字节(UTF-16),有效节省内存

hash用于缓存哈希码,避免重复计算。

2.2字符串常量池

        字符串常量池是JVM中特殊的存储区域,本质上是一个固定大小的哈希表。它的作用是避免重复创建相同内容的字符串对象,从而节省内存。

不同JDK版本中常量池的位置和大小有所差异:

JDK版本 常量池位置 大小特性
Java6 方法区(永久代) 固定大小(1009)
Java7 堆中 可设置,默认(60013)
Java8 堆中

可设置,有范围(最小1009)

2.3两种赋值方法的区别

        示例1:直接赋值

String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2); // true

“abc”首先被放入字符串常量池。

str2创建时发现常量池中已经存在“abc”,直接复用同一个引用。

因此str1和str2指向常量池中的同一个对象。

        示例2:

String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2); // false

执行new String(“abc”)时,如果常量池中没有”abc“,会先在常量池中创建一个。

然后再在堆上创建一个新的String对象,该对象的value数组指向常量池中的”abc“数据(实际上会复制一份)。

每次new都会产生一个独立的堆对象,所以str1和str2不相等。

三.字符串的比较

        字符串比较是开发中的高频操作,java提供了多种比较方式。

3.1 ==:比较引用地址

        基本类型:比较值是否相等。

        引用类型:比较两个引用是否指向同一个对象。

int a = 10, b = 20, c = 10;
System.out.println(a == b); // false
System.out.println(a == c); // true

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(不同对象)

3.2equals():比较内容(字典序)

        String重写了Object的equals()方法,逐个比较字符:

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // true

3.3compareTo():按字典序返回差值

        若两字符串不同,返回第一个不相等字符的ASCII差值

        若前k个字符都相等,则返回长度差。

String s1 = "abc";
String s2 = "ac";
System.out.println(s1.compareTo(s2)); // -1('b' - 'c' = -1)

String s3 = "abcdef";
System.out.println(s1.compareTo(s3)); // -3(长度差)

四.常用方法速览

方法 说明
charAt(int index) 获取指定位置字符
indexOf(int ch/String str) 返回第一次出现的位置
lastIndexOf() 从后往前查找
toUpperCase()/toLowerCase() 大小写转换
toCharArray() 转为字符数组
replaceAll(String regex ,String replacement) 全部替换
split(String regex) 按分隔符拆分
substring(int begin,int end) 截取子串(前闭后开)
trim() 去除首尾空格
intern() 手动将字符串放入常量池

4.1拆分注意事项

        特殊字符(如 . |  +)需要转义,例如split("\\.")才能按点号拆分。

        多个分割符可以用|组合,如split(",|;")。

4.2intern()方法示例

char[] ch = {'a', 'b', 'c'};
String s1 = new String(ch);     // 堆中对象,常量池中无 "abc"
s1.intern();                    // 将 s1 的引用放入常量池
String s2 = "abc";              // 直接复用常量池中的引用
System.out.println(s1 == s2);   // true(未注释 intern 前为 false)

五.字符串的不可变性

5.1为什么String是不可变的?

        value数组被private final修饰:虽然final保证了数组引用不可变,但数组内部元素其实是可变的,真正关键的是:

        String类没有提供任何修改value数组的公共方法。所有看似修改的操作(replace ,toUpperCase)都会创建一个新的String对象,原对象不变。

源码片段:

public final class String {
    private final byte[] value;   // 不可变的关键
    // 没有 setter 方法修改 value 内容
}

5.2不可变的好处

        线程安全:不可变对象天然可以在多线程环境下共享。

        常量池复用:因为内容不变,才能放心地让多个引用指向同一个字符串。

        哈希值可缓存:hash字段只需计算一次,适合作为HashMap的键。

5.3误区提醒

        下面这段代码并没有修改原字符串,而是创建了新的对象:

String s = "hello";
s = s + " world";   // 产生新的 String 对象

六.字符串拼接的性能陷阱

        如果使用+在循环中拼接字符串,会产生大量临时对象,效率极低。

long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 10000; i++) {
    s += i;        // 每次循环都会 new 一个 String 对象
}
long end = System.currentTimeMillis();
System.out.println("String 耗时:" + (end - start));

String方式可能耗时几十毫秒,而StringBilder几乎为0

七.StringBilder与StringBuffer

        为了高效修改字符串,java提供了两个可变字符串类:

线程安全 性能 使用场景
StringBuilder 不安全

单线程下频繁

拼接/修改

StringBuffer 安全(synchronized) 稍低 多线程下操作字符串

7.1常用方法

        append():追加内容(类似+操作)

        insert(int offset, String str):在指定位置插入

        delete(int start ,int end):删除区间字符

        reverse():反转字符串

        toString():转换不可变的String。

StringBuilder sb = new StringBuilder("hello");
sb.append(" world");
sb.insert(5, ",");
System.out.println(sb); // hello, world
String result = sb.toString();

7.2String与StringBuilder互转

        String->StringBuilder:通过构造器或append()。

        StringBuilder->String:调用toString()。

八.总结

特性/类 String StringBuilder StringBuffer
是否可变
线程安全
性能(修改时)
适用场景 不可变字符串,常量 单线程频繁修改 多线程频繁修改

更多推荐