Skip to content

Files

Latest commit

8814ef5 · Oct 11, 2021

History

History
1057 lines (850 loc) · 55.2 KB

File metadata and controls

1057 lines (850 loc) · 55.2 KB

十三、Java 9 中的模块化

在本章中,我们将利用 Java9 中添加的一个新特性,使我们能够模块化源代码并轻松管理依赖关系。我们将:

  • 重构现有代码以利用面向对象编程
  • 用 Java9 中的新模块化组织面向对象代码
  • 在 Java9 中创建模块化源代码
  • 使用 Java 9 编译器编译多个模块
  • 使用 Java9 运行模块化代码

重构现有代码以利用面向对象编程

如果我们从头开始编写面向对象的代码,我们就可以充分利用我们在前几章学到的一切以及 Java9 中包含的所有功能。随着需求的发展,我们必须对接口和类进行更改,以进一步概括或特化它们,编辑它们,并创建新的接口和类。我们以面向对象的方法开始我们的项目,这一事实将使我们很容易对代码进行必要的调整。

有时候,我们非常幸运,一开始我们就有机会遵循最佳实践。然而,很多时候我们并没有这么幸运,我们不得不从事那些没有遵循最佳实践的项目。在这些情况下,我们可以使用我们最喜欢的 IDE 和其他辅助工具提供的功能重构现有代码,并生成面向对象的代码,从而促进代码重用并减少维护方面的麻烦,而不是遵循生成容易出错、重复且难以维护的代码的同样的错误做法。

例如,假设我们必须开发一个 Web 服务,它允许我们使用 3D 模型,并以特定的分辨率在 2D 图像上渲染它们。这些要求规定,我们必须使用 Web 服务渲染的前两个 3D 模型是球体和立方体。Web 服务必须允许我们更改透视摄影机的以下参数,以允许我们查看在 2D 屏幕上渲染的 3D 世界的特定部分:

  • 位置(XYZ值)
  • 方向(XYZ值)
  • 上方向向量(XYZ值)
  • 透视视野(度)
  • 近剪裁平面
  • 远剪裁平面

假设其他开发人员开始处理该项目,并使用声明两个静态方法的类包装器生成一个 Java 文件。其中一种方法渲染立方体,另一种方法渲染球体。这些方法接收渲染每个三维图形所需的所有参数,包括确定三维图形位置和大小所需的所有参数,以及配置透视摄影机和平行光。

下面几行显示了一个名为Renderer的类的声明示例,该类有两个静态方法:renderCuberenderSphere。第一个设置并渲染立方体,第二个设置并渲染球体。理解示例代码没有遵循最佳实践非常重要,我们将对其进行重构。考虑到这两个静态方法有很多共同的代码。样本的代码文件包含在example13_01.java文件的java_9_oop_chapter_13_01文件夹中。

// The following code doesn't follow best practices
// Please, do not use this code as a baseline
// We will refactor it to generate object-oriented code
public class Renderer {
    public static void renderCube(int x, int y, int z, int edgeLength,
        int cameraX, int cameraY, int cameraZ,
        int cameraDirectionX, int cameraDirectionY, int cameraDirectionZ,
        int cameraVectorX, int cameraVectorY, int cameraVectorZ,
        int cameraPerspectiveFieldOfView,
        int cameraNearClippingPlane,
        int cameraFarClippingPlane,
        int directionalLightX, int directionalLightY, int directionalLightZ,
        String directionalLightColor) {
            System.out.println(
                String.format("Created camera at (x:%d, y:%d, z:%d)",
                    cameraX, cameraY, cameraZ));
            System.out.println(
                String.format("Set camera direction to (x:%d, y:%d, z:%d)",
                    cameraDirectionX, cameraDirectionY, cameraDirectionZ));
            System.out.println(
                String.format("Set camera vector to (x:%d, y:%d, z:%d)",
                    cameraVectorX, cameraVectorY, cameraVectorZ));
            System.out.println(
                String.format("Set camera perspective field of view to: %d",
                    cameraPerspectiveFieldOfView));
            System.out.println(
                String.format("Set camera near clipping plane to: %d", 
                    cameraNearClippingPlane));
            System.out.println(
                String.format("Set camera far clipping plane to: %d",
                    cameraFarClippingPlane));
            System.out.println(
                String.format("Created directional light at (x:%d, y:%d, z:%d)",
                    directionalLightX, directionalLightY, directionalLightZ));
            System.out.println(
                String.format("Set light color to %s",
                    directionalLightColor));
            System.out.println(
                String.format("Drew cube at (x:%d, y:%d, z:%d) with edge length equal to %d" +
                    "considering light at (x:%d, y:%d, z:%d) " +
                    "and light's color equal to %s", 
                    x, y, z, edgeLength,
                    directionalLightX, directionalLightY, directionalLightZ,
                    directionalLightColor));
    }

