Skip to content

Latest commit

 

History

History
526 lines (386 loc) · 29.4 KB

File metadata and controls

526 lines (386 loc) · 29.4 KB

五、可变类和不可变类

在本章中,我们将学习可变类和不可变类。在构建面向对象的代码时,我们将了解它们的差异以及它们的优缺点。我们将:

  • 创建可变类
  • 在 JShell 中使用可变对象
  • 构建不可变类
  • 在 JShell 中使用不可变对象
  • 了解变异对象和非变异对象之间的区别
  • 了解编写并发代码时非变异对象的优点
  • 使用不可变String类的实例

在 Java 9 中创建可变类

当我们声明没有final关键字的实例字段时,我们创建了一个可变的实例字段,这意味着我们可以在字段初始化后为我们创建的每个新实例更改它们的值。当我们创建一个定义了至少一个可变字段的类的实例时,我们创建了一个可变对象,它是一个可以在初始化后更改其状态的对象。

可变对象也称为突变对象。

例如,假设我们必须开发一个 Web 服务来渲染 3D 世界中的元素并返回高分辨率渲染场景。这样的任务要求我们使用 3D 向量。首先,我们将使用具有三个可变字段的可变 3D 向量:xyz。可变 3D 矢量必须提供以下功能:

  • double类型的三个可变实例字段:xyz
  • 通过为xyz字段提供初始值来创建实例的构造函数。
  • 创建实例的构造函数,该实例的所有值都初始化为0,即x = 0y = 0z = 0。具有这些值的 3D 向量是被称为原点向量的。
  • 一个构造函数,它创建一个实例,其中所有值都初始化为一个公共值。例如,如果我们指定3.0作为公共值,则构造函数必须生成一个具有x = 3.0y = 3.0z = 3.0的实例。
  • 将 3D 向量的每个分量设置为其绝对值的absolute方法。
  • 一种negate方法,在适当的位置对 3D 向量的每个分量求反。
  • 一种add方法,将 3D 向量的值设置为自身和作为参数接收的 3D 向量之和。
  • 一种sub方法,将 3D 向量的值设置为自身与作为参数接收的 3D 向量的差值。
  • toString方法的一种实现,该方法打印 3D 向量的三个分量的值:xyz

以下几行声明了表示 Java 中 3D 向量可变版本的Vector3d类。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_01.java文件中。

public class Vector3d {
    public double x;
    public double y;
    public double z;

 Vector3d(double x, double y, double z) {
 this.x = x;
 this.y = y;
 this.z = z;
 }

 Vector3d(double valueForXYZ) {
 this(valueForXYZ, valueForXYZ, valueForXYZ);
 }

 Vector3d() {
 this(0.0);
 }

    public void absolute() {
        x = Math.abs(x);
        y = Math.abs(y);
        z = Math.abs(z);
    }

    public void negate() {
        x = -x;
        y = -y;
        z = -z;
    }

    public void add(Vector3d vector) {
        x += vector.x;
        y += vector.y;
        z += vector.z;
    }

    public void sub(Vector3d vector) {
        x -= vector.x;
        y -= vector.y;
        z -= vector.z;
    }

    public String toString() {
        return String.format(
            "(x: %.2f, y: %.2f, z: %.2f)",
            x,
            y,
            z);
    }
}

新的Vector3d类声明了三个构造函数,其行在前面的代码清单中突出显示。第一个构造函数接收三个double参数xyz,并使用这些参数中接收的值初始化具有相同名称和类型的字段。

第二个构造函数接收一个double参数valueForXYZ,并使用this关键字调用前面解释过的构造函数,将接收到的参数作为三个参数的值。

提示

我们可以在一个构造函数中使用this关键字来调用其他具有我们类中定义的不同参数的构造函数。

第三个构造函数是一个无参数的构造函数,使用this关键字调用前面解释过的构造函数,将0.0作为valueForXYZ参数的值。这样,构造函数允许我们构建一个原始向量。

无论何时调用absolutenegateaddsub方法,我们都会对实例进行变异,即改变对象的状态。这些方法更改我们从中调用它们的实例的xyz字段的值。

在 JShell 中处理可变对象

以下几行创建了一个名为vector1的新Vector3d实例,其中xyz的初始值分别为10.020.030.0。第二行创建了一个名为vector2的新Vector3d实例,其中1.02.03.0xyz的初始值。然后,代码使用vector1调用System.out.println方法,然后使用vector2作为参数。对println方法的两次调用都将对每个Vector3d实例执行toString方法,以显示可变 3D 向量的String表示。然后,代码以vector2作为参数为vector1调用add方法。最后一行再次调用println方法,并以vector1为参数,在调用add方法时对象发生变异后,打印xyz的新值。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_01.java文件中。

