Java入门(String类)

目录

[一、为什么 Java 要单独设计 String 类?](#一、为什么 Java 要单独设计 String 类?)

[1. 对比 C 语言:面向对象的必然选择](#1. 对比 C 语言:面向对象的必然选择)

[2. Java String 的设计目标](#2. Java String 的设计目标)

[二、String 的 4 种创建方式(附内存分析)](#二、String 的 4 种创建方式(附内存分析))

[1. 4 种核心创建方式(完整可运行代码)](#1. 4 种核心创建方式(完整可运行代码))

[2. 底层存储说明(JDK 9 + 重要优化)](#2. 底层存储说明(JDK 9 + 重要优化))

[三、字符串常量池:JVM 的 "字符串缓存神器"](#三、字符串常量池:JVM 的 “字符串缓存神器”)

[1. 什么是字符串常量池?](#1. 什么是字符串常量池?)

[2. 不同创建方式的内存差异(面试核心)](#2. 不同创建方式的内存差异(面试核心))

核心结论(务必记牢)

[3. intern () 方法:手动将字符串入池](#3. intern () 方法:手动将字符串入池)

实用场景

[四、字符串的 4 种比较方式(开发必避坑)](#四、字符串的 4 种比较方式(开发必避坑))

[1. 4 种比较方式对比](#1. 4 种比较方式对比)

[2. 实战代码示例](#2. 实战代码示例)

[3. 避坑提醒](#3. 避坑提醒)

[五、String 高频操作大全(可直接复制使用)](#五、String 高频操作大全(可直接复制使用))

[1. 字符串查找(定位字符 / 子串)](#1. 字符串查找(定位字符 / 子串))

[2. 类型转换(字符串↔基本类型)](#2. 类型转换(字符串↔基本类型))

[3. 替换、拆分、截取、去空格](#3. 替换、拆分、截取、去空格)

[✅ 核心提醒](#✅ 核心提醒)

[六、String 的不可变性:为什么不能改?](#六、String 的不可变性:为什么不能改?)

[1. 不可变性的底层实现](#1. 不可变性的底层实现)

[2. 不可变性的 3 个核心原因](#2. 不可变性的 3 个核心原因)

[3. 不可变性的 4 个核心好处](#3. 不可变性的 4 个核心好处)

[七、字符串拼接:StringBuilder vs StringBuffer](#七、字符串拼接:StringBuilder vs StringBuffer)

[1. 拼接性能问题演示](#1. 拼接性能问题演示)

[2. 高效拼接:StringBuilder(推荐)](#2. 高效拼接:StringBuilder(推荐))

[3. StringBuilder vs StringBuffer(核心区别)](#3. StringBuilder vs StringBuffer(核心区别))

最佳实践

[八、3 道经典 String 面试题(附详细解析)](#八、3 道经典 String 面试题(附详细解析))

[1. 字符串中第一个唯一字符](#1. 字符串中第一个唯一字符)

题目要求

解题思路

完整代码

[2. 验证回文串](#2. 验证回文串)

题目要求

解题思路

完整代码

[3. 字符串拼接性能对比(面试口述题)](#3. 字符串拼接性能对比(面试口述题))

问题

标准答案

九、核心知识点总结(速记版)


在 Java 开发中,String 是当之无愧的 "顶流" 类 ------ 日常业务中 80% 以上的代码都会接触字符串操作,面试中更是绕不开常量池、不可变性等核心考点。但看似简单的 String,却藏着无数容易踩坑的细节:为什么==equals()结果不一样?循环拼接字符串为什么性能差?常量池到底是怎么优化内存的?

本文从设计初衷→核心特性→实战用法→面试真题,层层拆解 Java 字符串的所有关键知识点,既有底层原理剖析,又有可直接复用的代码示例,帮你彻底搞懂 String,告别踩坑。

一、为什么 Java 要单独设计 String 类?

在理解 String 的特性前,我们先搞懂一个基础问题:为什么 Java 要为字符串专门设计一个类?

1. 对比 C 语言:面向对象的必然选择

C 语言中并没有专门的字符串类型,只能通过字符数组 + 库函数实现字符串功能:

cpp 复制代码
// C语言字符串:数据(字符数组)和操作(库函数)分离
char str[] = "hello";
// 调用库函数完成拼接,需要手动管理内存
strcat(str, " world");

这种方式存在三大问题:

  • 数据和操作分离,完全违背面向对象(OOP)的封装思想;
  • 需要手动管理内存,容易出现数组越界、内存泄漏等问题;
  • 字符串操作分散在各种库函数中,使用不统一、不直观。

2. Java String 的设计目标

由于字符串是开发中使用最频繁的数据类型 ,Java 专门设计java.lang.String类,核心目标是:

  • ✅ 封装:把字符串的数据(字符 / 字节)操作(拼接、替换、查找) 封装在类中,符合 OOP 思想;
  • ✅ 安全:避免手动操作内存,减少空指针、数组越界等异常;
  • ✅ 易用:提供丰富的 API,一站式解决所有字符串操作需求;
  • ✅ 高效:通过常量池、不可变性等设计优化性能和内存占用。

一句话总结:Java 中所有文本内容(如字面量、用户输入、文件内容),最终都会被封装成 String 对象。

二、String 的 4 种创建方式(附内存分析)

创建字符串远不止""这一种写法,不同创建方式对应不同的内存布局,也是面试高频考点。

1. 4 种核心创建方式(完整可运行代码)

java 复制代码
public class StringCreateDemo {
    public static void main(String[] args) {
        // 方式1:字符串字面量(最常用,推荐)
        String s1 = "Java String";
        
        // 方式2:new String()构造器(不推荐,会创建多余对象)
        String s2 = new String("Java String");
        
        // 方式3:字符数组构造(适合从字符序列构建字符串)
        char[] charArray = {'J', 'a', 'v', 'a'};
        String s3 = new String(charArray);
        
        // 方式4:字节数组构造(适合网络/文件IO场景)
        byte[] byteArray = {97, 98, 99, 100}; // 对应ASCII码:a,b,c,d
        String s4 = new String(byteArray);

        // 输出结果验证
        System.out.println("s1 = " + s1); // s1 = Java String
        System.out.println("s2 = " + s2); // s2 = Java String
        System.out.println("s3 = " + s3); // s3 = Java
        System.out.println("s4 = " + s4); // s4 = abcd
    }
}

2. 底层存储说明(JDK 9 + 重要优化)

  • JDK 8 及之前:String 底层用char[] value存储(char占 2 字节);
  • JDK 9 及之后:优化为byte[] value + coder存储(byte占 1 字节),根据字符串编码(UTF-8/ISO-8859-1)自动选择存储方式,内存占用减少 50%
  • 无论哪种版本,value数组都被private final修饰,这是 String 不可变的核心基础。

三、字符串常量池:JVM 的 "字符串缓存神器"

常量池是 Java 对 String 的关键优化,也是面试必问的核心知识点,我们从 "是什么→为什么→怎么用" 三层拆解。

1. 什么是字符串常量池?

字符串常量池(StringTable)是 JVM 方法区中一个特殊的HashTable 结构 ,核心作用是:缓存字符串字面量,避免重复创建相同内容的 String 对象,节省内存 + 提升性能

这是编程中经典的 "池化思想"(类似线程池、连接池):把常用的资源提前缓存,重复使用时直接取,不用重新创建。

2. 不同创建方式的内存差异(面试核心)

java 复制代码
public class StringPoolDemo {
    public static void main(String[] args) {
        // 场景1:字面量创建(复用常量池)
        String str1 = "hello";
        String str2 = "hello";
        // == 比较的是对象的内存地址
        System.out.println(str1 == str2); // true(指向常量池同一个对象)

        // 场景2:new创建(强制新建对象)
        String str3 = new String("hello");
        String str4 = new String("hello");
        System.out.println(str3 == str4); // false(堆上两个不同对象)
        
        // 场景3:字面量 vs new
        System.out.println(str1 == str3); // false(常量池 vs 堆)
    }
}
核心结论(务必记牢)
创建方式 内存位置 是否复用常量池 性能 / 内存
String s = "abc" 常量池(无则创建,有则复用) 最优
new String("abc") 堆(新对象)+ 常量池(若不存在) 否(堆对象唯一) 较差(冗余对象)

关键提醒new String("abc")会创建1~2 个对象

  • 如果常量池已存在 "abc":仅在堆创建 1 个新对象;
  • 如果常量池无 "abc":先在常量池创建 1 个,再在堆创建 1 个,共 2 个。

3. intern () 方法:手动将字符串入池

intern()是 String 的 native 方法(底层 C++ 实现),作用是:将当前字符串对象的内容放入常量池,并返回常量池中的引用

java 复制代码
public class StringInternDemo {
    public static void main(String[] args) {
        // 场景1:char数组构造的字符串(初始不在常量池)
        char[] arr = {'m', 'a', 'i', 'n'};
        String s1 = new String(arr); // 堆对象,常量池无"main"
        s1.intern(); // 手动将s1的内容"main"放入常量池
        String s2 = "main"; // 复用常量池中的"main"
        System.out.println(s1 == s2); // JDK 1.7+ 返回true

        // 场景2:先有字面量,后intern(复用已有常量)
        String s3 = new String("test"); // 常量池已创建"test"
        String s4 = s3.intern(); // 返回常量池的"test"引用
        String s5 = "test";
        System.out.println(s4 == s5); // true
        System.out.println(s3 == s5); // false(s3是堆对象)
    }
}
实用场景

当你需要大量复用相同内容的字符串(如解析日志、处理海量文本),用intern()可以大幅减少内存占用(但注意:常量池空间有限,过度使用可能导致 OOM)。

四、字符串的 4 种比较方式(开发必避坑)

字符串比较是最容易出错的场景之一,核心原则:比较内容用 equals (),比较地址用 ==,我们用表格 + 代码讲清楚:

1. 4 种比较方式对比

方法 比较目标 返回值 适用场景
== 对象的内存地址 boolean 判断是否为同一个对象
equals() 字符串内容(区分大小写) boolean 常规内容比较(开发最常用)
compareTo() 字典序(ASCII 码差值) int(正 / 负 / 0) 排序、比较大小
compareToIgnoreCase() 字典序(忽略大小写) int(正 / 负 / 0) 不区分大小写的排序 / 比较

2. 实战代码示例

java 复制代码
public class StringCompareDemo {
    public static void main(String[] args) {
        String a = "Java";
        String b = "java";
        String c = new String("Java");

        // 1. ==:比较地址(易错点)
        System.out.println(a == c); // false(a在常量池,c在堆)
        System.out.println(a == b); // false(内容不同,地址不同)

        // 2. equals():比较内容(推荐)
        System.out.println(a.equals(c)); // true(内容相同)
        System.out.println(a.equals(b)); // false(区分大小写)
        // 空指针安全写法(推荐):常量在前
        System.out.println("Java".equals(a)); // true

        // 3. compareTo():字典序比较
        // 'J'的ASCII码是74,'j'是106,74-106=-32
        System.out.println(a.compareTo(b)); // -32
        System.out.println(b.compareTo(a)); // 32
        System.out.println(a.compareTo(c)); // 0(内容相同)

        // 4. compareToIgnoreCase():忽略大小写
        System.out.println(a.compareToIgnoreCase(b)); // 0
    }
}

3. 避坑提醒

  • ❌ 错误:if (str == "abc")(即使内容相同,地址可能不同);
  • ✅ 正确:if ("abc".equals(str))(常量在前,避免 str 为 null 时抛空指针);
  • ✅ 进阶:JDK 1.7 + 可用Objects.equals(str, "abc")(自动处理 null)。

五、String 高频操作大全(可直接复制使用)

String 提供了丰富的 API,以下是开发中最常用的操作,附完整示例和注意事项。

1. 字符串查找(定位字符 / 子串)

java 复制代码
public class StringFindDemo {
    public static void main(String[] args) {
        String s = "hello-java-string";
        
        // 1. 获取指定下标字符(下标从0开始)
        char ch = s.charAt(6); 
        System.out.println("下标6的字符:" + ch); // j
        
        // 2. 查找字符首次出现的下标(无则返回-1)
        int firstJ = s.indexOf('j'); 
        System.out.println("j首次出现下标:" + firstJ); // 6
        
        // 3. 查找字符最后出现的下标
        int lastT = s.lastIndexOf('t'); 
        System.out.println("t最后出现下标:" + lastT); // 12
        
        // 4. 查找子串首次出现的下标
        int javaIndex = s.indexOf("java"); 
        System.out.println("java子串下标:" + javaIndex); // 6
    }
}

2. 类型转换(字符串↔基本类型)

java 复制代码
public class StringConvertDemo {
    public static void main(String[] args) {
        // 场景1:基本类型 → 字符串(推荐用String.valueOf,避免null)
        String numStr = String.valueOf(1024); // "1024"
        String boolStr = String.valueOf(true); // "true"
        
        // 场景2:字符串 → 基本类型
        int num = Integer.parseInt("1024"); // 1024
        double d = Double.parseDouble("3.14"); // 3.14
        boolean bool = Boolean.parseBoolean("true"); // true
        
        // 场景3:大小写转换(常用作统一格式)
        String upper = "java".toUpperCase(); // JAVA
        String lower = "JAVA".toLowerCase(); // java
        System.out.println(upper + " → " + lower); // JAVA → java
        
        // 注意:转换失败会抛NumberFormatException,需捕获
        try {
            Integer.parseInt("abc");
        } catch (NumberFormatException e) {
            System.out.println("字符串无法转换为数字");
        }
    }
}

3. 替换、拆分、截取、去空格

java 复制代码
public class StringOptDemo {
    public static void main(String[] args) {
        String str = "  hello-world-java  ";
        
        // 1. 替换(支持正则表达式)
        String replaceAll = str.replaceAll("-", "/"); // "  hello/world/java  "
        String replaceFirst = str.replaceFirst("-", "/"); // "  hello/world-java  "
        
        // 2. 拆分(特殊字符需转义,如.、|、\)
        String ip = "192.168.1.1";
        String[] ipArr = ip.split("\\."); // 转义.,拆分结果:["192","168","1","1"]
        System.out.println("IP拆分后第1段:" + ipArr[0]); // 192
        
        // 3. 截取(左闭右开区间 [start, end))
        String sub1 = str.substring(2, 7); // 从下标2到6:hello
        String sub2 = str.substring(7); // 从下标7到末尾:-world-java  
        
        // 4. 去空格(开发常用:处理用户输入、接口参数)
        String trimStr = str.trim(); // 去首尾空格:hello-world-java
        // JDK 11+新增:去所有空格
        // String stripAll = str.replaceAll("\\s", ""); // hello-world-java
        
        // 输出验证
        System.out.println("替换全部:" + replaceAll);
        System.out.println("截取:" + sub1);
        System.out.println("去空格:" + trimStr);
    }
}

✅ 核心提醒

String 是不可变对象 :所有修改操作(替换、截取、拼接)都不会改变原字符串,而是返回一个新的 String 对象

java 复制代码
// 示例:不可变性验证
String original = "hello";
String modified = original.concat(" world"); // 拼接
System.out.println(original); // hello(原字符串不变)
System.out.println(modified); // hello world(新字符串)

六、String 的不可变性:为什么不能改?

很多初学者疑惑:明明可以写str = "a" + "b",为什么说 String 是不可变的?我们从底层原理→不可变原因→核心好处三层讲透。

1. 不可变性的底层实现

String 的不可变性,本质是由两个关键设计保证的:

java 复制代码
// JDK 8 String核心源码(关键部分)
public final class String {
    // 存储字符串的数组,private + final修饰
    private final char value[];
    // 哈希值缓存(不可变所以哈希值固定)
    private int hash;

    // 构造器(外部无法直接修改value)
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    
    // 所有修改方法都返回新对象(如concat、replace)
    public String concat(String str) {
        // 省略实现:本质是创建新的char数组,复制原内容+新内容
        return new String(buf, true);
    }
}

2. 不可变性的 3 个核心原因

设计点 作用
String类被final修饰 禁止继承,避免子类修改核心逻辑
value数组private final 外部无法访问,且引用不可变(数组本身不能被替换)
修改方法返回新对象 所有 "修改" 都是创建新对象,原对象不变

3. 不可变性的 4 个核心好处

  • 线程安全:多线程并发访问时,无需加锁,不会出现数据不一致;
  • 哈希值稳定:hashCode () 计算一次后缓存,适合做 HashMap 的 Key(HashMap 要求 Key 的哈希值不变);
  • 常量池复用:不可变保证常量池中的字符串不会被篡改,可安全复用;
  • 安全性:避免字符串被恶意修改(如文件路径、数据库连接字符串)。

七、字符串拼接:StringBuilder vs StringBuffer

直接用+拼接字符串,在循环中会产生大量临时对象,性能极差!我们先看问题,再讲解决方案。

1. 拼接性能问题演示

java 复制代码
// 反例:循环中用+拼接,性能极差
public class StringConcatBadDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < 100000; i++) {
            s += i; // 每次拼接都创建新String对象
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "ms"); // 约5000ms(视机器而定)
    }
}

2. 高效拼接:StringBuilder(推荐)

java 复制代码
// 正例:用StringBuilder拼接,性能提升100倍+
public class StringBuilderDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append(i); // 直接追加到内部数组,不创建新对象
        }
        String result = sb.toString(); // 最终转为String
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "ms"); // 约50ms
        
        // 常用扩展方法
        sb.reverse(); // 反转字符串
        sb.insert(0, "prefix-"); // 在指定位置插入
        sb.delete(0, 7); // 删除指定区间字符
    }
}

3. StringBuilder vs StringBuffer(核心区别)

特性 StringBuilder StringBuffer
线程安全 不安全(无锁) 安全(方法加 synchronized)
性能 极高(推荐) 稍低(锁开销)
适用场景 单线程(90% 场景) 多线程(如并发拼接)
核心方法 append()/insert()/reverse() 同左
最佳实践
  • 90% 的业务场景(单线程):优先用StringBuilder
  • 多线程并发拼接(如日志收集):用StringBuffer
  • 少量固定拼接(如"hello" + "world"):直接用+(编译器会优化为 StringBuilder)。

八、3 道经典 String 面试题(附详细解析)

面试中 String 的题目常结合原理和实战,以下 3 道是高频题,附完整代码和思路解析。

1. 字符串中第一个唯一字符

题目要求

给定一个字符串,找到它的第一个不重复的字符,并返回其索引。如果不存在,则返回 -1。

解题思路
  • 第一步:遍历字符串,用数组统计每个字符出现的次数(ASCII 码范围 0-255,数组效率高于 HashMap);
  • 第二步:再次遍历字符串,找到第一个次数为 1 的字符,返回其索引。
完整代码
java 复制代码
public class FirstUniqCharDemo {
    public static int firstUniqChar(String s) {
        // 边界判断
        if (s == null || s.length() == 0) {
            return -1;
        }
        // 统计字符出现次数(ASCII码覆盖所有字符)
        int[] count = new int[256];
        for (int i = 0; i < s.length(); i++) {
            count[s.charAt(i)]++;
        }
        // 找第一个出现次数为1的字符
        for (int i = 0; i < s.length(); i++) {
            if (count[s.charAt(i)] == 1) {
                return i;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        System.out.println(firstUniqChar("leetcode")); // 0('l'是第一个唯一字符)
        System.out.println(firstUniqChar("loveleetcode")); // 2('v')
        System.out.println(firstUniqChar("aabb")); // -1(无唯一字符)
    }
}

2. 验证回文串

题目要求

给定一个字符串,验证它是否是回文串(正读和反读一样),只考虑字母和数字,忽略大小写。

解题思路
  • 双指针法:左指针从头部、右指针从尾部向中间移动;
  • 跳过非字母 / 数字的字符,比较对应位置的字符(忽略大小写);
  • 若所有对应字符都相等,则是回文串。
完整代码
javascript 复制代码
public class IsPalindromeDemo {
    public static boolean isPalindrome(String s) {
        // 边界判断
        if (s == null || s.length() == 0) {
            return true;
        }
        // 统一转为小写,简化比较
        s = s.toLowerCase();
        int left = 0;
        int right = s.length() - 1;
        while (left < right) {
            // 左指针跳过非字母/数字
            if (!Character.isLetterOrDigit(s.charAt(left))) {
                left++;
            }
            // 右指针跳过非字母/数字
            else if (!Character.isLetterOrDigit(s.charAt(right))) {
                right--;
            }
            // 字符不相等,直接返回false
            else if (s.charAt(left++) != s.charAt(right--)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        System.out.println(isPalindrome("A man, a plan, a canal: Panama")); // true
        System.out.println(isPalindrome("race a car")); // false
        System.out.println(isPalindrome("12321")); // true
    }
}

3. 字符串拼接性能对比(面试口述题)

问题

为什么循环中用+拼接字符串性能差?编译器对String s = "a" + "b" + "c"有优化吗?

标准答案
  • 循环中+拼接:每次拼接都会创建新的 StringBuilder 和 String 对象,大量临时对象导致 GC 频繁,性能差;
  • 固定拼接"a" + "b" + "c":编译器会优化为new StringBuilder().append("a").append("b").append("c").toString(),只创建一个 StringBuilder 和一个 String 对象,性能和手动写 StringBuilder 一致;
  • 最佳实践:循环拼接用 StringBuilder,固定拼接可直接用+

九、核心知识点总结(速记版)

  1. 创建方式 :优先用字面量""(复用常量池),避免new String()(冗余对象);
  2. 比较规则 :比较内容用equals()(常量在前防 NPE),比较地址用==,排序用compareTo()
  3. 核心特性:String 不可变(final 类 + final 数组),所有修改返回新对象;
  4. 性能优化 :频繁拼接用StringBuilder(单线程)/StringBuffer(多线程);
  5. 面试核心:常量池原理、intern () 方法、不可变性原因、拼接性能对比。

掌握以上知识点,你不仅能解决日常开发中的所有 String 问题,还能轻松应对面试中的 String 考点。如果觉得本文有用,欢迎收藏转发,后续会持续更新 Java 核心知识点!

相关推荐
l软件定制开发工作室2 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
0xDevNull2 小时前
Spring Boot 循环依赖解决方案完全指南
java·开发语言·spring
爱丽_2 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
bbq粉刷匠2 小时前
Java--多线程--单例模式
java·开发语言·单例模式
随风,奔跑2 小时前
Spring MVC
java·后端·spring
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
追风林2 小时前
idea支持本地 的 服务器 远程debug
java·服务器·intellij-idea
凸头2 小时前
AI 流式聊天接口实现(WebFlux+SSE)
java·人工智能