String 是 Java 中最常用的引用数据类型,用于表示不可变的字符序列,掌握其核心特性和用法是 Java 基础的关键。

一、String 的核心特性

1. 不可变性(Immutable)

  • 定义:字符串对象一旦创建,其内容(字符序列)无法被修改。
  • 底层原因:String 类的核心存储是 private final char value[](JDK9+ 改为 byte[] 节省空间),final 修饰的数组无法重新赋值,且数组本身的内容也被封装为私有,无对外修改的方法。
  • 表现:对字符串的拼接、替换等操作,本质是创建新的 String 对象,原对象不变。

    java

    运行

    String s = "abc";
    s += "d"; // 实际创建了新字符串 "abcd",原 "abc" 仍存在
    
  • 优势:线程安全、可缓存(字符串常量池)、哈希值可缓存(hashCode 计算一次后复用)。

2. 字符串常量池(String Pool)

  • 定义:JVM 为优化内存,在堆内存中开辟的一块特殊区域,用于存储唯一的字符串字面量。
  • 创建规则
    • 直接使用字面量(String s = "abc"):优先从常量池查找,不存在则创建,存在则复用引用。
    • new String("abc"):先在常量池创建 "abc",再在堆中创建新对象指向该常量池内容(共创建 1-2 个对象)。
  • 示例

    java

    运行

    String s1 = "abc"; // 常量池创建
    String s2 = "abc"; // 复用常量池引用,s1 == s2 → true
    String s3 = new String("abc"); // 堆新对象,s1 == s3 → false
    String s4 = s3.intern(); // 手动将 s3 入池,s1 == s4 → true
    

二、String 的创建与比较

1. 创建方式

表格

方式 示例 特点
字面量创建 String s = "hello" 优先使用常量池,效率高
new 关键字 String s = new String("hello") 堆内存创建新对象,效率低,避免频繁使用
字符数组转换 String s = new String(new char[]{'h','e','l','l','o'}) 从字符数组构建,适合动态生成字符串

2. 比较方式

表格

方式 作用 示例
== 比较对象引用地址(是否指向同一对象) s1 == s2(字面量相等则 true,new 创建则 false)
equals() 比较字符串内容(String 重写了 Object 的 equals) s1.equals(s2)(内容相同则 true)
equalsIgnoreCase() 忽略大小写比较内容 "Abc".equalsIgnoreCase("abc") → true

三、String 常用方法(高频)

1. 基础获取

  • length():获取字符串长度(字符数),注意与数组 length 属性区分。
  • charAt(int index):获取指定索引的字符(索引从 0 开始),越界抛 IndexOutOfBoundsException
  • indexOf(String str):查找子串首次出现的索引,无则返回 -1。
  • lastIndexOf(String str):查找子串最后出现的索引,无则返回 -1。

2. 内容判断

  • isEmpty():判断是否为空字符串(长度为 0),注意 null 调用会抛空指针。
  • contains(CharSequence s):判断是否包含指定子串。
  • startsWith(String prefix):判断是否以指定前缀开头。
  • endsWith(String suffix):判断是否以指定后缀结尾。

3. 内容修改(返回新字符串)

  • substring(int beginIndex) / substring(int begin, int end):截取子串(左闭右开)。
  • replace(char old, char new) / replace(CharSequence target, CharSequence replacement):替换字符 / 子串。
  • trim():去除首尾空白字符(空格、制表符、换行等,JDK11+ 可用 strip() 支持全角空格)。
  • toLowerCase() / toUpperCase():转换大小写。
  • split(String regex):按正则表达式分割字符串,返回字符串数组。

4. 类型转换

  • 字符串转字符数组:toCharArray()(如字符串反转的核心操作)。
  • 字符串转字节数组:getBytes()(指定编码如 getBytes("UTF-8") 避免乱码)。
  • 其他类型转字符串:String.valueOf(xxx)(推荐,避免 null 转为 "null")。

四、String 拼接的效率对比

表格