    public static void renderSphere(int x, int y, int z, int radius,
        int cameraX, int cameraY, int cameraZ,
        int cameraDirectionX, int cameraDirectionY, 
        int cameraDirectionZ,
        int cameraVectorX, int cameraVectorY, int cameraVectorZ,
        int cameraPerspectiveFieldOfView,
        int cameraNearClippingPlane,
        int cameraFarClippingPlane,
        int directionalLightX, int directionalLightY, 
        int directionalLightZ,
        String directionalLightColor) {
            System.out.println(
                String.format("Created camera at (x:%d, y:%d, z:%d)",
                    cameraX, cameraY, cameraZ));
            System.out.println(
                String.format("Set camera direction to (x:%d, y:%d, z:%d)",
                    cameraDirectionX, cameraDirectionY, cameraDirectionZ));
            System.out.println(
                String.format("Set camera vector to (x:%d, y:%d, z:%d)",
                    cameraVectorX, cameraVectorY, cameraVectorZ));
            System.out.println(
                String.format("Set camera perspective field of view to: %d",
                    cameraPerspectiveFieldOfView));
            System.out.println(
                String.format("Set camera near clipping plane to: %d", 
                    cameraNearClippingPlane));
            System.out.println(
                String.format("Set camera far clipping plane to: %d",
                    cameraFarClippingPlane));
            System.out.println(
                String.format("Created directional light at (x:%d, y:%d, z:%d)",
                    directionalLightX, directionalLightY, directionalLightZ));
            System.out.println(
                String.format("Set light color to %s",
                    directionalLightColor));
            // Render the sphere
            System.out.println(
                String.format("Drew sphere at (x:%d, y:%d z:%d) with radius equal to %d",
                    x, y, z, radius));
            System.out.println(
                String.format("considering light at (x:%d, y:%d, z:%d)",
                    directionalLightX, directionalLightY, directionalLightZ));
            System.out.println(
                String.format("and the light's color equal to %s",
                    directionalLightColor));
    }
}

每个静态方法都需要大量的参数。现在,让我们假设我们对 Web 服务有了新的需求。我们必须添加代码来渲染其他形状,并添加不同类型的相机和灯光。此外,我们必须在一个物联网物联网项目中工作,在这个项目中,我们必须在计算机视觉应用程序中重用形状,因此,我们希望利用我们的 Web 服务代码,并与这个新项目共享代码库。此外,我们还必须致力于另一个项目,该项目将在功能强大的物联网板上运行,特别是 Intel Joule 系列的一个成员,该系列将运行渲染服务,并将使用其 4K 视频输出功能来显示生成的图形。我们将使用此板中包含的强大四核 CPU 来运行本地渲染服务,在这种情况下,我们不会调用 Web 服务。

许多应用程序必须共享许多代码,我们的代码必须为新的形状、相机和灯光做好准备。代码很容易变得非常混乱、重复,并且很难维护。当然,前面显示的代码已经很难维护了。因此,我们将重构现有代码,并创建许多接口和类来创建一个面向对象的版本,我们将能够根据新的需求进行扩展,并在不同的应用程序中重用。

到目前为止,我们一直在使用 JShell 来运行我们的代码示例。这次,我们将为每个接口或类创建一个 Java 源代码文件。此外,我们将把这些文件组织到 Java9 中引入的新模块中。最后,我们将编译这些模块并运行一个控制台应用程序。您可以使用您喜爱的编辑器或 IDE 来创建不同的代码文件。请记住,您可以下载指定的代码文件,而不必键入任何代码。

我们将创建以下公共接口、抽象类和具体类:

  • Vector3d:这个具体的类表示一个可变的 3D 向量,其xyzint值。
  • Rendereable:此接口指定了对具有位置且可呈现的元素的要求。
  • SceneElement:这个抽象类实现了Rendereable接口,表示任何有位置且可以呈现的元素。所有场景元素都将从此抽象类继承。
  • Light:这个抽象类继承自SceneElement并表示场景中的灯光,该灯光必须提供其属性的描述。
  • DirectionalLight:该具体类继承自Light并表示具有特定颜色的平行光。
  • Camera:这个抽象类继承自SceneElement,表示场景中的一个摄像头。
  • PerspectiveCamera:这个具体类继承自Camera并表示一个透视摄影机,该摄影机具有方向、上方向向量、视野、近剪裁平面和远剪裁平面。
  • Shape:这个抽象类继承自SceneElement,表示场景中的一个形状,可以使用活动摄影机进行渲染并接收多个灯光。
  • Sphere:这个具体类继承自Shape并表示一个球体。
  • Cube:这个具体类继承自Shape并表示一个立方体。
  • Scene:这个具体类表示具有活动摄影机、形状和灯光的场景。我们可以使用该类的实例来合成场景并进行渲染。
  • Example01:这个具体类将声明一个主静态方法,该方法将使用PerspectiveCameraSphereCubeDirectionalLight创建Scene实例并调用其呈现方法。

我们将在一个扩展名为.java且与我们声明的类型同名的文件中声明前面列举的每个接口、抽象类和具体类。例如,我们将在Vector3d.java文件中声明Vector3d类,也称为 Java 源文件。

提示

在 Java 源文件中声明一个与我们声明的类型同名的公共接口或类是一种很好的实践和常见的约定。当我们在 Java 源文件中声明多个公共类型时,Java 编译器将生成一个错误。

用 Java 9 中的新模块化组织面向对象代码

当我们只有几个接口和类时,数百行面向对象代码就很容易组织和维护。但是,随着代码类型和行数的增加,有必要遵循一些规则来组织代码并使其易于维护。

如果没有有效的组织方式,一个编写良好的面向对象代码可能会引起维护方面的麻烦。我们不必忘记,编写良好的面向对象代码可以促进代码重用。

在我们的示例中,我们只有几个接口、抽象类和具体类。然而,我们必须想象,我们将有大量的额外类型来支持额外的需求。因此,我们最终将得到数十个与渲染构成场景的元素所需的数学运算相关的类、其他类型的灯光、新类型的摄影机、与这些新灯光和摄影机相关的类,以及数十个其他形状及其相关类。

我们将创建许多模块,以允许我们创建具有名称的软件单元,需要其他模块,并导出可供其他模块使用和访问的公共 API。当一个模块需要其他模块时,这意味着该模块依赖于列为需求的模块。每个模块的名称将遵循我们通常用于 Java 包的相同约定。

提示

其他模块只能访问模块导出的公共类型。如果我们在模块内声明了一个公共类型,但没有将其包含在导出的 API 中,那么我们将无法在模块外访问它。在创建模块依赖项时,我们必须避免循环依赖项。

