【Day12】String 类详解:不可变性、常用方法与字符串拼接优化

哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十二篇内容~ 前面十一天我们搭建了 Java 基础和面向对象的核心体系,今天要攻克 Java 中最常用的类 ------String(字符串类)。字符串是开发中无处不在的元素,而String的不可变性、常用方法、拼接优化更是面试和实战的高频考点。本文会从String的底层原理、不可变性,到常用方法、拼接优化、常量池,结合实战案例帮你彻底掌握String类!

一、String 类的核心本质:不可变的字符序列

1. 什么是 String 的不可变性?

String对象一旦创建,其内部的字符序列(内容)就无法被修改 。看似能 "修改" 字符串的操作(如substringreplace),本质都是创建了新的 String 对象,原对象内容始终不变。

2. 底层实现(Java 9+)

Java 9 之前,String底层用char[]存储字符;Java 9 之后优化为byte[](节省内存,因为大部分字符串是 ASCII 字符,占 1 字节而非 2 字节),核心源码简化如下:

java

运行

java 复制代码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 存储字符的数组(final修饰,引用不可变)
    private final byte[] value;
    // 编码标识(LATIN1/UTF16)
    private final byte coder;

    // 构造方法:初始化value数组
    public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
    }
}

3. 不可变性的核心原因

从源码能看出,String的不可变性由 3 个关键点保证:

  1. 类用 final 修饰String不能被继承,避免子类修改其核心逻辑;
  2. 存储字符的数组用 final 修饰value数组的引用不可变(不能指向新数组);
  3. 无 setter 方法value数组是私有且无修改方法,外部无法直接操作数组内容。

实战 1:验证 String 的不可变性

java

运行

java 复制代码
public class StringImmutableDemo {
    public static void main(String[] args) {
        // 1. 创建String对象s1,指向常量池中的"Java"
        String s1 = "Java";
        // 2. 看似修改,实际创建新对象,s1仍指向原"Java"
        String s2 = s1.concat("学习"); // 新对象"Java学习"
        String s3 = s1.replace("J", "j"); // 新对象"java"

        // 输出原对象内容,仍为"Java"
        System.out.println(s1); // Java
        System.out.println(s2); // Java学习
        System.out.println(s3); // java

        // 3. 引用可改,但对象内容不可改
        s1 = "Python"; // s1指向新对象"Python",原"Java"对象仍存在
        System.out.println(s1); // Python
    }
}

4. 不可变性的优势

  • 线程安全:多线程环境下,无需同步即可安全访问字符串;
  • 可缓存:字符串常量池(String Pool)的基础(下文详解);
  • 安全性高:作为参数传递时(如数据库用户名 / 密码),内容不会被篡改。

二、String 的常用方法(实战高频)

String提供了大量操作字符串的方法,以下是开发中最常用的核心方法,按场景分类讲解:

1. 字符串创建与比较

方法 作用 示例
String(String original) 构造方法创建 new String("Java")
equals(Object obj) 比较字符串内容(区分大小写) "abc".equals("ABC") → false
equalsIgnoreCase(String str) 比较内容(不区分大小写) "abc".equalsIgnoreCase("ABC") → true
== 比较对象引用(是否指向同一内存地址) "abc" == new String("abc") → false
compareTo(String str) 按字典序比较(返回 int,0 = 相等,<0 = 小于,>0 = 大于) "a".compareTo("b") → -1

实战 2:字符串比较核心坑点

java

运行

java 复制代码
public class StringCompareDemo {
    public static void main(String[] args) {
        // 1. 常量池:直接赋值的字符串指向常量池同一对象
        String s1 = "Java";
        String s2 = "Java";
        System.out.println(s1 == s2); // true(引用相同)
        System.out.println(s1.equals(s2)); // true(内容相同)

        // 2. new创建:每次new都生成新对象(堆内存)
        String s3 = new String("Java");
        System.out.println(s1 == s3); // false(引用不同)
        System.out.println(s1.equals(s3)); // true(内容相同)

        // 3. 空字符串比较:避免空指针,用常量.equals(变量)
        String s4 = null;
        // System.out.println(s4.equals("Java")); // 空指针异常
        System.out.println("Java".equals(s4)); // false(安全)
    }
}

2. 字符串查找与判断

