百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

为什么Java中的String是不可变的(Immutable)

wptr33 2025-07-28 00:36 1 浏览

在Java中,String类型是用于表示字符串的类,而字符串则是字符序列,是Java编程中最常用的数据类型之一。String类是不可变的,这意味着一旦创建,字符串的值就不能改变,下面我们就来介绍一下为什么String类型是不可变的?

为什么String是不可变的?

final关键字

有人说String类型是被声明为final,意味着不能继承String类,也就是说String类型没有子类型,不能通过子类型的变化去改变String类型的行为。并且在String 类中的内部字段也被声明为final。例如用于存储字符数组的char[]字段。并且我们知道final字段在对象构造完成后就不能被改变,如下所示。

public final class String {
    private final char value[];
    // other fields and methods
}

在String构造函数中,传入的字符串数组也会被复制到内部的私有的char数组中,如下所示。

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

Arrays.copyOf 方法会创建一个新的数组,并将传入的数组内容拷贝到新数组中,从而确保了内部数组的独立性和不可变性。

哈希码缓存

在String类中缓存了哈希码(hashCode) 的计算结果,在String类中有一个 hash 字段,用于存储哈希码,并且在第一次计算后就不会再改变。所以String类型如果是可变的,那么就会导致这种缓存机制所提供的哈希码的不一致,就会导致程序出现其他的问题。

private final int hash; // default to 0
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

字符串常量池

在Java中提供了字符串常量池来对内存结构进行优化,也就是说当一个字符串字面量被创建时,JVM 会先检查池中是否已经存在相同内容的字符串。如果存在,则返回池中的字符串引用;如果不存在,则创建新的字符串并放入池中。利用这种机制String是不可变的从而保证了这种机制的高效安全性。如下所示。

String s1 = "Hello";
String s2 = "Hello";
boolean result = (s1 == s2); // true, because s1 and s2 point to the same object in the pool

其实从上面的角度上来讲,都没有具体的说明为什么String类型是不可变的?只是告诉了大家从程序角度上的String类型不可变的原理。那么为什么被final修饰了就不可变,为什么要处理不可变呢?

为什么final修饰就是不可变的?

首先来讲,被final修饰,就不能被继承,这样就保证了不能通过继承来修改对象本身的属性。其次,String 类使用一个私有的 final char[] 数组来存储字符串内容,这个数组只能在 String 类内部访问。任何对字符串内容的操作,都是在这个数组上进行的,但数组本身不可修改。这也就导致了String的内部的不可变的特性。

另外我们发现在String类中没有提供任何的可以修改内部状态的方法,所有对于字符串内容的修改操作,如 substring()、concat()、replace(),都会返回一个新的 String 对象,而不是修改原对象。结合字符串常量池机制这个也很容易理解。

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

在Java中final可以修饰类、方法、属性等很多的内容,当final修饰引用类型变量时,确保引用本身不可变,但不保证引用的对象不可变。如下所示。

final char[] chars = {'a', 'b', 'c'};
chars[0] = 'z'; // 这是合法的,因为数组内容是可变的
// chars = new char[] {'x', 'y', 'z'}; // 这是非法的,因为 chars 引用是 final

在 String 类中,虽然 char[] value 是 final 的,但 String 类没有方法允许修改 value 的内容,从而间接确保了内容不可变性。

底层逻辑

从底层代码的角度上来讲,当一个类被声明为final时,编译器会在生成的字节码中标记该类为final,这在字节码中表示为ACC_FINAL标志。编译器会在编译阶段检查final类和 inal 方法是否被正确使用。如果试图继承一个 final 类或覆盖一个 final 方法,编译器会报错。

编译器在生成字节码时,会为final类和方法添加ACC_FINAL标志。这个标志在JVM加载字节码时被识别。JVM在加载字节码时,会检查这个标志并禁止任何对该类的继承。由于继承是改变类行为的主要手段,禁止继承确保了类的行为不会通过子类化被修改。

其次类的final标志在字节码层面上也是不允许被修改的,这是JVM提供的保护机制,不会尝试通过其他的方式来修改这个标识。

如下所示

public final class FinalClass {
    public final void finalMethod() {
        // method body
    }
}

生成字节码

public final class FinalClass {
  public final void finalMethod();
    descriptor: ()V
    flags: (0x0010) ACC_FINAL
    Code:
      // method body
}

如下所示

public final class MyFinalClass {
    private final int myFinalVar = 10;

    public final void myFinalMethod() {
        System.out.println("This is a final method.");
    }
}

生成的字节码

public final class MyFinalClass {
  private final int myFinalVar;