我们将创建以下八个模块:

  • com.renderer.math
  • com.renderer.sceneelements
  • com.renderer.lights
  • com.renderer.cameras
  • com.renderer.shapes
  • com.renderer.shapes.curvededges
  • com.renderer.shapes.polyhedrons
  • com.renderer

现在,每当我们需要使用灯光时,我们都将探索在com.renderer.lights模块中声明的类型。每当我们需要处理具有弯曲边缘的 3D 形状时,我们都将探索com.renderer.shapes.curvededges模块中声明的类型。

每个模块将在一个与模块名同名的包中声明类和接口。例如,com.renderer.cameras模块将在com.renderer.cameras包中声明类。是一组相关类型。每个包生成一个声明范围的命名空间。因此,我们将结合模块使用包。

下表总结了我们将创建的模块,以及我们将在每个模块中声明的接口、抽象类和具体接口。此外,该表还指定了每个模块所需的模块列表。

|

模块名

|

声明的公共类型

|

模块要求

| | --- | --- | --- | | com.renderer.math | Vector3d | - | | com.renderer.sceneelements | Rendereable``SceneElement | com.renderer.math | | com.renderer.lights | Light``DirectionalLight | com.renderer.math``com.renderer.sceneelements | | com.renderer.cameras | Camera``PerspectiveCamera | com.renderer.math``com.renderer.sceneelements | | com.renderer.shapes | Shape | com.renderer.math``com.renderer.sceneelements``com.renderer.lights``com.renderer.cameras | | com.renderer.shapes.curvededges | Sphere | com.renderer.math``com.renderer.lights``co``m.renderer.shapes | | com.renderer.shapes.polyhedrons | Cube | com.renderer.math``com.renderer.lights``com.renderer.shapes | | com.renderer | Scene``Example01 | com.renderer.math``com.renderer.cameras``com.renderer.lights``com.renderer.shapes``com.renderer.shapes.curvededges``com.renderer.shapes.polyhedrons |

非常重要的是要注意,所有的模块还需要导出所有平台核心包的java.base模块,如java.iojava.langjava.mathjava.netjava.util等。但是,我们需要隐式地在 T6 中声明每个模块,因此不需要在 T6 中指定它们的依赖项。

下一个图显示了模块图,其中模块是节点,一个模块对另一个模块的依赖关系是有向边。模块图中不包括java.lang

Organizing object-oriented code with the new modularity in Java 9

我们不会使用任何特定的 IDE 来创建所有的模块。这样,我们将了解目录结构和所有必需的文件。然后,我们可以利用我们喜欢的 IDE 中包含的特性,轻松创建新模块及其必要的目录结构。

有一个约定指定模块的源代码必须位于与模块名同名的目录中。例如,名为com.renderer.math的模块必须位于名为com.renderer.math的目录中。我们必须为每个所需模块创建一个模块描述符,即在模块的根文件夹中创建一个名为module-info.java的源代码文件。此文件指定模块名称、所需模块以及模块导出的包。导出的包将由需要该模块的模块可见。

然后,需要为模块名称中用点(.分隔的每个名称创建子目录。例如,我们将在com.renderer.math目录中创建com/renderer/math目录(Windows 中的com\renderer\math子文件夹)。声明每个模块的接口、抽象类和具体类的 Java 源文件将位于这些子文件夹中。

我们将创建一个名为Renderer的基本目录,其中包含一个名为src的子文件夹,该子文件夹将包含我们模块的所有源代码。因此,我们将使用Renderer/srcRenderer\src在 Windows 中)作为源代码的基本目录。然后,我们将使用module-info.java文件为每个模块创建一个文件夹,并使用 Java 源代码文件创建子文件夹。以下目录结构显示了我们将在Renderer/srcRenderer\src在 Windows 中)目录中拥有的最终内容。文件名将高亮显示。

├───com.renderer
│   │   module-info.java
│   │
│   └───com
│       └───rendererExample01.javaScene.java
│
├───com.renderer.cameras
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───camerasCamera.javaPerspectiveCamera.java
│
├───com.renderer.lights
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───lightsDirectionalLight.javaLight.java
│
├───com.renderer.math
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───mathVector3d.java
│
├───com.renderer.sceneelements
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───sceneelementsRendereable.javaSceneElement.java
│
├───com.renderer.shapes
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───shapesShape.java
│
├───com.renderer.shapes.curvededges
│   │   module-info.java
│   │
│   └───com
│       └───renderer
│           └───shapes
│               └───curvededgesSphere.java
│
└───com.renderer.shapes.polyhedronsmodule-info.java
    │
    └───com
        └───renderer
            └───shapes
                └───polyhedrons
 Cube.java

创建模块化源代码

现在是开始创建必要的目录结构的时候了,为module-info.java文件和每个模块的源 Java 文件编写代码。我们将创建com.renderer.math模块。

创建名为Renderer的目录和src子目录。我们将使用Renderer/srcRenderer\src在 Windows 中)作为源代码的基本目录。但是,要考虑到,在下载源代码的情况下,不需要创建任何文件夹。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.math目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.math的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.math子文件夹中。

module com.renderer.math {
    exports com.renderer.math;
}

module关键字后跟模块名称com.renderer.math开始模块声明。大括号中包含的线指定模块主体。exports关键字后跟包名com.renderer.math表示此模块导出com.renderer.math包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/mathcom\renderer\math窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Vector3d.java的文件中。下一行将公开的Vector3d混凝土类声明为com.renderer.math包的成员。我们将使用Vector3d类,而不是使用xyz的单独值。package关键字后跟包名表示类将包含在其中的包。

样本的代码文件包含在Vector3d.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.math/com/renderer/math子文件夹中。