方法 作用 示例
contains(CharSequence s) 是否包含指定子串 "Java学习".contains("Java") → true
startsWith(String prefix) 是否以指定前缀开头 "Java".startsWith("Ja") → true
endsWith(String suffix) 是否以指定后缀结尾 "test.txt".endsWith(".txt") → true
indexOf(String str) 查找子串首次出现的索引(无则返回 - 1) "abcabc".indexOf("bc") → 1
lastIndexOf(String str) 查找子串最后出现的索引 "abcabc".lastIndexOf("bc") → 4
isEmpty() 是否为空字符串(长度为 0) "".isEmpty() → true
isBlank()(Java 11+) 是否为空或全空格 " ".isBlank() → true

实战 3:查找与判断场景

java

运行

java 复制代码
public class StringFindDemo {
    public static void main(String[] args) {
        String str = "Java学习日记-2025";

        // 1. 包含判断
        System.out.println(str.contains("学习")); // true

        // 2. 前缀/后缀判断
        System.out.println(str.startsWith("Java")); // true
        System.out.println(str.endsWith("2025")); // true

        // 3. 索引查找
        System.out.println(str.indexOf("日记")); // 4
        System.out.println(str.lastIndexOf("a")); // 3

        // 4. 空判断
        String emptyStr = "   ";
        System.out.println(emptyStr.isEmpty()); // false(有空格)
        System.out.println(emptyStr.isBlank()); // true(Java 11+)
    }
}

3. 字符串截取与替换

方法 作用 示例
substring(int beginIndex) 从指定索引截取到末尾 "Java学习".substring(4) → "学习"
substring(int begin, int end) 截取 [begin, end) 区间(左闭右开) "Java学习".substring(0,4) → "Java"
replace(CharSequence old, CharSequence new) 替换所有匹配子串 "aabbaa".replace("aa", "bb") → "bbbbbb"
replaceFirst(String regex, String replacement) 替换第一个匹配子串 "aabbaa".replaceFirst("aa", "bb") → "bbaaa"
trim() 去除首尾空格(不处理中间) " Java 学习 ".trim() → "Java 学习"
strip()(Java 11+) 去除首尾所有空白(包括全角空格) " Java ".strip() → "Java"

实战 4:截取与替换

java

运行

java 复制代码
public class StringCutReplaceDemo {
    public static void main(String[] args) {
        String str = "  2025-Java-学习日记  ";

        // 1. 截取
        String sub1 = str.substring(3); // "2025-Java-学习日记  "
        String sub2 = str.substring(3, 7); // "2025"
        System.out.println(sub2); // 2025

        // 2. 替换
        String replace1 = str.replace("-", "|"); // "  2025|Java|学习日记  "
        String replace2 = str.replaceFirst("-", "|"); // "  2025|Java-学习日记  "
        System.out.println(replace1);

        // 3. 去空格
        String trimStr = str.trim(); // "2025-Java-学习日记"
        System.out.println(trimStr);
    }
}

4. 字符串拆分与转换

方法 作用 示例
split(String regex) 按正则拆分字符串为数组 "a,b,c".split(",") → ["a","b","c"]
toCharArray() 转换为 char 数组 "Java".toCharArray() → ['J','a','v','a']
toUpperCase() 转大写 "java".toUpperCase() → "JAVA"
toLowerCase() 转小写 "JAVA".toLowerCase() → "java"
valueOf(Object obj) 静态方法:将任意类型转为字符串 String.valueOf(123) → "123"

实战 5:拆分与转换

java

运行

java 复制代码
public class StringSplitConvertDemo {
    public static void main(String[] args) {
        // 1. 拆分(注意:. | * 等正则符号需转义)
        String str = "2025.12.23";
        String[] arr = str.split("\\."); // 转义.,否则按任意字符拆分
        for (String s : arr) {
            System.out.print(s + " "); // 2025 12 23
        }

        // 2. 转换
        char[] chars = "Java".toCharArray();
        System.out.println(chars[0]); // J

        // 3. 大小写转换
        System.out.println("Java".toUpperCase()); // JAVA

        // 4. 任意类型转字符串(避免空指针)
        Integer num = null;
        System.out.println(String.valueOf(num)); // "null"(安全)
        // System.out.println(num.toString()); // 空指针异常
    }
}

