分类
Java

Java中的不可变对象

Immutable Objects in Java

1. 概述

本文我们将讨论什么是不可变对象,在JAVA中如何创建一个不可变的对象,以及为何要把某些对象设置为不可变的,这样做又有什么好处。

2. 什么是不可变对象

顾名思意,不可变对象就是说对象一旦通过实例化产生,其所有的属性将永远保持初始值,不会改变。

这也意味着:一旦不可变对象实例化创建完毕,在其被回收前,我们可以自由调用对象上任意暴露的方法,而对象将时刻保持实例化的初始状态。

字符串类型String是比较典型的不可变类,通过该类实例化的字符串为不可变对象。这同时意味着,字符串对象一旦创建,无论我们调用该对象是任意方法,该字符串均不会发生变化:

        String a = "codedemo.club";
        String b = a.replace("codedemo", "yunzhi");➊

        System.out.println(a);
        System.out.println(b);

        Assertions.assertEquals("codedemo.club", a);➋
        Assertions.assertEquals("yunzhi.club", b);
  • ➊ 调用a对象的replace方法来替换a字符串中的特定内容。
  • ➋ 替换方法执行后,a的值并未发生变化,我们说a为不可变对象。
codedemo.club
yunzhi.club

字符串String类型上的replace方法虽然提供了字符串替换的功能,但由于String被设计为不可变类,所以replace被调用后虽然返回了替换后的字符串,但原字符串a并不发生变化。我们把字符串a称为不可变对象,把字符串a属的String类称为不可变类

3. Java中的 final 关键字

在尝试自定义不可变类之前,我们先简单了解一下Java中的final关键字。

在Java中,变量默认是可以改变的(变量:可以改变的量),这意味着变量定义完成后,我们可以随时改变变量的值。

但如果我们使用final关键字来声明变量,Java编译器则不允许我们在后面改变该变量的值。比如以下代码将在编译时发生编译错误:

        final String a = "codedemo.club";

        // 以下代码将发生编译错误
        a = "yunzhi.club";

编译错误如下:

java: cannot assign a value to final variable a

值得注意的final关键字的作用仅是:禁止改变变量的引用值。但并不能禁止我们通过调用对象上的方法来改变其内部属性值(状态值),比如:

        final List<String> strings = new ArrayList<>(); 
        Assertions.assertEquals(0, strings.size());

        // 你不能改变strings的引用值,否则将发生编译错误
        // strings = new ArrayList<>();

        // 但可以调用ArrayList的add方法来改变strings的状态
        strings.add("codedemo.club"); 
        Assertions.assertEquals(1, strings.size());

上述代码成功的向被声明为final类型的strings列表中增加了子元素,也就是说strings虽然被声明了final,但其状态仍然是可改变不稳定的。这是由于ArrayList类型并不是一个不可变类型造成的。

4. Java中创建不可变对象

在了解了如何避免变量被改变的方法后,下面我们尝试创建不可变对象。

不可变对象的原则是:无论外部如何调用对象提供的公有方法,该对象的状态均不会发生改变。

比如我们可以直接将类中的属性全部声明为final:

public class Student {
    final private String name;
    final private int age;

上述代码中我们使用了final关键字来声明了Student类中的所有属性,而且这些属性的类型为主类型或不可变类型,这保证了Stduent类为不可变类,由该类实例化的对象为不可变对象。

但如果Student类型再增加一个Clazz班级属性,则欲保证Student为不可变类型,则需要保证Calzz类同样为不可变类。

public class Student {
    final private String name;
    final private int age;
    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

大多数时候,我们都需要在类中定义多个属性以存储对象的各个状态,对于声明为final类型的属性而言,只能在对象实例化时为其赋值:

public class Student {
    final private String name;
    
    // 声明属性时赋初值
    final private int age = 12;

    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

    /**
     * 或在构造函数中为其赋值
     */
    public Student(String name, Clazz clazz) {
        this.name = name;
        this.clazz = clazz;
    }
    
    // 此处省略了getter方法。所有属性均为final,所以该类无setter方法。

值得注意的是虽然Student在此为不可变类型,但Java提供的反射机制是可以忽视该不可变性,从而改变不可变对象的。在实际的使用中,我们往往不会(也不应该)这么做。

5. 不可变对象的优势

由于不可变对象状态的稳定性,所以在多线程情况下,我们可以放心地将不可变对象在不同的线程间传递、共享,而可以完全忽略各个线程是如何、何时利用该不可变对象的。

同时,我们也可以放心地将同一不可变对象分别嵌入到其它多个对象(可能是不同类型的)中,从而实现多对象共享某一相同对象的目标。重要的是,在此共享过程中,我们完全不必担心该共享对象可能会发生改变。

总之:不可变对象一旦实例化完成,我们便可以放心的使用它,而不必担心该对象可能会发生变化。

6. 总结

不可变对象一旦创建便会一直保持当前的状态,所以说它们是线程安全以及没有副作用的。正是由于这些特性,不可变对象往往被更多用于多线程环境中。