package com.renderer.math;

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

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

    public Vector3d(int valueForXYZ) {
        this(valueForXYZ, valueForXYZ, valueForXYZ);
    }

    public Vector3d() {
        this(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: %d, y: %d, z: %d)",
            x,
            y,
            z);
    }
}

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.sceneelements目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。接下来的几行组成了名为com.renderer.sceneelements的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.sceneelements子文件夹中。

module com.renderer.sceneelements {
    requires com.renderer.math;
    exports com.renderer.sceneelements;
}

module关键字后跟模块名称com.renderer.sceneelements开始模块声明。大括号中包含的线指定模块主体。requires关键字后跟模块名称com.renderer.math,表示此模块需要在先前声明的com.renderer.math模块中导出的类型。exports关键字后跟包名com.renderer.sceneelements,表示此模块导出com.renderer.sceneelements包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/sceneelementscom\renderer\sceneelements窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Rendereable.java的文件中。接下来的几行将公共Rendereable接口声明为com.renderer.sceneelements包的成员。样本的代码文件包含在Rendereable.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.sceneeelements/com/renderer/sceneelements子文件夹中。

package com.renderer.sceneelements;

import com.renderer.math.Vector3d;

public interface Rendereable {
    Vector3d getLocation();
    void setLocation(Vector3d newLocation);
    void render();
}

将下面的行添加到最近创建的子文件夹中名为SceneElement.java的文件中。接下来的几行将公共SceneElement抽象类声明为com.renderer.sceneelements包的成员。样本的代码文件包含在SceneElement.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.sceneelements/com/renderer/sceneelements子文件夹中。

package com.renderer.sceneelements;

import com.renderer.math.Vector3d;

public abstract class SceneElement implements Rendereable {
    protected Vector3d location;

    public SceneElement(Vector3d location) {
        this.location = location;
    }

    public Vector3d getLocation() {
        return location;
    }

    public void setLocation(Vector3d newLocation) {
        location = newLocation;
    }
}

SceneElement抽象类实现了前面定义的Rendereable接口。该类表示一个 3D 元素,该元素是场景的一部分,其位置由Vector3d指定。该类是所有需要在三维空间中定位的场景元素的基类。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.lights目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.lights的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.lights子文件夹中。

module com.renderer.lights {
    requires com.renderer.math;
    requires com.renderer.sceneelements;
    exports com.renderer.lights;
}

前面几行声明了com.renderer.lights模块,并指定该模块需要两个模块:com.renderer.mathcom.renderer.sceneelementsexports关键字后跟包名com.renderer.lights,表示此模块导出com.renderer.lights包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/lightscom\renderer\lights窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Light.java的文件中。接下来的几行将 publicLight抽象类声明为com.renderer.lights包的成员。该类继承自SceneElement类,并声明一个抽象getPropertiesDescription方法,该方法必须返回一个String,其中包含灯光具有的所有属性的描述。继承自Light类的具体类将负责提供此方法的实现。样本的代码文件包含在java_9_oop_chapter_13_01/Renderer/src/com.renderer.lights/com/renderer/lights子文件夹中的Light.java文件中。

package com.renderer.lights;

import com.renderer.sceneelements.SceneElement;
import com.renderer.math.Vector3d;

public abstract class Light extends SceneElement {
    public Light(Vector3d location) {
        super(location);
    }

    public abstract String getPropertiesDescription();
}

将以下行添加到最近创建的子文件夹中名为DirectionalLight.java的文件中。接下来的几行将 publicDirectionalLight混凝土类声明为com.renderer.lights包的成员。样本的代码文件包含在DirectionalLight.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.lights/com/renderer/lights子文件夹中。

package com.renderer.lights;

import com.renderer.math.Vector3d;

public class DirectionalLight extends Light {
    public final String color;

    public DirectionalLight(Vector3d location, 
        String color) {
        super(location);
        this.color = color;
    }

    @Override
    public void render() {
        System.out.println(
            String.format("Created directional light at %s",
                location));
        System.out.println(
            String.format("Set light color to %s",
                color));
    }

    @Override
    public String getPropertiesDescription() {
        return String.format(
            "light's color equal to %s",
            color);
    }
}

DirectionalLight具体类继承自先前定义的Light抽象类。DirectionalLight类表示平行光,并为rendergetPropertiesDescription方法提供了一个实现。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.cameras目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.cameras的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.cameras子文件夹中。

module com.renderer.cameras {
    requires com.renderer.math;
    requires com.renderer.sceneelements;
    exports com.renderer.cameras;
}

前面几行声明了com.renderer.cameras模块,并指定该模块需要两个模块:com.renderer.mathcom.renderer.sceneelementsexports关键字后跟包名com.renderer.cameras,表示此模块导出com.renderer.cameras包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/camerascom\renderer\cameras窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Camera.java的文件中。接下来的几行将 publicCamera抽象类声明为com.renderer.cameras包的成员。该类继承自SceneElement类。该类表示三维摄影机。它是所有摄影机的基类。在本例中,类声明为空,我们之所以声明它,是因为我们知道将有多种类型的摄影机。此外,我们希望能够概括未来所有类型相机的通用要求,就像我们对灯光所做的那样。样本的代码文件包含在Camera.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.cameras/com/renderer/cameras子文件夹中。

package com.renderer.cameras;

import com.renderer.math.Vector3d;
import com.renderer.sceneelements.SceneElement;

public abstract class Camera extends SceneElement {
    public Camera(Vector3d location) {
        super(location);
    }
}

将以下行添加到最近创建的子文件夹中名为PerspectiveCamera.java的文件中。接下来的几行将 publicPerspectiveCamera混凝土类声明为com.renderer.cameras包的成员。样本的代码文件包含在PerspectiveCamera.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.cameras/com/renderer/cameras子文件夹中。