三、字符串拼接:效率优化核心(面试高频)

字符串拼接是开发中高频操作,但不同拼接方式效率差异极大,核心原因是String的不可变性 ------ 每次+拼接都会创建新对象,频繁拼接会产生大量临时对象,消耗内存且效率低。

1. 三种拼接方式对比

拼接方式 底层实现 效率 适用场景
+ 运算符 编译后自动转为StringBuilder(单次拼接) 中等 少量、固定次数拼接(如 2-3 次)
StringBuilder(非线程安全) 可变字符序列,直接修改内部数组 单线程、大量拼接(如循环拼接)
StringBuffer(线程安全) StringBuilder类似,方法加synchronized 中高 多线程、大量拼接

实战 6:拼接效率对比(循环 10000 次)

java

运行

java 复制代码
public class StringConcatDemo {
    public static void main(String[] args) {
        // 1. + 拼接(效率低)
        long start1 = System.currentTimeMillis();
        String s1 = "";
        for (int i = 0; i < 10000; i++) {
            s1 += i; // 每次创建新String对象
        }
        long end1 = System.currentTimeMillis();
        System.out.println("+拼接耗时:" + (end1 - start1) + "ms"); // 约100ms+

        // 2. StringBuilder(效率高)
        long start2 = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(i); // 直接修改内部数组,无新对象
        }
        String s2 = sb.toString();
        long end2 = System.currentTimeMillis();
        System.out.println("StringBuilder耗时:" + (end2 - start2) + "ms"); // 约1ms

        // 3. StringBuffer(线程安全,略慢于StringBuilder)
        long start3 = System.currentTimeMillis();
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < 10000; i++) {
            sbf.append(i);
        }
        String s3 = sbf.toString();
        long end3 = System.currentTimeMillis();
        System.out.println("StringBuffer耗时:" + (end3 - start3) + "ms"); // 约2ms
    }
}

2. 拼接优化最佳实践

  1. 单线程大量拼接 :优先用StringBuilder,且指定初始容量(避免扩容):

    java

    运行

    java 复制代码
    // 指定初始容量(预估长度),减少数组扩容次数
    StringBuilder sb = new StringBuilder(10000);
  2. 多线程拼接 :用StringBuffer(或手动加锁的StringBuilder);

  3. 少量固定拼接 :直接用+(代码简洁,编译后会优化为StringBuilder);

  4. 集合拼接为字符串 (Java 8+):用String.join()

    java

    运行

    java 复制代码
    List<String> list = Arrays.asList("Java", "学习", "日记");
    String str = String.join("-", list); // "Java-学习-日记"

四、字符串常量池(String Pool):底层优化核心

字符串常量池是 JVM 为优化字符串创建而设计的内存区域(方法区 / 元空间),核心作用是:缓存字符串常量,避免重复创建相同内容的字符串对象,节省内存。

1. 常量池的核心规则

  • 直接赋值String s = "Java" → 先检查常量池是否有 "Java",有则直接指向,无则创建后指向;

  • new 创建String s = new String("Java") → 先在常量池创建 "Java",再在堆内存创建新对象指向常量池的 "Java"(共 2 个对象,除非常量池已有);

  • intern () 方法 :将堆中的字符串对象加入常量池(若不存在),返回常量池引用:

    java

    运行

    java 复制代码
    String s1 = new String("Java");
    String s2 = s1.intern(); // s2指向常量池的"Java"
    System.out.println(s2 == "Java"); // true

实战 7:常量池核心案例

java

运行

java 复制代码
public class StringPoolDemo {
    public static void main(String[] args) {
        // 1. 直接赋值:指向常量池同一对象
        String s1 = "Java";
        String s2 = "Java";
        System.out.println(s1 == s2); // true

        // 2. new创建:堆对象 vs 常量池对象
        String s3 = new String("Java");
        System.out.println(s1 == s3); // false

        // 3. intern():堆对象入池
        String s4 = s3.intern();
        System.out.println(s1 == s4); // true

        // 4. 拼接常量:编译期优化,直接指向常量池
        String s5 = "Ja" + "va";
        System.out.println(s1 == s5); // true

        // 5. 拼接变量:运行期拼接,创建新对象
        String a = "Ja";
        String s6 = a + "va";
        System.out.println(s1 == s6); // false
    }
}