  public MyFinalClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field myFinalVar:I
      10: return

  public final void myFinalMethod();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String This is a final method.
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}
  • ACC_FINAL 标志用于标记 MyFinalClass 类和 myFinalMethod 方法。
  • myFinalVar 的赋值在构造方法中完成,确保其在对象初始化后不可改变。

当一个变量被声明为 final 时,它必须在声明时或构造器中初始化,一旦被赋值后,其引用或值就不能再被改变。

编译器在编译期间会检查 final 变量是否被正确初始化,并且确保在其生命周期中不会被重新赋值。

编译器可以对 final 变量进行优化,例如内联常量值,这在字节码级别表现为直接使用常量值而不是引用变量。字节码中没有提供重新赋值 final 变量的指令。例如,如果尝试对一个 final 变量进行重新赋值,编译器会生成错误。

通过以上解释可以看出,final 关键字通过编译时检查和运行时限制,确保了类、方法和变量在其生命周期内的不可变性。这种机制在语言层面和底层字节码中得到了严格的保证,使得 final 关键字成为实现不可变性的基础工具。

总结

综上,String 类型在 Java 中是一个设计精巧、功能强大且不可变的类。它在性能、安全性和多线程编程中提供了诸多优势,使其成为 Java 编程中不可或缺的一部分。

相关推荐

HIVE 窗口函数详解(hive常用开窗函数)

什么是窗口函数窗口函数是SQL中一类特别的函数。和聚合函数相似,窗口函数的输入也是多行记录。不同的是,聚合函数的作用于由GROUPBY子句聚合的组,而窗口函数则作用于一个窗口,这里,窗口...

SQL高效使用20招:数据分析师必备技巧

基础优化技巧善用EXPLAIN分析执行计划EXPLAINSELECT*FROMordersWHEREorder_date>'2024-01-01';...

答记者问之 - Redis 的高效架构与应用模式解析

问:极客程序员你好,请帮我讲一讲redis答:redis主要涉及以下核心,我来一一揭幕Redis的高效架构与应用模式解析...

MySQL通过累计求新增(mysql新增表字段语句)

前两天的那篇内容《MySQL递归实现单列分列成多行》...

一文讲懂SQL窗口函数 大厂必考知识点

大家好,我是宁一。今天是我们的第24课:窗口函数。...

圣诞快乐:用GaussDB T 绘制一颗圣诞树,兼论高斯数据库语法兼容

转眼就是圣诞的节日,祝大家节日快乐。用GaussDBT(也就是GaussDB100)绘制一棵圣诞树,纯国产,更喜庆。话不多说,上图:SQL如下:SELECTCASEWHENENMOTE...

Minitab:功能强大的质量管理、统计分析及统计图形软件

一、Minitab简介Minitab软件是为质量改善、教育和研究应用领域提供统计软件和服务的先导,是全球领先的质量管理和六西格玛实施软件工具及持续质量改进的良好工具软件,她具有强大的功能和简易的可视化...

如何熟练使用SQL查询(如何熟练使用sql查询内容)

要熟练使用SQL查询(StructuredQueryLanguage),你需要系统地从语法入门,到实战练习,再到性能优化与多表查询的掌握。下面是一条循序渐进、实战驱动的学习路径:第一阶段:S...

SAP SE38如何在多个系统间同步代码

上一篇文章写了如何在多个系统之间同步开发对象:多套SAPERP之间一键同步ABAP开发内容,有兄弟问有没有简单办法同步SE38程序代码的,因为使用请求的方式同步代码有点小题大做了。...

Python | 垂直模态分解(phython垂直输出)

...

技术栈:刷了百道SQL题,还是不会用?你应该这样补短板

这是来自用户的提问,也是很多人遇到的困惑:...

mysql窗口函数为了解决更加复杂的问题

为了解决复杂问题的窗口函数我们先讲一下窗口函数是什么窗口和普通的函数作用相同在不同列上进行查询和返回比如我们有如下的表...

MariaDB开窗函数(开窗函数 mysql)

在使用GROUPBY子句时,总是需要将筛选的所有数据进行分组操作,它的分组作用域是整张表。分组以后,为每个组只返回一行。而使用基于窗口的操作,类似于分组,但却可以对这些"组"(即窗口...

一文掌握 DuckDB 时间序列分析:窗口函数实战详解

...

一篇文章搞定MySQL中的窗口函数(mysql常用的窗口函数)

我是孙斌,北理数学系毕业,分享数据分析相关知识,点击右上角“关注”,学习更多数据分析知识。在MySQL中,分组groupby一般和聚合函数连用,如groupby+sum,这样能够得到每个组的总和,...