package com.renderer.cameras;

import com.renderer.math.Vector3d;

public class PerspectiveCamera extends Camera {
    protected Vector3d direction;
    protected Vector3d vector;
    protected int fieldOfView;
    protected int nearClippingPlane;
    protected int farClippingPlane;

    public Vector3d getDirection() {
        return direction;
    }

    public void setDirection(Vector3d newDirection) {
        direction = newDirection;
    }

    public Vector3d getVector() {
        return vector;
    }

    public void setVector(Vector3d newVector) {
        vector = newVector;
    }

    public int getFieldOfView() {
        return fieldOfView;
    }

    public void setFieldOfView(int newFieldOfView) {
        fieldOfView = newFieldOfView;
    }

    public int nearClippingPlane() {
        return nearClippingPlane;
    }

    public void setNearClippingPlane(int newNearClippingPlane) {
        this.nearClippingPlane = newNearClippingPlane;
    }

    public int farClippingPlane() {
        return farClippingPlane;
    }

    public void setFarClippingPlane(int newFarClippingPlane) {
        this.farClippingPlane = newFarClippingPlane;
    }

    public PerspectiveCamera(Vector3d location, 
        Vector3d direction, 
        Vector3d vector, 
        int fieldOfView, 
        int nearClippingPlane, 
        int farClippingPlane) {
        super(location);
        this.direction = direction;
        this.vector = vector;
        this.fieldOfView = fieldOfView;
        this.nearClippingPlane = nearClippingPlane;
        this.farClippingPlane = farClippingPlane;
    }

    @Override
    public void render() {
        System.out.println(
            String.format("Created camera at %s",
                location));
        System.out.println(
            String.format("Set camera direction to %s",
                direction));
        System.out.println(
            String.format("Set camera vector to %s",
                vector));
        System.out.println(
            String.format("Set camera perspective field of view to: %d",
                fieldOfView));
        System.out.println(
            String.format("Set camera near clipping plane to: %d", 
                nearClippingPlane));
        System.out.println(
            String.format("Set camera far clipping plane to: %d",
                farClippingPlane));
    }
}

PerspectiveCamera具体类继承了前面定义的Camera抽象类的。PerspectiveCamera类表示透视摄影机,其中包含许多用于摄影机属性的 getter 和 setter 方法。该类为render方法提供了一个实现,该方法显示所创建摄影机的所有详细信息及其不同属性的值。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.shapes目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.shapes的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.shapes子文件夹中。

module com.renderer.shapes {
    requires com.renderer.math;
    requires com.renderer.sceneelements;
    requires com.renderer.lights;
    requires com.renderer.cameras;
    exports com.renderer.shapes;
}

前几行声明了com.renderer.shapes模块,并指定该模块需要四个模块:com.renderer.mathcom.renderer.sceneelementscom.renderer.lightscom.renderer.camerasexports关键字后跟包名com.renderer.shapes,表示此模块导出com.renderer.shapes包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/shapescom\renderer\shapes窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Shape.java的文件中。接下来的几行将 publicShape抽象类声明为com.renderer.shapes包的成员。样本的代码文件包含在Shape.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.shapes/com/renderer/shapes子文件夹中。

package com.renderer.shapes;

import com.renderer.math.Vector3d;
import com.renderer.sceneelements.SceneElement;
import com.renderer.lights.Light;
import com.renderer.cameras.Camera;
import java.util.*;
import java.util.stream.Collectors;

public abstract class Shape extends SceneElement {
    protected Camera activeCamera;
    protected List<Light> lights;

    public Shape(Vector3d location) {
        super(location);
        lights = new ArrayList<>();
    }

    public void setActiveCamera(Camera activeCamera) {
        this.activeCamera = activeCamera;
    }

    public void setLights(List<Light> lights) {
        this.lights = lights;
    }

    protected boolean isValidForRender() {
        return !((activeCamera == null) && lights.isEmpty());
    }

    protected String generateConsideringLights() {
        return lights.stream()
            .map(light -> String.format(
                "considering light at %s\nand %s",
                    light.getLocation(), 
                    light.getPropertiesDescription()))
            .collect(Collectors.joining());
    }
}

Shape类继承自SceneElement类。该类表示一个三维形状,是所有三维形状的基类。该类定义了以下方法:

  • setActiveCamera:此公共方法接收Camera实例并保存为活动摄像机。
  • setLights:此公共方法接收List<Light>并将其保存为渲染形状时必须考虑的灯光列表。
  • isValidForRender:此受保护方法返回一个boolean值,指示形状是否有活动摄像头和至少一个灯光。否则,该形状对于渲染无效。
  • generateConsideringLights:此受保护的方法返回一个String,其中包含用于渲染形状的灯光及其位置和属性描述。

Shape类中表示特定 3D 形状的每个子类将为render方法提供一个实现。我们将在另外两个模块中对这些子类进行编码。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.shapes.curvededges目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.curvededges的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.curvededges子文件夹中。

module com.renderer.shapes.curvededges {
    requires com.renderer.math;
    requires com.renderer.lights;
    requires com.renderer.shapes;
    exports com.renderer.shapes.curvededges;
}

前面的行声明了com.renderer.shapes模块,并指定该模块需要三个模块:com.renderer.mathcom.renderer.lightscom.renderer.shapesexports关键字后跟包名com.renderer.shapes.curvededges,表示此模块导出com.renderer.shapes.curvededges包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderer/shapes/curvededgescom\renderer\shapes\curvededges窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Sphere.java的文件中。下一行将公开的Sphere混凝土类声明为com.renderer.shapes.curvededges包的成员。样本的代码文件包含在Sphere.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.shapes.curvededges/com/renderer/shapes/curvededges子文件夹中。

