Java String类

        在 Java 编程世界中,String 类无疑是使用频率最高的类之一。无论是日常开发中的数据处理,还是校招笔试中的算法题,亦或是面试时被频繁问到的底层原理,String 类都占据着重要地位。与 C 语言中用字符数组或指针处理字符串的方式不同,Java 的 String 类将数据与操作方法封装在一起,完美契合面向对象的设计思想。今天,我们就从 String 类的基础认知出发,逐步深入其核心用法、底层特性,并对比相关的 StringBuffer 和 StringBuilder 类,帮助大家全面掌握这一核心知识点。

一、为什么 String 类如此重要?

        在 C 语言中,字符串的处理依赖于字符数组和标准库函数(如strlenstrcpy),这种 “数据与操作分离” 的模式不仅不够直观,还容易出现内存越界等问题。而 Java 作为面向对象语言,专门设计了 String 类来封装字符串相关的属性和方法,让字符串操作更安全、更便捷。

从实际应用场景来看,String 类的重要性体现在三个方面:

  1. 开发高频性:字符串处理是开发中的基础需求,比如用户输入验证、数据格式转换(如字符串转整数)、日志输出等,都离不开 String 类的支持;
  2. 笔试常客:校招笔试中,字符串相关题目占比极高,例如 “字符串相加”“第一个只出现一次的字符”“最后一个单词的长度” 等,都是经典考点;
  3. 面试热点:面试官常围绕 String 类的底层原理提问,比如 “String 为什么不可变?”“String、StringBuffer、StringBuilder 的区别是什么?”,这些问题直接考察对 Java 底层设计的理解。

二、String 类的基础用法

        String 类提供了丰富的方法来满足字符串操作需求,我们从最核心的 “构造、比较、查找、转化、替换、拆分、截取” 七个维度展开讲解。

2.1 字符串构造:三种常用方式

String 类的构造方式有很多,日常开发中最常用的是以下三种:

public class StringDemo {
    public static void main(String[] args) {
        //1. 直接使用常量串构造(推荐,效率更高)
        String s1 = "hello world";
        //2. 通过new关键字创建String对象
        String s2 = new String("hello world");
        //3. 利用字符数组构造
        char[] charArray = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
        String s3 = new String(charArray);
        
        System.out.println(s1); //输出:hello world
        System.out.println(s2); //输出:hello world
        System.out.println(s3); //输出:hello world
    }
}

        注意:String 是引用类型,其内部并不直接存储字符串,而是通过一个private final char value[]数组(JDK1.8 及之前)来保存字符数据。这一点对理解 String 的 “不可变性” 至关重要。

2.2 字符串比较:四种核心方式

        字符串比较是判断两个字符串 “是否相等” 或 “大小关系” 的操作,Java 提供了四种常用方式,需注意区分适用场景:

比较方式核心逻辑适用场景
==对基本类型比较 “值”,对引用类型比较 “地址”(是否指向同一个对象)判断两个引用是否指向同一对象
equals(Object obj)重写自 Object 类,按 “字典序” 逐个比较字符,完全一致则返回 true判断字符串内容是否相等
compareTo(String s)按字典序比较,返回差值(正数:当前串大;负数:参数串大;0:相等)字符串排序(如 Collections.sort)
compareToIgnoreCase与 compareTo 逻辑一致,但忽略大小写(如 'A' 和 'a' 视为相等)不区分大小写的比较

代码示例

public class StringCompare {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");
        String s3 = new String("Hello");
        
        //1. == 比较地址
        System.out.println(s1 == s2); //false(两个不同对象,地址不同)
        
        //2. equals 比较内容
        System.out.println(s1.equals(s2)); //true(内容完全一致)
        System.out.println(s1.equals(s3)); //false(大小写不同)
        
        //3. compareTo 比较大小
        System.out.println(s1.compareTo(s2)); //0(相等)
        System.out.println(s1.compareTo(s3)); //32('h'的ASCII码比'H'大32)
        
        //4. compareToIgnoreCase 忽略大小写比较
        System.out.println(s1.compareToIgnoreCase(s3)); //0(忽略大小写后相等)
    }
}

2.3 字符串查找:定位字符或子串

        当需要在字符串中查找某个字符或子串的位置时,可使用 String 类提供的charAtindexOflastIndexOf等方法,常用方法如下:

方法功能描述
charAt(int index)返回指定索引处的字符,索引越界会抛出IndexOutOfBoundsException
indexOf(int ch)从左到右查找字符 ch,返回第一次出现的索引,未找到返回 - 1
indexOf(String str, int from)从 from 索引开始,查找子串 str 第一次出现的位置,未找到返回 - 1
lastIndexOf(int ch)从右到左查找字符 ch,返回第一次出现的索引,未找到返回 - 1

代码示例