五、综合实战:String 类高频业务场景

场景 1:用户输入校验(空判断、格式校验)

java

运行

java 复制代码
public class StringValidateDemo {
    // 校验手机号格式(简单版)
    public static boolean isValidPhone(String phone) {
        // 1. 空判断(安全)
        if (phone == null || phone.isBlank()) {
            System.out.println("手机号不能为空");
            return false;
        }
        // 2. 长度判断
        if (phone.length() != 11) {
            System.out.println("手机号必须为11位");
            return false;
        }
        // 3. 开头判断
        if (!phone.startsWith("1")) {
            System.out.println("手机号必须以1开头");
            return false;
        }
        // 4. 是否全为数字(简化版,实际用正则)
        for (char c : phone.toCharArray()) {
            if (!Character.isDigit(c)) {
                System.out.println("手机号只能包含数字");
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        System.out.println(isValidPhone("13800138000")); // true
        System.out.println(isValidPhone("123456")); // false(长度不足)
    }
}

场景 2:字符串反转(面试高频)

java

运行

java 复制代码
public class StringReverseDemo {
    // 方法1:用StringBuilder
    public static String reverse1(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        return new StringBuilder(str).reverse().toString();
    }

    // 方法2:手动反转(char数组)
    public static String reverse2(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        char[] chars = str.toCharArray();
        int left = 0;
        int right = chars.length - 1;
        while (left < right) {
            // 交换字符
            char temp = chars[left];
            chars[left] = chars[right];
            chars[right] = temp;
            left++;
            right--;
        }
        return new String(chars);
    }

    public static void main(String[] args) {
        String str = "Java学习";
        System.out.println(reverse1(str)); // 习学avaJ
        System.out.println(reverse2(str)); // 习学avaJ
    }
}

六、高频避坑指南(新手必背)

  1. 空指针异常 :比较字符串时,始终用常量.equals (变量)(如"Java".equals(s));
  2. == 与 equals 混淆==比引用,equals比内容,切勿用==比较字符串内容;
  3. split 方法转义 :拆分.|*等正则特殊字符时,需用\\转义;
  4. 大量拼接用 + :循环中用+拼接字符串,效率极低,务必用StringBuilder
  5. new String 的滥用 :直接赋值(String s = "Java")比new String("Java")更节省内存;
  6. trim 与 isBlank 混淆trim()仅去除半角空格,isBlank()(Java 11+)可处理全角空格和空字符串。

总结

今天我们系统掌握了String类的核心知识点:

  • 不可变性 :由final类、final数组、无 setter 方法保证,是常量池和线程安全的基础;
  • 常用方法:按 "比较、查找、截取、替换、拆分、转换" 分类记忆,覆盖 80% 业务场景;
  • 拼接优化 :单线程大量拼接用StringBuilder(指定初始容量),多线程用StringBuffer
  • 常量池 :直接赋值复用常量池对象,new String创建堆对象,intern()可将堆对象入池。

String是 Java 中最基础也最易踩坑的类,掌握以上核心点,能解决开发中 90% 的字符串相关问题。下一篇【Day13】,我们会讲解 "集合框架(二):Map 的核心用法与场景",帮你掌握键值对存储的核心容器!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀

相关推荐
JIngJaneIL2 小时前
基于springboot + vue健康管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端
秋饼2 小时前
【三大锁王争霸赛:Java锁、数据库锁、分布式锁谁是卷王?】
java·数据库·分布式
dyxal2 小时前
Python包导入终极指南:子文件如何成功调用父目录模块
开发语言·python
电商API&Tina2 小时前
【电商API接口】关于电商数据采集相关行业
java·python·oracle·django·sqlite·json·php
刘一说2 小时前
Spring Boot中IoC(控制反转)深度解析:从实现机制到项目实战
java·spring boot·后端
悟空码字2 小时前
SpringBoot参数配置:一场“我说了算”的奇幻之旅
java·spring boot·后端
我居然是兔子2 小时前
Java虚拟机(JVM)内存模型与垃圾回收全解析
java·开发语言·jvm
关于不上作者榜就原神启动那件事2 小时前
Spring Data Redis 中的 opsFor 方法详解
java·redis·spring
其美杰布-富贵-李2 小时前
Java (Spring Boot) 反射完整学习笔记
java·spring boot·学习