深入理解 Java 中的 String 类

        在 Java 的世界里,String 类就像一位无处不在的 "老朋友"—— 从简单的 Hello World 到复杂的业务逻辑,几乎每个程序都离不开它。但你真的了解这位 "老朋友" 吗?为什么同样是字符串,直接赋值和 new 出来的对象会有区别?为什么频繁拼接字符串时推荐用 StringBuilder?今天,我们就来揭开 String 类的神秘面纱,用生动的例子带你吃透它的方方面面。

一、为什么需要 String 类?

        在 C 语言中,字符串是用字符数组或指针表示的,比如char[] str = "hello"。这种方式把数据(字符数组)和操作(字符串函数)分离开来,每次操作都要调用strlenstrcpy等函数,不仅麻烦,还容易出现内存越界等问题。

        Java 作为面向对象语言,将字符串的数据和操作封装成了 String 类,让字符串处理变得简单、安全。比如判断两个字符串是否相等,C 语言需要strcmp,而 Java 直接调用equals方法即可 —— 这就是面向对象的一点好处。

二、创建字符串:不止一种方式

String 类提供了多种构造方式

构造方法多种多样,常用的有以下几种,让我们用代码来直观表示:

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

        // 2. new String对象
        String s2 = new String("hello bit");
        System.out.println(s2);  // 输出:hello bit

        // 3. 用字符数组构造
        char[] chars = {'h','e','l','l','o',' ','b','i','t'};
        String s3 = new String(chars);
        System.out.println(s3);  // 输出:hello bit

        // 4. 用字节数组构造(字节对应ASCII码)
        byte[] bytes = {97, 98, 99};  // 'a','b','c'的ASCII码
        String s4 = new String(bytes);
        System.out.println(s4);  // 输出:abc
    }
}

        看起来这几种方式都能得到 "hello bit",但它们在内存中的存储方式大不相同 —— 这就要说到字符串常量池了。

三、字符串常量池:Java 的 "字符串仓库"

        你有没有想过:如果程序中多次用到 "hello",Java 会存储多个相同的字符串吗?答案是否定的,因为有字符串常量池(StringTable) 这个 "仓库"。

        那么什么是常量池呢?常量池是 JVM 中的一块特殊内存,本质是一个哈希表,用来存储字符串常量。它的核心作用是避免重复存储相同的字符串,提高内存利用率。        可以把它比作家里的 "冰箱":第一次买牛奶(字符串)会放进冰箱(常量池),下次再喝时直接从冰箱拿,不用再买新的 —— 这就是 "池化技术" 的思想(类似线程池、数据库连接池)。

不同 JDK 版本的常量池位置:

直接赋值   vs   new 对象:内存差异

看两个例子,你就能明白常量池的作用:

1.直接用常量赋值

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);  // 输出:true

在栈中str1和str2都指向常量池中的“abc”,常量池中存储着“abc”地址为(0x112233)

str1->0x112233, str2->0x112233,故结果返回true。s1 创建时,"abc" 被放入常量池;s2 创建时,JVM 先检查常量池,发现已有 "abc",直接让 s2 指向它 —— 所以 s1 和 s2 指向同一个对象,==判断为 true。

2.用new创建对象

String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2);  // 输出:false

        在栈中str1指向堆中对象A(地址0x998877),str2指向堆中对象B(地址0x556677),对象A和B的value属性都指向常量池中的"abc"(0x112233),在常量池存储"abc"(0x112233)

  new关键字会强制在堆中创建新对象,即使常量池中有 "abc",str1 和 str2 也是两个不同的堆对象,所以==判断为 false。

四、String 类的常用方法:从比较到操作

String 类提供了大量实用方法,掌握它们能让字符串处理事半功倍。

1. 字符串比较。

代码如下:

public class StringCompare {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");
        String s3 = new String("HELLO");
        String s4 = s1;

        // ==:比较地址
        System.out.println(s1 == s2);  // false(不同对象)
        System.out.println(s1 == s4);  // true(同一对象)

        // equals:比较内容
        System.out.println(s1.equals(s2));  // true(内容相同)
        System.out.println(s1.equals(s3));  // false(大小写不同)

        // compareTo:返回字符差值或长度差
        System.out.println("abc".compareTo("abd"));  // -1('c'比'd'小1)
        System.out.println("abc".compareTo("ab"));    // 1(前者长1)

        // compareToIgnoreCase:忽略大小写
        System.out.println("abc".compareToIgnoreCase(s3));  // 0(内容相同)
    }
}

判断字符串内容是否相等时,永远用equals而不是 ==

2. 字符串查找:定位字符或子串

当你需要在字符串中找某个字符或子串时,这些方法很有用:

代码示例:

public class StringSearch {
    public static void main(String[] args) {
        String s = "aaabbbcccaaabbbccc";

        // 获取索引3的字符(注意:索引从0开始)
        System.out.println(s.charAt(3));  // 输出:b

        // 找'c'第一次出现的位置
        System.out.println(s.indexOf('c'));  // 输出:6

        // 从索引10开始找'c'
        System.out.println(s.indexOf('c', 10));  // 输出:15

        // 找子串"bbb"最后一次出现的位置
        System.out.println(s.lastIndexOf("bbb"));  // 输出:12
    }
}