public class StringSearch {
    public static void main(String[] args) {
        String s = "aaabbbcccaaabbbccc";
        
        System.out.println(s.charAt(3)); //输出:b(索引3处的字符)
        System.out.println(s.indexOf('c')); //输出:6('c'第一次出现的位置)
        System.out.println(s.lastIndexOf("bbb")); //输出:12("bbb"最后一次出现的位置)
    }
}

2.4 字符串转化:格式与类型的转换

        字符串转化是开发中常见的需求,主要包括 “数值与字符串互转”“大小写转换”“字符串与数组互转” 和 “格式化” 四种场景:

(1)数值与字符串互转
  • 数值转字符串:使用String.valueOf(数值),支持 int、double、boolean 等类型;
  • 字符串转数值:使用包装类的parseXxx方法(如Integer.parseIntDouble.parseDouble)。
//数值转字符串
String numStr1 = String.valueOf(123); // "123"
String numStr2 = String.valueOf(3.14); // "3.14"

//字符串转数值
int num1 = Integer.parseInt("123"); // 123
double num2 = Double.parseDouble("3.14"); // 3.14
(2)大小写转换

        通过toUpperCase()(转大写)和toLowerCase()(转小写)实现,仅影响字母,不改变其他字符:

String lowerStr = "hello WORLD";
System.out.println(lowerStr.toUpperCase()); //"HELLO WORLD"
System.out.println(lowerStr.toLowerCase()); //"hello world"
(3)字符串与数组互转
  • 字符串转数组:toCharArray(),返回字符数组;
  • 数组转字符串:通过new String(字符数组)实现。
//字符串转数组
char[] chars = "hello".toCharArray(); // ['h','e','l','l','o']

//数组转字符串
String str = new String(chars); // "hello"
(4)格式化

        使用String.format(格式, 参数),按指定格式生成字符串(如日期、数字格式):

String dateStr = String.format("%d年%d月%d日", 2024, 5, 20);
System.out.println(dateStr); //输出:2024年5月20日

2.5 字符串替换:替换指定内容

        字符串替换用于将字符串中的某个字符或子串替换为新内容,常用replaceAll(替换所有)和replaceFirst(替换第一个):

String str = "helloworld";
//替换所有'l'为'_'
System.out.println(str.replaceAll("l", "_")); //"he__owor_d"
//只替换第一个'l'为'_'
System.out.println(str.replaceFirst("l", "_")); //"he_loworld"

        注意:String 是不可变对象,替换操作不会修改原字符串,而是返回一个新的字符串。

2.6 字符串拆分:按分隔符分割

        将一个完整字符串按指定分隔符拆分为多个子串,使用split方法,支持 “全部分拆” 和 “指定拆分数”:

//1. 全部分拆(按空格分割)
String str1 = "hello world hello bit";
String[] parts1 = str1.split(" ");
for (String part : parts1) {
    System.out.println(part); //依次输出:hello、world、hello、bit
}

//2. 指定拆分数(拆分为2组)
String[] parts2 = str1.split(" ", 2);
for (String part : parts2) {
    System.out.println(part); //输出:hello、world hello bit
}

        特殊情况:若分隔符是 “|”“*”“.” 等特殊字符,需添加转义符 “\”(如拆分 IP 地址时,分隔符为 “\.”):

String ip = "192.168.1.1";
String[] ipParts = ip.split("\\."); //注意转义
for (String part : ipParts) {
    System.out.println(part); //依次输出:192、168、1、1
}

2.7 字符串截取:获取子串

        从原字符串中截取部分内容,使用substring方法,支持 “从指定索引截到末尾” 和 “指定起止索引截取”:

String str = "helloworld";
//1. 从索引5截到末尾(索引从0开始)
System.out.println(str.substring(5)); //"world"

//2. 截取[0,5)区间的子串(前闭后开,包含0,不包含5)
System.out.println(str.substring(0, 5)); //"hello"

三、String 的核心特性:不可变性

        String 类最核心的特性是 “不可变性”—— 一旦字符串对象被创建,其内容就无法被修改。其原因是:

  1. String 类被 final 修饰:意味着 String 类不能被继承,避免子类重写方法破坏不可变性;
  2. value 数组被 final 修饰:仅表示 value 数组的引用不能指向其他数组,但数组内部的字符是可以修改的(只是 String 类没有提供修改数组的方法);
  3. 无修改方法:String 类没有提供任何可以修改value数组内容的 public 方法(如 “修改某个索引的字符”“追加字符” 等),所有看似 “修改” 的操作(如replacesubstring)都会创建新的 String 对象。

代码验证

//尝试修改value数组(反射方式,不推荐)
String str = "hello";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true); //跳过访问权限检查
char[] value = (char[]) valueField.get(str);
value[0] = 'H';
System.out.println(str); //输出:Hello(证明value数组内容可改,但String类未提供公开方法)

为什么要设计成不可变性?