package com.renderer.shapes.curvededges;

import com.renderer.math.Vector3d;
import com.renderer.shapes.Shape;
import com.renderer.lights.Light;

public class Sphere extends Shape {
    protected int radius;

    public Sphere(Vector3d location, int radius) {
        super(location);
        this.radius = radius;
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int newRadius) { 
        radius = newRadius;
    }

    @Override
    public void render() {
        if (!isValidForRender()) {
            System.out.println(
                "Setup wasn't completed to render the sphere.");
            return;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(String.format(
            "Drew sphere at %s with radius equal to %d\n",
            location, 
            radius));
        String consideringLights = 
            generateConsideringLights();
        sb.append(consideringLights);
        System.out.println(sb.toString());
    }
}

Sphere类继承自Shape类,除了指定球体位置的Vector3d实例外,还需要构造函数中的半径值。该类为检查isValidForRender方法返回的值的render方法提供了一个实现。如果该方法返回true,则球体可有效渲染,代码将生成一条消息,其中包含球体半径、位置以及渲染球体时考虑的灯光。代码调用generateConsideringLights方法来构建消息。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer.shapes.polyhedrons目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer.polyhedrons的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.polyhedrons子文件夹中。

module com.renderer.shapes.polyhedrons {
    requires com.renderer.math;
    requires com.renderer.lights;
    requires com.renderer.shapes;
    exports com.renderer.shapes.polyhedrons;
}

前面几行声明了com.renderer.polyhedrons模块,并指定该模块需要三个模块:com.renderer.mathcom.renderer.lightscom.renderer.shapesexports关键字后跟包名com.renderer.shapes.polyhedrons,表示此模块导出com.renderer.shapes.polyhedrons包中声明的所有公共类型。

Renderer/src【Windows 中的Renderer\src中创建com/renderer/shapes/polyhedrons【Windows 中的com\renderer\shapes\polyhedrons文件夹)。将以下行添加到最近创建的子文件夹中名为Cube.java的文件中。下一行将公开的Cube混凝土类声明为com.renderer.shapes.polyhedrons包的成员。样本的代码文件包含在Cube.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer.shapes.polyhedrons/com/renderer/shapes/polyhedrons子文件夹中。

package com.renderer.shapes.polyhedrons;

import com.renderer.math.Vector3d;
import com.renderer.shapes.Shape;
import com.renderer.lights.Light;
import java.util.stream.Collectors;

public class Cube extends Shape {
    protected int edgeLength;

    public Cube(Vector3d location, int edgeLength) {
        super(location);
        this.edgeLength = edgeLength;
    }

    public int getEdgeLength() {
        return edgeLength;
    }

    public void setEdgeLength(int newEdgeLength) { 
        edgeLength = newEdgeLength;
    }

    @Override
    public void render() {
        if (!isValidForRender()) {
            System.out.println(
                "Setup wasn't completed to render the cube.");
            return;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(String.format(
            "Drew cube at %s with edge length equal to %d\n",
            location,
            edgeLength));
        String consideringLights = 
            generateConsideringLights();
        sb.append(consideringLights);
        System.out.println(sb.toString());
    }
}

Cube类继承自Shape类,除了指定多维数据集位置的Vector3d之外,还需要构造函数中的edgeLength值。该类为检查isValidForRender方法返回的值的render方法提供了一个实现。如果该方法返回true,则立方体可有效渲染,代码将生成一条消息,其中包含立方体的边长度、位置以及渲染立方体时考虑的灯光。代码调用generateConsideringLights方法来构建消息。

现在在Renderer/src(Windows 中的Renderer\src中)创建com.renderer目录。将以下行添加到最近创建的子文件夹中名为module-info.java的文件中。下面几行组成名为com.renderer的模块的模块描述符。样本的代码文件包含在module-info.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer子文件夹中。

module com.renderer {
    exports com.renderer;
    requires com.renderer.math;
    requires com.renderer.cameras;
    requires com.renderer.lights;
    requires com.renderer.shapes;
    requires com.renderer.shapes.curvededges;
    requires com.renderer.shapes.polyhedrons;
}

前几行声明了com.renderer模块,并指定该模块需要六个模块:com.renderer.mathcom.renderer.camerascom.renderer.lightscom.renderer.shapescom.renderer.shapes.curvededgescom.renderer.shapes.polyhedronsexports关键字后跟包名com.renderer,表示此模块导出com.renderer包中声明的所有公共类型。

Renderer/srcRenderer\src窗口)中创建com/renderercom\renderer窗口)文件夹。将以下行添加到最近创建的子文件夹中名为Scene.java的文件中。下一行将公开的Scene混凝土类声明为com.renderer包的成员。样本的代码文件包含在Scene.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer/com/renderer子文件夹中。

package com.renderer;

import com.renderer.math.Vector3d;
import com.renderer.cameras.Camera;
import com.renderer.lights.Light;
import com.renderer.shapes.Shape;
import java.util.*;

public class Scene {
    protected List<Light> lights;
    protected List<Shape> shapes;
    protected Camera activeCamera;

    public Scene(Camera activeCamera) {
        this.activeCamera = activeCamera;
        this.lights = new ArrayList<>();
        this.shapes = new ArrayList<>();
    }

    public void addLight(Light light) {
        this.lights.add(light);
    }

    public void addShape(Shape shape) {
        this.shapes.add(shape);
    }

    public void render() {
        activeCamera.render();
        lights.forEach(Light::render);
        shapes.forEach(shape -> {
            shape.setActiveCamera(activeCamera);
            shape.setLights(lights);
            shape.render();
        });
    }
}