3. 字符串转换:灵活处理数据格式

字符串和其他类型的转换是高频操作,比如用户输入的数字是字符串,需要转成 int 才能计算。

3.1 数值 ↔ 字符串
public class StringConvert {
    public static void main(String[] args) {
        // 数字转字符串
        String s1 = String.valueOf(123);    // "123"
        String s2 = String.valueOf(12.34);  // "12.34"
        String s3 = String.valueOf(true);   // "true"

        // 字符串转数字(注意:字符串必须是纯数字,否则抛异常)
        int num1 = Integer.parseInt("123");     // 123
        double num2 = Double.parseDouble("12.34");  // 12.34
    }
}
3.2 大小写转换
String s = "Hello World";
System.out.println(s.toUpperCase());  // 输出:HELLO WORLD(转大写)
System.out.println(s.toLowerCase());  // 输出:hello world(转小写)
3.3 字符串 ↔ 字符数组
// 字符串转字符数组
String s = "hello";
char[] chars = s.toCharArray();  // {'h','e','l','l','o'}

// 字符数组转字符串
String s2 = new String(chars);  // "hello"

4. 字符串替换、拆分与截取

这些方法能帮你快速处理字符串的结构:

4.1 替换
String str = "helloworld";
// 替换所有'l'为'_'
System.out.println(str.replaceAll("l", "_"));  // he__owor_d

// 只替换第一个'l'
System.out.println(str.replaceFirst("l", "_"));  // he_loworld
4.2 拆分(按分隔符分割)
// 按空格拆分字符串
String str = "hello world hello bit";
String[] parts = str.split(" ");
for (String part : parts) {
    System.out.println(part);  // 依次输出hello、world、hello、bit
}

// 拆分IP地址(注意:.需要转义为\\.)
String ip = "192.168.1.1";
String[] ipParts = ip.split("\\.");  // ["192","168","1","1"]

注意:拆分特殊字符(如*+\)时需要转义,比如split("\\*")

4.3 截取子串
String str = "helloworld";
// 从索引5截取到结尾
System.out.println(str.substring(5));  // world

// 截取[0,5)区间(包含0,不包含5)
System.out.println(str.substring(0, 5));  // hello

注意:

1.索引是从0开始的。

2.截取遵循前闭后开的原则,即第一位包含,第二位数字不包含。

在Java中还有更多的String方法,能够更好的满足你的不同需求,有待你的发现。

五、String 的不可变性:为什么字符串不能被修改?

        你可能会疑惑:明明可以写s = s + "world",为什么说 String 是不可变的?

1.什么是不可变性?

        String 的不可变性是指:字符串一旦创建,其内容(字符序列)就不能被修改。任何看似修改的操作(如拼接、替换),实际上都会创建新的 String 对象。

2.为什么不可变?

看 String 类的源码就知道了:

关键原因有两点:

  1. 存储字符的value数组被private final修饰,外部无法直接修改,且 String 类没有提供修改数组的方法(如 set 方法)。
  2. 所有修改操作(如toUpperCaseconcat)都会创建新的 String 对象,原对象内容不变。

3.直观感受:字符串拼接的本质

String s = "hello";
s = s + " world";  // 看似修改,实则创建新对象
  1. 初始s指向常量池中的 "hello"。
  2. 执行s + " world"时,JVM 创建新对象 "hello world"。
  3. s转而指向新对象,原 "hello" 仍在内存中(等待垃圾回收)。

六、StringBuffer 与 StringBuilder:可变字符串的救星

        由于 String 的不可变性,频繁修改字符串(如循环拼接)会创建大量临时对象,严重影响效率。这时就需要StringBufferStringBuilder

两者有以下主要区别:

建议:单线程场景用 StringBuilder(效率高),多线程场景用 StringBuffer(安全)。

常用方法(以 StringBuilder 为例)

public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("hello");

        // 追加内容(尾插)
        sb.append(" ");    // "hello "
        sb.append("world");// "hello world"
        sb.append(123);    // "hello world123"

        // 修改指定位置字符
        sb.setCharAt(0, 'H');  // "Hello world123"

        // 插入内容
        sb.insert(5, "!!!");   // "Hello!!! world123"

        // 逆转字符串
        sb.reverse();  // "321dlrow !!!olleH"

        // 转成String
        String result = sb.toString();
        System.out.println(result);
    }
}

效率对比:String vs StringBuilder

用循环拼接 10000 次数字,看看差距:

public class EfficiencyTest {
    public static void main(String[] args) {
        // String拼接(效率低)
        long start = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("String耗时:" + (end - start) + "ms");  // 约几百ms

        // StringBuilder拼接(效率高)
        start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuilder耗时:" + (end - start) + "ms");  // 约1ms
    }
}

结论:频繁修改字符串时,坚决不用 String,改用 StringBuilder!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值