String 的不可变性带来了三个核心优势:

  1. 支持字符串常量池:常量池可以缓存相同内容的字符串,避免重复创建对象,节省内存(下文详解);
  2. 线程安全:不可变对象的状态不会被修改,多线程环境下无需同步,避免线程安全问题;
  3. 高效缓存哈希值:String 的hashCode会缓存到hash变量中,后续获取时无需重新计算,作为 HashMap 的 key 时效率更高。

四、字符串常量池:优化内存的关键

        为了减少重复创建字符串对象带来的内存开销,Java 虚拟机(JVM)设计了 “字符串常量池”—— 一个存储字符串常量的特殊区域(JDK1.8 后位于堆区)。

常量池的工作原理

  1. 当使用 “常量串构造”(如String s = "hello")时,JVM 会先检查常量池中是否存在 “hello”:若存在,直接让s指向常量池中的对象;若不存在,先在常量池中创建 “hello” 对象,再让s指向它。
  2. 当使用new关键字构造(如String s = new String("hello"))时,JVM 会先在常量池中检查 “hello”:若不存在,先在常量池创建 “hello”,再在堆区创建新的 String 对象(指向常量池的 “hello”);若存在,直接在堆区创建新的 String 对象(指向常量池的 “hello”)。

代码示例

String s1 = "hello"; //常量池查找,不存在则创建
String s2 = "hello"; //常量池已存在,直接指向
String s3 = new String("hello"); //堆区创建新对象,指向常量池的"hello"

System.out.println(s1 == s2); //true(都指向常量池的对象)
System.out.println(s1 == s3); //false(s1指向常量池,s3指向堆区)

常量池的优化:intern () 方法

  intern()方法可以将堆区的字符串对象 “入池”—— 若常量池中已存在相同内容的字符串,返回常量池对象的引用;若不存在,将当前对象加入常量池并返回其引用。

代码示例

String s1 = new String("hello"); //堆区对象,常量池创建"hello"
String s2 = s1.intern(); //常量池已存在"hello",返回常量池引用
String s3 = "hello"; //指向常量池

System.out.println(s1 == s2); //false(s1指向堆区,s2指向常量池)
System.out.println(s2 == s3); //true(都指向常量池)

五、StringBuffer 与 StringBuilder:可变字符串

        由于 String 的不可变性,频繁修改字符串(如循环追加)会创建大量临时对象,导致效率极低。为了解决这个问题,Java 提供了两个可变字符串类:StringBuffer 和 StringBuilder。

5.1 核心方法(以 StringBuilder 为例)

        StringBuffer 和 StringBuilder 的方法基本一致,常用方法包括append(追加)、insert(插入)、delete(删除)、replace(替换)、reverse(反转)等:

StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); //追加:hello world
sb.insert(5, ","); //插入:hello, world
sb.setCharAt(0, 'H'); //修改:Hello, world
sb.delete(5, 6); //删除:Hello world
sb.reverse(); //反转:dlrow olleH

System.out.println(sb.toString()); //输出:dlrow olleH

5.2 三者的核心区别

String、StringBuffer、StringBuilder 的区别是面试高频题,核心差异体现在 “可变性” 和 “线程安全性” 上:

特性StringStringBufferStringBuilder
可变性不可变(修改创建新对象)可变(直接修改内容)可变(直接修改内容)
线程安全性线程安全(不可变)线程安全(同步方法)线程不安全(无同步)
效率低(频繁修改创建临时对象)中(同步开销)高(无同步开销)
适用场景字符串不修改(如常量定义)多线程修改(如日志输出)单线程修改(如字符串拼接)

性能对比
通过循环追加 10000 次数字,对比三者的耗时(单位:毫秒):

//String:耗时约100ms(频繁创建对象)
String s = "";
for (int i = 0; i < 10000; i++) s += i;

//StringBuffer:耗时约1ms(同步开销)
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 10000; i++) sbf.append(i);

//StringBuilder:耗时约0.5ms(无同步开销)
StringBuilder sbd = new StringBuilder();
for (int i = 0; i < 10000; i++) sbd.append(i);

六、总结

        String 类是 Java 的核心类之一,掌握其用法和底层原理对开发和面试都至关重要。本文从基础用法出发,深入讲解了 String 的不可变性、字符串常量池,并对比了 StringBuffer 和 StringBuilder 的差异,最后通过面试题实战帮助大家巩固知识点。

核心要点回顾:

  1. String 不可变,修改操作会创建新对象,频繁修改建议用 StringBuilder(单线程)或 StringBuffer(多线程);
  2. 字符串比较优先使用equals,而非==
  3. 字符串常量池通过缓存相同内容的字符串优化内存,intern()方法可手动入池;
  4. StringBuffer 线程安全但效率低,StringBuilder 线程不安全但效率高,根据场景选择。

希望本文能帮助大家更加了解 String 类,在开发中写出更高效的代码。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值