拼接方式 原理 效率 适用场景
+ / += 底层自动创建 StringBuilder,拼接后转 String 低(频繁创建对象) 少量、简单拼接
StringBuilder 可变字符序列,直接修改底层数组 循环 / 大量拼接
StringBuffer 与 StringBuilder 功能一致,线程安全(方法加 synchronized 中(比 StringBuilder 慢) 多线程环境拼接

示例(循环拼接推荐 StringBuilder):

java

运行

// 低效
String s = "";
for (int i = 0; i < 1000; i++) {
    s += i; // 每次创建新对象
}

// 高效
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

五、String 与其他相关类的区别

表格

可变性 线程安全 核心用途
String 不可变 安全 存储固定字符串
StringBuilder 可变 不安全 单线程字符串拼接
StringBuffer 可变 安全(同步) 多线程字符串拼接

六、常见易错点

  1. 空指针问题:调用 null 字符串的方法(如 null.equals("abc"))会抛 NullPointerException,建议写成 "abc".equals(null)(返回 false)。
  2. 常量池与 new 的区别new String("abc") 会创建堆对象,避免不必要的 new 操作。
  3. split 正则陷阱split("\\.") 才是分割点号(点号是正则通配符,需转义)。
  4. JDK 版本差异:JDK9 后 String 底层由 char[] 改为 byte[],节省内存(Latin 字符占 1 字节,中文等占 2 字节)。

总结

  1. String 核心特性是不可变性常量池缓存,决定了其使用方式和性能特点。
  2. 字符串比较优先用 equals(),拼接优先用 StringBuilder(单线程)。
  3. 高频方法需重点掌握:charAt()substring()replace()split()contains() 等,注意方法返回新字符串的特性。

例题一:Java 字符串反转编程题解析

一、解题步骤分析

步骤 1:明确需求与约束

  • 功能需求:将输入字符串按字符顺序完全反转(如 HelloWorlddlroWolleH)。
  • 约束条件:禁止直接使用 StringBuilder.reverse() 等现成反转方法。
  • 边界要求:需处理空字符串、纯空格字符串、单字符字符串等特殊情况。

步骤 2:选择核心实现思路

由于不能使用现成反转方法,我们需要手动操作字符序列,双指针交换法是最高效且直观的选择:

  1. 将字符串转换为可修改的字符数组(String 本身不可变,必须通过数组操作)。
  2. 用两个指针分别指向数组的头部(left)尾部(right)
  3. 交换两个指针指向的字符,然后将 left 右移、right 左移,直到两指针相遇或交叉。
  4. 将修改后的字符数组重新封装为 String 并返回。

步骤 3:边界情况处理

在核心逻辑执行前,先处理特殊情况以避免无效操作:

  • 若输入为 null:直接返回 null(或根据需求返回空字符串)。
  • 若字符串长度 ≤ 1(空字符串、单字符):直接返回原字符串(无需反转)。
  • 纯空格字符串:交换后仍为纯空格,核心逻辑可正常处理,无需额外判断。

二、完整代码示例

package com.sy;

/**
 * 描述:字符串反转
 */
public class Demo01 {
    /**
     * 编写一个 Java 程序,实现字符串反转功能(禁止直接使用 `StringBuilder/reverse()` 等现成的反转方法)。
     * 输入:一个任意字符串(例如 "HelloWorld")
     * 输出:反转后的字符串(例如 "dlroWolleH")
     * 要求:需处理空字符串、纯空格字符串等边界情况。
     * 输出结果
     * 原字符串:HelloWorld,反转后:dlroWolleH
     * 原字符串:Java编程,反转后:程编avaJ
     */
    public static void main(String[] args) {
        String s = "HelloWorld";
        System.out.println("原字符串:" + s + ",反转后:" + reverse(s));
        String s1 = "Java编程";
        System.out.println("原字符串:" + s1 + ",反转后:" + reverse(s1));
    }
    public static String reverse(String s) {
        char[] chars = s.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);
    }
}

三、解题总结

核心思路总结

本题的核心是手动操作字符序列实现反转,通过双指针交换法在 O (n) 时间复杂度、O (n) 空间复杂度下完成高效反转,同时通过边界判断保证代码健壮性。

