Day19 | Java中的String

35 阅读4分钟

今天我们要聊一个在写Java代码时几乎天天会用到的类:String。

没错,就是那个用双引号包起来的 "Hello World"。

一、为什么说String很重要?

在Java中,String被设计成一个不可变类。

它不仅是我们平时打印日志、处理输入输出的时候频繁使用的类型,也是类加载、反射、注解等机制的核心参与者。

System.out.println("Hello World");
String s = "懒惰蜗牛";
if (s.equals("学习Java")) { ... }

这些看起来普通的字符串字面量,其实背后还是有很多值得我们探索的东西。

二、String的本质什么

1、String是个类

在 java.lang包中,String是一个final类,表示它不能被继承。

来看源码片段(基于JDK 17):

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    private final byte[] value;
    private final byte coder;
    private int hash; // Default to 0
    ...
}

value是存储字符数据的底层数组。

左侧是JDK 17,右侧是JDK 8。JDK 9之后value从char[]修改为了byte[]。

String类的UML类图如下:

String实现了Serializable,表示他可以被序列化。

实现了Comparable,表示可以进行排序。

实现了CharSequence,表示可按字符序列处理。

2、String是不可变的

这个点应该经常被强调,特别是在面试场景。

String s = "懒惰";
s = s + "蜗牛";
System.out.println(s); // 懒惰蜗牛

你以为是把"蜗牛"加到了"懒惰"后面,其实是新建了一个字符串"懒惰蜗牛",原来的"懒惰"没变。

这种设计有下面几个好处:

既然不可变,那对于多线程的读写来说就是线程安全的,不需要加锁。

可以缓存hashCode,如果使用HashMap这类容器的时候,就能提高使用效率。

不可变的东西,就可以设计一个常量池,避免重复创建相同内容的字符串,减少内存的使用。

在JDK 8中,字符串字面量的连接操作实际是通过显示创建StringBuilder对象实现的;

在JDK 17中,字符串字面量的连接操作是通过动态调用makeConcatWithConstants实现的;

三、String是怎么存储和比较的?

1、字符串常量池

看下面的代码:

package com.lazy.snail.day19;

/**
 * @ClassName StringConstantPool
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/4 15:29
 * @Version 1.0
 */
public class StringConstantPool {
    public static void main(String[] args) {
        String a = "蜗牛";
        String b = "蜗牛";
        System.out.println(a == b);
    }
}

输出结果是true。因为这两个"蜗牛"都是字符串字面量,JVM会把它们存储在字符串常量池中,a和b实际指向的是同一个对象。

但是如果你写成这样:

package com.lazy.snail.day19;

/**
 * @ClassName StringConstantPool
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/4 15:29
 * @Version 1.0
 */
public class StringConstantPool {
    public static void main(String[] args) {
        String a = new String("蜗牛");
        String b = new String("蜗牛");
        System.out.println(a == b);
    }
}

输出结果就变成了false。这是因为new关键字会强制在堆上分配新内存,每次都生成新对象。

2、equals()和==的区别

在之前我们讲Object.equals() 的时候已经提到过这个问题,但在String里面它更为重要:

看这个例子:

package com.lazy.snail.day19;

/**
 * @ClassName StringTest
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/4 15:34
 * @Version 1.0
 */
public class StringTest {
    public static void main(String[] args) {
        String a = "蜗牛";
        String b = new String("蜗牛");

        System.out.println(a == b);
        System.out.println(a.equals(b));
    }
}

脑跑一下代码,看看结果是什么。

没错,fasle\true。==比较的是引用,equals比较的是内容。

大多数开发场景中都是使用的equals比较。但是面试或者笔试会在==和equals之间绕来绕去。

四、String的常用方法

String类中定义了非常多的方法:

下面我把开发中常用的一些方法列举一下:

方法作用描述
length()获取字符串长度
charAt(int index)获取指定位置的字符
substring(int, int)截取子串
equals(Object obj)判断内容相等
equalsIgnoreCase()忽略大小写比较
indexOf(String)查找子串第一次出现的位置
startsWith() / endsWith()判断前缀/后缀
replace()替换内容
split(String)拆分字符串
trim()去除首尾空格
toUpperCase() / toLowerCase()转大小写

结语

开发过程中,如果对字符串操作的API不熟悉,可以直接阅读源码。

String (Java SE 17 & JDK 17)中也有对String类里方法、参数详细的描述。

下一篇预告

Day20 | static详解:静态变量、方法、代码块与类加载流程全面剖析

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

更多文章请关注我的公众号《懒惰蜗牛工坊》