Vector3d vector1 = new Vector3d(10.0, 20.0, 30.0);
Vector3d vector2 = new Vector3d(1.0, 2.0, 3.0);
System.out.println(vector1);
System.out.println(vector2);
vector1.add(vector2);
System.out.println(vector1);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Working with mutable objects in JShell

字段的初始值为x10.0y20.0z30.0add方法更改三个字段的值。因此,对象状态发生如下变化:

  • vector1.x10.0突变为10.0+1.0=11.0
  • vector1.y20.0突变为20.0+2.0=22.0
  • vector1.z30.0突变为30.0+3.0=33.0

调用add方法后的vector1字段的值为x11.0y22.0z33.0。我们可以说该方法改变了对象的状态。因此,vector1是一个可变对象和可变类的实例。

以下几行使用三个可用构造函数创建名为vector3vector4vector5Vector3d类的三个实例。然后,接下来的几行调用System.out.println方法,在创建对象后打印xyz的值。样本的代码文件包含在example05_02.java文件的java_9_oop_chapter_05_01文件夹中。

Vector3d vector3 = new Vector3d();
Vector3d vector4 = new Vector3d(5.0);
Vector3d vector5 = new Vector3d(-15.5, -11.1, -8.8);
System.out.println(vector3);
System.out.println(vector4);
System.out.println(vector5);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Working with mutable objects in JShell

接下来的几行为先前创建的实例调用许多方法。样本的代码文件包含在example05_02.java文件的java_9_oop_chapter_05_01文件夹中。

vector4.negate();
System.out.println(vector4);
vector3.add(vector4);
System.out.println(vector3);
vector4.absolute();
System.out.println(vector4);
vector5.sub(vector4);
System.out.println(vector5);