Scene类表示要渲染的场景。该类声明了一个包含一个Camera实例的activateCamera受保护字段。lights受保护字段是Light实例中的List,而shapes受保护字段是构成场景的Shape实例中的ListaddLight方法将作为参数接收的Light实例添加到List<Light>lightsaddShape方法将作为参数接收的Shape实例添加到List<Shape> shapes

render方法调用活动摄影机和所有灯光的渲染方法。然后,代码对每个形状执行以下操作:设置其活动摄影机,设置灯光,并调用render方法。

最后,将以下行添加到最近创建的子文件夹中名为Example01.java的文件中。接下来的几行将 publicExample01混凝土类声明为com.renderer包的成员。样本的代码文件包含在Example01.java文件的java_9_oop_chapter_13_01/Renderer/src/com.renderer/com/renderer子文件夹中。

package com.renderer;

import com.renderer.math.Vector3d;
import com.renderer.cameras.PerspectiveCamera;
import com.renderer.lights.DirectionalLight;
import com.renderer.shapes.curvededges.Sphere;
import com.renderer.shapes.polyhedrons.Cube;

public class Example01 {
    public static void main(String[] args){
        PerspectiveCamera camera = new PerspectiveCamera(
            new Vector3d(30),
            new Vector3d(50, 0, 0),
            new Vector3d(4, 5, 2),
            90,
            20,
            40);
        Sphere sphere = new Sphere(new Vector3d(20), 8);
        Cube cube = new Cube(new Vector3d(10), 5);
        DirectionalLight light = new DirectionalLight(
            new Vector3d(2, 2, 5), "Cornflower blue");
        Scene scene = new Scene(camera);
        scene.addShape(sphere);
        scene.addShape(cube);
        scene.addLight(light);
        scene.render();
    }
}

Example01类是我们测试应用程序的主要类。该类只是声明了一个名为mainstatic方法,该方法接收一个名为argsString数组作为参数。当我们执行应用程序时,Java 将调用此方法,并将在args参数中传递参数。在这种情况下,main方法中的代码不考虑任何指定的参数。

main 方法创建一个带有必要参数的PerspectiveCamera实例,然后创建一个名为shapecubeShapeCube。然后,代码创建一个名为lightDirectionalLight实例。

下一行创建一个Scene实例,其中camera作为activeCamera参数的值。然后,代码使用spherecube作为参数两次调用scene.addShape方法。最后,代码使用light作为参数调用scene.addLight,并调用scene.render方法来显示模拟渲染过程生成的消息。

使用 Java 9 编译器编译多个模块

在名为Renderer的基础目录中创建名为mods的子文件夹。这个新的子文件夹将复制我们在Renderer/src(Windows 中的Renderer\src文件夹)中创建的目录结构。我们将运行 Java 编译器为每个 Java 源文件生成一个 Java 类文件。Java 类文件将包含可在Java 虚拟机上执行的 Java 字节码,也称为JVM。对于每个扩展名为.java的 Java 源文件,我们将有一个扩展名为.class的文件,包括模块描述符。例如,我们成功使用 Java 编译器编译Renderer/src/com.renderer.math/com/renderer/math/Vector3d.java源文件后,编译器将生成一个包含 Java 字节码的Renderer/mods/com.renderer.math/com/renderer/math/Vector3d.class文件(称为 Java 类文件)。在 Windows 中,我们必须使用反斜杠(\)作为路径分隔符,而不是斜杠(/)。

现在,在 macOS 或 Linux 上打开一个终端窗口,或在 Windows 中打开命令提示符,然后转到Renderer文件夹。确保路径中包含了javac命令,并且它是针对 Java 9 的 Java 编译器,而不是针对与 Java 9 中引入的模块不兼容的以前的 Java 版本。

在 macOS 或 Linux 中,运行以下命令编译我们最近创建的所有模块,并将生成的 Java 类文件放置在mods文件夹中的目录结构中。-d选项指定将生成的类文件放置在何处,--module-source-path选项指示在何处查找多个模块的输入源文件。

javac -d mods --module-source-path src src/com.renderer.math/module-info.java src/com.renderer.math/com/renderer/math/Vector3d.java src/com.renderer.sceneelements/module-info.java src/com.renderer.sceneelements/com/renderer/sceneelements/Rendereable.java src/com.renderer.sceneelements/com/renderer/sceneelements/SceneElement.java src/com.renderer.cameras/module-info.java src/com.renderer.cameras/com/renderer/cameras/Camera.java src/com.renderer.cameras/com/renderer/cameras/PerspectiveCamera.java src/com.renderer.lights/module-info.java src/com.renderer.lights/com/renderer/lights/DirectionalLight.java src/com.renderer.lights/com/renderer/lights/Light.java src/com.renderer.shapes/module-info.java src/com.renderer.shapes/com/renderer/shapes/Shape.java src/com.renderer.shapes.curvededges/module-info.java src/com.renderer.shapes.curvededges/com/renderer/shapes/curvededges/Sphere.java src/com.renderer.shapes.polyhedrons/module-info.java src/com.renderer.shapes.polyhedrons/com/renderer/shapes/polyhedrons/Cube.java src/com.renderer/module-info.java src/com.renderer/com/renderer/Example01.java src/com.renderer/com/renderer/Scene.java

在 Windows 中,运行以下命令以实现相同的目标:

javac -d mods --module-source-path src src\com.renderer.math\module-info.java src\com.renderer.math\com\renderer\math\Vector3d.java src\com.renderer.sceneelements\module-info.java src\com.renderer.sceneelements\com\renderer\sceneelements\Rendereable.java src\com.renderer.sceneelements\com\renderer\sceneelements\SceneElement.java src\com.renderer.cameras\module-info.java src\com.renderer.cameras\com\renderer\cameras\Camera.java src\com.renderer.cameras\com\renderer\cameras\PerspectiveCamera.java src\com.renderer.lights\module-info.java src\com.renderer.lights\com\renderer\lights\DirectionalLight.java src\com.renderer.lights\com\renderer\lights\Light.java src\com.renderer.shapes\module-info.java src\com.renderer.shapes\com\renderer\shapes\Shape.java src\com.renderer.shapes.curvededges\module-info.java src\com.renderer.shapes.curvededges\com\renderer\shapes\curvededges\Sphere.java src\com.renderer.shapes.polyhedrons\module-info.java src\com.renderer.shapes.polyhedrons\com\renderer\shapes\polyhedrons\Cube.java src\com.renderer\module-info.java src\com.renderer\com\renderer\Example01.java src\com.renderer\com\renderer\Scene.java

下面的目录结构显示了我们将在Renderer/mods(Windows 中的Renderer\mods目录)中拥有的最终内容。Java 编译器生成的 Java 类文件将突出显示。

├───com.renderer
│   │   module-info.class
│   │
│   └───com
│       └───rendererExample01.classScene.class
│
├───com.renderer.cameras
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───camerasCamera.classPerspectiveCamera.class
│
├───com.renderer.lights
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───lightsDirectionalLight.classLight.class
│
├───com.renderer.math
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───mathVector3d.class
│
├───com.renderer.sceneelements
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───sceneelementsRendereable.classSceneElement.class
│
├───com.renderer.shapes
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───shapesShape.class
│
├───com.renderer.shapes.curvededges
│   │   module-info.class
│   │
│   └───com
│       └───renderer
│           └───shapes
│               └───curvededgesSphere.class
│
└───com.renderer.shapes.polyhedronsmodule-info.class
    │
    └───com
        └───renderer
            └───shapes
                └───polyhedrons
 Cube.class

使用 Java 9 运行模块化代码

最后,我们可以使用和java命令来启动 Java 应用程序。返回 macOS 或 Linux 上的终端窗口,或 Windows 中的命令提示符,并确保您位于Renderer文件夹中。确保路径中包含了java命令,并且它是针对 Java 9 的java命令,而不是针对与 Java 9 中引入的模块不兼容的早期 Java 版本。

在 macOS、Linux 或 Windows 中,运行以下命令加载已编译的模块,解析com.renderer模块,并对com.renderer包中声明的Example01类运行main静态方法。--module-path选项指定可在其中找到模块的目录。在这种情况下,我们只需指定mods文件夹。但是,我们可能包含许多用分号(;分隔的目录。-m选项指定要解析的初始模块名,后跟斜杠(/和要执行的主类的名称。

java --module-path mods -m com.renderer/com.renderer.Example01

以下几行显示了在执行上一个命令后生成的输出,该命令为Example01类运行main静态方法。

Created camera at (x: 30, y: 30, z: 30)
Set camera direction to (x: 50, y: 0, z: 0)
Set camera vector to (x: 4, y: 5, z: 2)
Set camera perspective field of view to: 90
Set camera near clipping plane to: 20
Set camera far clipping plane to: 40
Created directional light at (x: 2, y: 2, z: 5)
Set light color to Cornflower blue
Drew sphere at (x: 20, y: 20, z: 20) with radius equal to 8
considering light at (x: 2, y: 2, z: 5)
and light's color equal to Cornflower blue
Drew cube at (x: 10, y: 10, z: 10) with edge length equal to 5
considering light at (x: 2, y: 2, z: 5)
and light's color equal to Cornflower blue

在以前的 Java 版本中,我们可以将许多 Java 类文件及其相关的元数据和资源聚合到一个扩展名为.jar的压缩文件中,称为JARJava Archive文件)。我们还可以将模块打包为 modularjar,其中包括顶层文件夹中压缩文件中的module-info.class文件。

此外,我们可以使用 Java 链接工具(jlink)创建自定义的运行时映像,其中只包含应用程序所需的模块。这样,我们就可以利用整个程序的优化,生成一个在 JVM 上运行的自定义运行时映像。

测试你的知识

  1. 默认情况下,模块需要:
    1. java.base模块。
    2. java.lang模块。
    3. java.util模块。
  2. 有一种约定,规定 Java 9 模块的源代码必须位于以下目录中:
    1. 与模块导出的主类同名。
    2. 与模块名称相同的名称。
    3. 与模块导出的主类型同名。
  3. 以下哪个源代码文件是模块描述符:
    1. module-def.java
    2. module-info.java
    3. module-data.java
  4. 在模块描述符中,以下哪些关键字后面必须跟模块名称:
    1. name
    2. module-name
    3. module
  5. 模块描述符中的exports关键字后跟包名,表示模块导出:
    1. 包中声明的所有类。
    2. 包中声明的所有类型。
    3. 包中声明的所有公共类型。

总结

在本章中,我们学习了如何重构现有代码,以充分利用 Java 9 的面向对象代码。我们已经为未来的需求准备了代码,降低了维护成本,并最大限度地实现了代码重用。

我们学会了组织面向对象的代码。我们创建了许多 Java 源文件。我们在不同的 Java 源文件中声明了接口、抽象类和具体类。我们利用 Java9 中包含的新模块化特性创建了许多依赖于不同模块并导出特定类型的模块。我们学会了声明模块,将它们编译成 Java 字节码,并在 JShell 之外启动应用程序。

现在您已经学会了用 Java 9 编写面向对象的代码,您已经准备好使用在现实生活中的桌面应用程序、移动应用程序、企业应用程序、Web 服务和 Web 应用程序中所学到的一切。这些应用程序将最大限度地实现代码重用,简化维护,并随时准备满足未来的需求。您可以使用 JShell 轻松地原型化新的接口和类,这将提高您作为面向对象 Java 9 开发人员的生产率。