关键知识点

  1. String 不可变性:必须通过 toCharArray() 转换为字符数组才能修改字符顺序。
  2. 双指针思想:通过首尾指针向中间移动交换,避免了多次创建新字符串的开销。
  3. 边界处理:空字符串、单字符、纯空格等特殊情况是代码健壮性的关键。
  4. 字符数组与 String 转换new String(char[]) 是将修改后的数组还原为字符串的标准方式。

优化与拓展

  • 若需进一步优化空间,可考虑递归反转,但递归会增加栈开销,不推荐用于长字符串。
  • 若允许使用 StringBuilder,可通过 append 逆序遍历字符实现反转,但本题明确禁止使用 reverse() 方法。

题目二:Java 字符串字符类型频次统计题解析

一、解题步骤与代码分析

步骤 1:明确需求与约束

  • 功能需求:统计输入字符串中大写字母、小写字母、数字、空格、其他字符五类字符的出现次数。
  • 技术约束:必须使用 charAt()toCharArray() 遍历字符,通过ASCII 范围判断字符类型,不能依赖 Character 工具类(如 isUpperCase())。
  • 输出要求:清晰展示每类字符的数量,格式友好易读。

步骤 2:核心实现思路

  1. 初始化计数器:为五类字符分别定义一个 int 类型的计数器,初始值为 0。
  2. 遍历字符:将字符串转换为字符数组(toCharArray()),或通过 charAt() 逐个获取字符,遍历每一个字符。
  3. 类型判断:根据字符的 ASCII 值范围进行分类:
    • 大写字母:'A'(65) ~ 'Z'(90)
    • 小写字母:'a'(97) ~ 'z'(122)
    • 数字:'0'(48) ~ '9'(57)
    • 空格:' '(32)
    • 其他字符:不属于以上范围的字符
  4. 累加计数:根据判断结果,对对应类别的计数器执行 ++ 操作。
  5. 结果输出:按要求格式打印原字符串和各类字符的统计结果。

二、完整代码示例

package com.sy;

/**
* 统计字符串字符类型及频次
*/
public class Demo02 {
    public static void main(String[] args) {
        // 测试用例
        String testStr = "Hello 123 World!@#";
        Char(testStr);
    }
    public static void Char(String str) {
        // 初始化各类字符计数器
        int upperCase = 0;
        int lowerCase = 0;
        int digit = 0;
        int space = 0;
        int other = 0;

        // 将字符串转换为字符数组,遍历每个字符
        char[] chars = str.toCharArray();
        for (char c : chars) {
            if (c >= 'A' && c <= 'Z') {
                upperCase++;
            } else if (c >= 'a' && c <= 'z') {
                lowerCase++;
            } else if (c >= '0' && c <= '9') {
                digit++;
            } else if (c == ' ') {
                space++;
            } else {
                other++;
            }
        }

        // 输出统计结果
        System.out.println("字符串: " + str);
        System.out.println("大写字母: " + upperCase + " 个");
        System.out.println("小写字母: " + lowerCase + " 个");
        System.out.println("数字: " + digit + " 个");
        System.out.println("空格: " + space + " 个");
        System.out.println("其他字符: " + other + " 个");
    }
}
 

三、解题总结

核心思路总结

本题的核心是遍历字符串 + 基于 ASCII 范围的字符分类 + 计数器累加,通过手动判断字符类型,加深对 ASCII 编码和字符串底层结构的理解。

关键知识点

  1. 字符串遍历toCharArray()charAt() 是遍历字符串字符的两种基础方式,本题推荐使用 toCharArray(),代码更简洁。
  2. ASCII 范围判断:掌握常见字符的 ASCII 区间是核心考点,避免依赖封装好的工具类,体现底层实现能力。
  3. 计数器设计:多计数器模式是统计类问题的通用解法,清晰分离不同类别的数据。
  4. 边界处理:代码天然支持空字符串、纯空格等边界情况,无需额外判断,保证了程序健壮性。

优化与拓展

  • 若需支持更多语言字符(如中文),可拓展判断逻辑,但本题仅要求 ASCII 范围内的分类。
  • 可将统计结果封装为 Map 或自定义对象,便于后续数据处理和复用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