三个vector4字段(xyz的初始值为5.0。对vector4.negate方法的调用将三个字段的值更改为-5.0

三个vector3字段(xyz的初始值为0.0。对vector3.add方法的调用通过vector3vector4的每个分量之和的结果改变三个字段的值。因此,对象状态发生如下变化:

  • vector3.x0.0突变为0.0+(-5.0)=-5.0
  • vector3.y0.0突变为0.0+(-5.0)=-5.0
  • vector3.z0.0突变为0.0+(-5.0)=-5.0

vector3字段的三个字段在调用add方法后设置为-5.0。对vector4.absolute方法的调用将三个字段的值从-5.0更改为5.0

字段的初始值为x-15.5y-11.1、和z-8.8。对vector5.sub方法的调用通过减去vector5vector4的每个分量的结果来改变三个字段的值。因此,对象状态发生如下变化:

  • vector5.x-15.5突变到*-15.5-5.0=-20.5*
  • vector5.y-11.1突变到*-11.1-5.0=-16.1*
  • vector5.z-8.8突变到*-8.8-5.0=-13.8*

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Working with mutable objects in JShell

在 Java 9 中构建不可变类

到目前为止,我们一直在处理可变类和变异对象。每当我们公开可变字段时,我们都会创建一个类来生成可变实例。在某些情况下,我们可能更喜欢在初始化后无法更改其状态的对象。我们可以将类设计为不可变的,并生成不可变的实例,这些实例在创建和初始化后无法更改其状态。

当我们处理并发代码时,不可变对象非常有用的一个典型场景。无法更改其状态的对象解决了许多典型的并发问题,并避免了可能难以检测和解决的潜在错误。由于不可变对象无法更改其状态,因此当许多不同的线程在没有适当的同步机制的情况下对其进行修改时,不可能最终导致对象的状态已损坏或不一致。

不可变对象也称为非变异对象。

我们将创建先前编码的Vector3d类的不可变版本,以表示不可变的 3D 向量。这样,我们将注意到可变类与其不可变版本之间的区别。不可变 3D 矢量必须提供以下功能:

  • 三个类型为doublexyz的不可变实例字段。初始化或构造实例后,无法更改这些字段的值。
  • 通过为xyz不可变字段提供初始值来创建实例的构造函数。
  • 创建实例的构造函数,其所有值都设置为0,即x = 0y = 0z = 0
  • 一种构造函数,它创建一个实例,其中所有值都初始化为一个公共值。例如,如果我们指定3.0作为公共值,则构造函数必须生成一个具有x = 3.0y = 3.0z = 3.0的不可变实例。
  • 一种absolute方法,返回一个新实例,其中新不可变 3D 向量的每个分量都设置为我们调用该方法的实例的每个分量的绝对值。
  • 一种negate方法,返回一个新实例,其中新的不可变 3D 向量的每个分量都设置为我们调用该方法的实例的每个分量的求反值。
  • 一种add方法,它返回一个新实例,其中新不可变 3D 向量的每个分量都设置为实例的每个分量之和,在该实例中,我们调用该方法并将不可变 3D 向量的每个分量作为参数接收。
  • 一种sub方法,返回一个新实例,其中新的不可变 3D 向量的每个分量都设置为实例的每个分量的减法,在该方法中,我们调用该方法,并将不可变 3D 向量的每个分量作为参数接收。
  • toString方法的一种实现,该方法打印 3D 向量的三个分量的值:xyz

以下几行声明了表示 Java 中 3D 向量的不可变版本的ImmutableVector3d类。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_03.java文件中。

public class ImmutableVector3d {
    public final double x;
    public final double y;
    public final double z;

 ImmutableVector3d(double x, double y, double z) {
 this.x = x;
 this.y = y;
 this.z = z;
 }

 ImmutableVector3d(double valueForXYZ) {
 this(valueForXYZ, valueForXYZ, valueForXYZ);
 }

 ImmutableVector3d() {
 this(0.0);
 }

    public ImmutableVector3d absolute() {
        return new ImmutableVector3d(
            Math.abs(x),
            Math.abs(y),
            Math.abs(z));
    }

    public ImmutableVector3d negate() {
        return new ImmutableVector3d(
            -x,
            -y,
            -z);
    }

    public ImmutableVector3d add(ImmutableVector3d vector) {
        return new ImmutableVector3d(
            x + vector.x,
            y + vector.y,
            z + vector.z);
    }

    public ImmutableVector3d sub(ImmutableVector3d vector) {
        return new ImmutableVector3d(
            x - vector.x,
            y - vector.y,
            z - vector.z);
    }

    public String toString() {
        return String.format(
            "(x: %.2f, y: %.2f, z: %.2f)",
            x,
            y,
            z);
    }
}

新的ImmutableVector3d类使用final关键字声明了三个不可变的实例字段:xyz。在前面的代码清单中,突出显示了为该类声明的三个构造函数的行。这些构造函数的代码与我们为Vector3d类分析的代码相同。唯一的区别在于执行,因为构造函数正在初始化不可变的实例字段,这些字段在初始化后不会更改其值。

无论何时调用absolutenegateaddsub方法,它们的代码都会返回一个ImmutableVector3d类的新实例,并给出每个操作的结果。我们永远不会改变我们的情况;也就是说,我们不会更改对象的状态。

在 JShell 中使用不可变对象

以下几行创建了一个名为vector10的新ImmutableVector3d实例,其中xyz的初始值分别为100.0200.0300.0。第二行创建了一个名为vector20的新ImmutableVector3d实例,其中11.012.013.0用于初始值xyz。然后,代码使用vector10调用System.out.println方法,然后使用vector20作为参数。对println方法的两次调用都将对每个ImmutableVector3d实例执行toString方法,以显示不可变 3D 向量的String表示。然后,代码以vector20为参数调用vector10add方法,并将返回的ImmutableVector3d实例保存在vector30中。

最后一行调用以vector30为参数的println方法,打印此实例中的xyz 的值,该实例的结果为vector10vector20之间的加法运算。在声明ImmutableVector3d类的代码后面输入行。样本的代码文件包含在example05_03.java文件的java_9_oop_chapter_05_01文件夹中。

ImmutableVector3d vector10 = 
    new ImmutableVector3d(100.0, 200.0, 300.0);
ImmutableVector3d vector20 = 
    new ImmutableVector3d(11.0, 12.0, 13.0);
System.out.println(vector10);
System.out.println(vector20);
ImmutableVector3d vector30 = vector10.add(vector20);
System.out.println(vector30);

下面的屏幕截图显示了在 JShell 中执行上一个代码的结果:

Working with immutable objects in JShell

作为add方法的结果,我们有另一个名为vector30的不可变实例,其字段值为x111.0y212.0z313.0。作为调用每个计算操作的方法的结果,我们将有另一个不可变的实例。

以下几行使用三个可用构造函数创建名为vector40vector50vector60ImmutableVector3d类的三个实例。然后,接下来的几行调用System.out.println方法,在创建对象后打印xyz的值。样本的代码文件包含在example05_03.java文件的java_9_oop_chapter_05_01文件夹中。

ImmutableVector3d vector40 = 
    new ImmutableVector3d();
ImmutableVector3d vector50 = 
    new ImmutableVector3d(-5.0);
ImmutableVector3d vector60 = 
    new ImmutableVector3d(8.0, 9.0, 10.0);
System.out.println(vector40);
System.out.println(vector50);
System.out.println(vector60);

下面的屏幕截图显示了在 JShell 中执行上一个代码的结果:

Working with immutable objects in JShell

接下来的几行为先前创建的实例调用许多方法,并生成ImmutableVector3d类的新实例。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_03.java文件中。

ImmutableVector3d vector70 = vector50.negate();
System.out.println(vector70);
ImmutableVector3d vector80 = vector40.add(vector70);
System.out.println(vector80);
ImmutableVector3d vector90 = vector70.absolute();
System.out.println(vector90);
ImmutableVector3d vector100 = vector60.sub(vector90);
System.out.println(vector100);

三个vector50字段(xyz的初始值为-5.0。对vector50.negate方法的调用返回一个新的ImmutableVector3d实例,代码保存在vector70中。新实例将5.0作为三个字段(xyz的值。

三个vector40字段(xyz的初始值为0。对vector40.add方法的调用,以vector70为参数,返回一个新的ImmutableVector3d实例,代码保存在vector80中。新实例将5.0作为三个字段(xyz的值。

vector70.absolute方法的调用返回一个新的ImmutableVector3d实例,代码保存在vector90中。新实例的三个字段(xyz的值为5.0。字段的绝对值与原始值相同,但代码仍然生成了一个新实例。

vector60字段的初始值为x8.0y9.0z10.0。以vector90作为参数调用vector60.sub方法返回一个新的ImmutableVector3d实例,代码保存在vector100中。vector100字段的值为x3.0y4.0z5.0

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Working with immutable objects in JShell

了解突变对象和非突变对象之间的差异

与可变版本相比,不可变版本增加了开销,因为调用absolutenegateaddsub方法需要创建类的新实例。前面分析的名为Vector3D的可变类只是更改了字段的值,不需要生成新实例。因此,不可变版本的内存占用高于可变版本。

与可变版本相比,名为ImmutableVector3d的不可变类具有内存和性能开销。创建新实例比更改几个字段的值更昂贵。然而,正如前面所解释的,当我们处理并发代码时,为避免可变对象引起的潜在问题而支付额外的开销是有意义的。我们只需要确保分析优势和权衡,以决定哪种方法是编写特定类的最方便的方法。

现在,我们将编写几行代码来处理可变版本,并为不可变版本生成等效代码。通过这种方式,我们将能够对这两段代码之间的差异进行简单而直观的比较。

以下几行创建了一个名为mutableVector3d1的新Vector3d实例,其中xyz的初始值分别为-30.5-15.5-12.5。然后,代码打印新实例的String表示,调用absolute方法,并打印变异对象的String表示。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_04.java文件中。

// Mutable version
Vector3d mutableVector3d1 = 
    new Vector3d(-30.5, -15.5, -12.5);
System.out.println(mutableVector3d1);
mutableVector3d1.absolute();
System.out.println(mutableVector3d1);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Understanding the differences between mutating and non-mutating objects

以下行创建一个名为immutableVector3d1的新ImmutableVector3d实例,其中xyz的初始值分别为-30.5-15.5-12.5。然后,代码打印新实例的String表示,调用生成名为immutableVector3d2的新ImmutableVector3d实例的absolute方法,并打印新对象的String表示。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_04.java文件中。

// Immutable version
ImmutableVector3d immutableVector3d1 = 
    new ImmutableVector3d(-30.5, -15.5, -12.5);
System.out.println(immutableVector3d1);
ImmutableVector3d immutableVector3d2 =
    immutableVector3d1.absolute();
System.out.println(immutableVector3d2);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Understanding the differences between mutating and non-mutating objects

可变的版本可用于单个Vector3d实例。Vector3d类的构造函数只执行一次。当我们调用absolute方法时,原始实例的状态发生了变化。

不可变版本使用两个ImmutableVector3d实例,因此,内存占用比可变版本高。ImmutableVector3d类的构造函数执行两次。当我们调用absolute方法时,第一个实例没有改变它的状态。

编写并发代码时学习非变异对象的优势

现在,让我们假设我们正在编写并发代码,该代码必须访问先前创建的实例的字段。首先,我们将分析可变版本的问题,然后我们将了解使用非可变对象的优势。

假设我们有两个线程,其中代码引用了保存在mutableVector3d1中的实例。第一个线程为这个变异对象调用absolute方法。absolute方法的第一行代码将Math.abs的结果和x的实际值作为参数分配给x可变字段。

此时,方法没有完成其执行,下一行代码将无法访问这些值。但是,在另一个线程中运行的引用了此实例的并发代码可能会在absolute方法完成执行之前访问xyz字段的值。对象处于损坏状态,因为字段的值分别为x30.5y-15.5z-12.5。这些值并不代表当absolute方法完成其执行时我们将拥有的 3D 向量。有许多代码同时运行,并且在没有任何同步机制的情况下可以访问同一个实例,这就产生了问题。

并发编程和线程编程是复杂的主题,值得一本完整的书。有一些同步机制可以避免前面提到的问题,并使类线程安全。然而,另一个解决方案是使用不可变类来生成非变异对象。

如果我们使用不可变版本,两个线程可以引用同一个初始实例。但是,当其中一个线程调用absolute方法时,原始 3D 向量不会发生变化,因此前面的问题永远不会发生。另一个线程将继续使用其对原始 3D 向量的引用及其原始状态。调用absolute方法的线程将生成一个完全独立于原始实例的新实例。

同样,理解这个主题值得一本完整的书是非常重要的。然而,重要的是要理解为什么不可变类在实例将参与并发代码的特定场景中可能是一个特殊需求。

处理不可变字符串类的实例

String类,特别是类java.lang.String类,表示字符串,是一个不可变的类,生成非变异对象。因此,String类提供的方法不会改变对象。

例如,以下几行创建了一个新的String,即名为welcomeMessagejava.lang.String类的一个新实例,初始值为"Welcome to Virtual Creatures Land"。然后,代码多次调用System.out.println,其中welcomeMessage后跟一个不同的方法作为参数。首先,我们调用toUpperCase方法生成一个新的String,其中所有字符都转换为大写。然后,我们调用toLowerCase方法生成一个新的String,将所有字符转换为小写。然后,我们调用replaceAll方法生成一个新的String,其中空格被连字符(-替换。最后,我们再次调用System.out.println方法,将welcomeMessage作为参数来检查原始String的值。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_05.java文件中。

String welcomeMessage = "Welcome to Virtual Creatures Land";
System.out.println(welcomeMessage);
System.out.println(welcomeMessage.toUpperCase());
System.out.println(welcomeMessage.toLowerCase());
System.out.println(welcomeMessage.replaceAll(" ", "-"));
System.out.println(welcomeMessage);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Working with instances of the immutable String class

welcomeMessage字符串从未更改其值。对toUpperCasetoLowerCasereplaceAll方法的调用分别为它们生成并返回了一个新的String实例。

提示

无论我们为String实例调用哪种方法,它都不会改变对象。因此,我们可以说String是一个不可变的类。

创建现有可变类的不可变版本

在上一章中,我们创建了一个名为VirtualCreature的可变类。我们提供了 setter 方法来更改hatvisibilityLevelbirthYear字段的值。我们可以通过调用setAge方法来更改birthYear

虚拟生物在进化后会改变它们的年龄、帽子和可见度。当它们进化时,它们会变成一个不同的生物,因此,在进化发生后生成一个新实例是有意义的。因此,我们将创建VirtualCreature类的不可变版本,并将其命名为ImmutableVirtualCreature

以下几行显示了新的ImmutableVirtualCreature类的代码。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_06.java文件中。

import java.time.Year;

public class ImmutableVirtualCreature {
 public final String name;
 public final int birthYear;
 public final String hat;
 public final int visibilityLevel;

    ImmutableVirtualCreature(final String name, 
        int birthYear, 
        String hat, 
        int visibilityLevel) {
        this.name = name;
        this.birthYear = birthYear;
        this.hat = hat.toUpperCase();
        this.visibilityLevel = 
            getValidVisibilityLevel(visibilityLevel);
    }

    private int getCurrentYear() {
        return Year.now().getValue();
    }

    private int getValidVisibilityLevel(int levelToValidate) {
        return Math.min(Math.max(levelToValidate, 0), 100);
    }

    public int getAge() {
        return getCurrentYear() - birthYear;
    }

    public ImmutableVirtualCreature evolveToAge(int age) {
        int newBirthYear = getCurrentYear() - age;
        return new ImmutableVirtualCreature(
            name,
            newBirthYear,
            hat,
            visibilityLevel);
    }

    public ImmutableVirtualCreature evolveToVisibilityLevel(
        final int visibilityLevel) {
        int newVisibilityLevel =
            getValidVisibilityLevel(visibilityLevel);
        return new ImmutableVirtualCreature(
            name,
            birthYear,
            hat,
            newVisibilityLevel);
    }
}

ImmutableVirtualCreature类用final关键字namebirthYearhatvisibilityLevel声明了四个公共不可变实例字段。在初始化或构造实例后,我们将无法更改这些字段的值。

构造函数根据hat参数中接收到的String生成大写的String,并将其存储在公共hat不可变字段中。我们对可见性级别进行了特定的验证,因此,构造函数使用在visibilityLevel参数中接收到的值调用名为getValidVisibilityLevel,的新私有方法,以将有效值分配给同名的不可变字段。

我们不再有 setter 方法,因为我们无法在不可变字段初始化后更改其值。该类声明了以下两个新的公共方法,它们返回一个新ImmutableVirtualCreature实例:

  • evolveToAge:此方法在age参数中接收进化虚拟生物的期望年龄。代码根据收到的年龄和当前年份计算出生年份,并返回一个新的带有新初始化值的ImmutableVirtualCreature实例。
  • evolveToVisibilityLevel:此方法在visibilityLevel参数中接收进化虚拟生物所需的可见性级别。代码调用getValidVisibilityLevel方法,根据接收到的值生成有效的可见性级别,并返回一个新的ImmutableVirtualCreature实例和新的初始化值。

以下几行创建了名为meowth1ImmutableVirtualCreature类的实例。然后代码调用meowth1.evolveToAge方法,将3作为age参数的值,并将此方法返回的新ImmutableVirtualCreature实例保存在meowth2变量中。代码打印meowth2.getAge方法返回的值。最后,代码调用以25作为invisibilityLevel参数值的meowth2.evolveToVisibilityLevel方法,并将此方法返回的新ImmutableVirtualCreature实例保存在meowth3变量中。然后,代码打印存储在meowth3.visibilityLevel不可变字段中的值。样本的代码文件包含在java_9_oop_chapter_05_01文件夹中的example05_06.java文件中。

ImmutableVirtualCreature meowth1 =
    new ImmutableVirtualCreature(
        "Meowth", 2010, "Baseball cap", 35);
ImmutableVirtualCreature meowth2 = 
    meowth1.evolveToAge(3);
System.out.printf("%d\n", meowth2.getAge());
ImmutableVirtualCreature meowth3 = 
    meowth2.evolveToVisibilityLevel(25);
System.out.printf("%d\n", meowth3.visibilityLevel);

以下屏幕截图显示了在 JShell 中执行前面代码的结果:

Creating the immutable version of an existing mutable class

测试你的知识

  1. 公开可变字段的类将:
    1. 生成不可变的实例。
    2. 生成可变实例。
    3. 生成可变类但不可变实例。
  2. 在构造函数中使用的下列哪个关键字允许我们使用类中定义的不同参数调用其他构造函数:
    1. self
    2. constructor
    3. this
  3. 初始化后无法更改其状态的对象称为:
    1. 可变对象。
    2. 不可变对象。
    3. 接口对象。
  4. 在 Java 9 中,java.lang.String生成:
    1. 不可变对象。
    2. 可变对象。
    3. 接口对象。
  5. 如果我们为java.lang.String调用toUpperCase方法,该方法将:
    1. 将现有的String转换为大写字符并更改其状态。
    2. 返回一个新的String,将原始String的内容转换为大写字符。
    3. 返回一个新的String,其中包含原始String的内容。

总结

在本章中,您了解了可变类和不可变类之间的区别,以及它们生成的变异实例和非变异实例之间的区别。我们在 Java9 中声明了 3D 向量类的可变和不可变版本。

然后,我们利用 JShell 轻松地处理这些类的变异和非变异实例,并分析了更改对象状态和在需要更改其状态时返回新对象之间的区别。我们分析了可变类和不可变类的优缺点,并理解了为什么后者在处理并发代码时很有用。

现在,您已经了解了可变类和不可变类,您已经准备好使用继承、抽象、扩展和特化,这是我们将在下一章中讨论的主题。