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

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

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

在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 编程中不可或缺的一部分。

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...