Java17-入门基础知识-十四-

150 阅读1小时+

Java17 入门基础知识(十四)

原文:Beginning Java 17 Fundamentals

协议:CC BY-NC-SA 4.0

二十二、枚举类型

在本章中,您将学习:

  • 什么是枚举类型

  • 如何声明枚举类型和枚举常量

  • 如何在switch语句中使用枚举

  • 如何将数据和方法与枚举常量相关联

  • 如何声明嵌套枚举

  • 如何实现枚举类型的接口

  • 如何对枚举常量执行反向查找

  • 如何使用EnumSet处理枚举常量的范围

本章中的所有示例程序都是清单 22-1 中声明的jdojo.enums模块的成员。

// module-info.java
module jdojo.enums {
    exports com.jdojo.enums;
}

Listing 22-1The Declaration of a jdojo.enums Module

什么是枚举类型?

枚举(也称为枚举和枚举数据类型)允许您创建一个常量的有序列表作为类型。在讨论什么是 enum 以及我们为什么需要它之前,让我们考虑一个问题,并使用 enum 之前的 Java 特性来解决它,enum 是在 Java 5 中引入的。假设您正在开发一个缺陷跟踪应用程序,您需要在其中表示一个缺陷的严重性。该应用程序允许您将缺陷的严重性指定为低、中、高和紧急。在 Java 5 之前,表示四种严重性的典型方式是在一个类中声明四个int常量,比如说Severity,如清单 22-2 所示。

// Severity.java
package com.jdojo.enums;
public class Severity {
    public static final int LOW = 0;
    public static final int MEDIUM = 1;
    public static final int HIGH = 2;
    public static final int URGENT = 3;
}

Listing 22-2A Severity Class with a Few Constants

假设您想要编写一个名为DefectUtil的实用程序类,它有一个方法来根据缺陷的严重性计算缺陷的预计周转天数。DefectUtil类的代码可能如清单 22-3 所示。

// DefectUtil.java
package com.jdojo.enums;
public class DefectUtil {
    public static int getProjectedTurnaroundDays(int severity) {
        int days = 0;
        switch (severity) {
            case Severity.LOW:
                days = 30;
                break;
            case Severity.MEDIUM:
                days = 15;
                break;
            case Severity.HIGH:
                days = 7;
                break;
            case Severity.URGENT:
                days = 1;
                break;
        }
        return days;
    }
    // Other code for the DefectUtil class goes here
}

Listing 22-3A DefectUtil Class

以下是这种方法在处理缺陷严重性时的一些问题:

  • 因为严重性被表示为一个整数常量,所以可以将任何整数值传递给getProjectedTurnaroundDays()方法,而不仅仅是严重性类型的有效值 0、1、2 和 3。您可能希望在此方法中添加一个检查,以便只有有效的严重性值可以传递给它。否则,该方法可能会引发异常。然而,这并不能永远解决问题。每当添加新的严重性类型时,都需要更新检查有效严重性值的代码。

  • 如果更改严重性常量的值,则必须重新编译使用它的代码以反映更改。当你编译DefectUtil类时,Severity.LOW被替换为0Severity.MEDIUM被替换为1,以此类推。如果您将Severity类中常量LOW的值更改为10,您必须重新编译DefectUtil类以反映这一更改。否则,DefectUtil类仍然会继续使用值0

  • 当你在磁盘上保存严重度的值时,会保存其对应的整数值,例如:012等。,不是字符串值LOWMEDIUMHIGH等。对于所有严重性类型,您必须维护一个单独的映射来将整数值转换为其相应的字符串表示形式。

  • 当你打印一个缺陷的严重性值时,它将打印一个整数,例如,012等。严重性的整数值对最终用户没有任何意义。

  • 缺陷的严重性类型有特定的顺序。例如,LOW严重缺陷的优先级低于MEDIUM严重缺陷。因为严重性由任意数字表示,所以您必须使用硬编码的值来编写代码,以保持在Severity类中定义的常量的顺序。假设您添加了另一个严重性类型VERY_HIGH,它的优先级比URGENT低,比HIGH高。现在,您必须更改处理严重性类型排序的代码,因为您已经在现有的严重性类型中间添加了一个。

  • 没有自动的方法(除了通过硬编码)让你列出所有的严重性类型。

您会同意使用整数常量来表示严重性类型是很难维护的。在 Java 5 之前,这是唯一容易实现的定义枚举常量的解决方案。在 Java 5 之前你就可以有效地解决这个问题了。然而,您必须编写的代码量与问题不成比例。Java 5 中的 enum 类型以简单有效的方式解决了这个问题。

根据韦氏词典在线词典,“列举”一词的意思是“一个接一个地指定”这正是枚举类型允许您做的。它允许您以特定的顺序指定常量。枚举类型中定义的常量是该枚举类型的实例。使用关键字enum定义一个枚举类型。它最简单的通用语法是

[access-modifier] enum <enum-type-name> {
    // List of comma separated names of enum constants
}

枚举的访问修饰符与类的访问修饰符相同:publicprivateprotected或包级别。枚举类型名是有效的 Java 标识符。枚举类型的主体放在名称后面的大括号中。枚举类型的主体可以有一列逗号分隔的常量和其他元素,这些元素类似于类中的元素,例如实例变量、方法等。大多数情况下,枚举体只包含常量。下面的代码声明了一个名为Gender的枚举类型,它声明了三个常量—NA、MALE,FEMALE:

public enum Gender {
    NA, MALE, FEMALE; // The semi-colon is optional in this case
}

Tip

用大写字母命名枚举常量是一种约定。如果常量列表后面没有代码,则最后一个枚举常量后面的分号是可选的。

清单 22-4 用四个枚举常量声明了一个名为Severity的公共枚举类型:LOWMEDIUMHIGHURGENT

// Severity.java
package com.jdojo.enums;
public enum Severity {
    LOW, MEDIUM, HIGH, URGENT;
}

Listing 22-4Declaration of a Severity Enum

可以从应用程序中的任何位置访问公共枚举类型。枚举类型的跨模块可访问性规则与其他类型的相同,本书在第十章中介绍了这些规则。

就像一个公共类一样,您需要将清单 22-4 中的代码保存在一个名为Severity.java的文件中。当你编译代码时,编译器会创建一个Severity.class文件。注意,除了使用enum关键字和主体部分之外,Severity枚举类型的所有内容看起来都像是一个类声明。事实上,Java 将 enum 类型实现为一个类。编译器为枚举类型做了很多工作,并为它生成了本质上是一个类的代码。您需要将一个枚举类型放入一个包中,就像您将所有类放入一个包中一样。可以使用import语句将枚举类型导入编译单元,就像导入类类型一样。

声明枚举类型变量的方式与声明类类型变量的方式相同:

// Declare defectSeverity variable of the Severity enum type
Severity defectSeverity;

您可以将null赋给一个枚举类型的变量,如下所示:

Severity defectSeverity = null;

还可以为枚举类型的变量赋什么值?枚举类型定义了两件事:

  • 枚举常量,是其类型的唯一有效值

  • 那些常量的顺序

Severity枚举类型定义了四个枚举常量。因此,Severity枚举类型的变量只能有四个值之一— LOWMEDIUMHIGHURGENT—null。通过将枚举类型名用作限定符,可以使用点标记来引用枚举常量。以下代码片段为Severity枚举类型的变量赋值:

Severity low = Severity.LOW;
Severity medium = Severity.MEDIUM;
Severity high = Severity.HIGH;
Severity urgent = Severity.URGENT;

不能实例化枚举类型。以下试图实例化Severity枚举类型的代码会导致编译时错误:

Severity badAttempt = new Severity(); // A compile-time error

Tip

枚举类型充当类型和工厂。它声明一个新类型和该类型的有效实例列表作为其常量。

枚举类型还为其所有常量分配一个顺序号(或位置号),称为序数。序号从零开始,当您在常量列表中从第一个移动到最后一个时,它会递增 1。第一个枚举常量被赋予序数值 0,第二个为 1,第三个为 2,依此类推。分配给在Severity枚举类型中声明的常量的序数值是 0 到LOW,1 到MEDIUM,2 到HIGH,3 到URGENT。如果更改枚举类型体中常量的顺序或添加新的常量,它们的序数值将相应地更改。

每个枚举常量都有一个名称。枚举常量的名称与其声明中为常量指定的标识符相同。例如,Severity枚举类型中的LOW常量的名称是"LOW"

您可以分别使用name()ordinal()方法读取枚举常量的名称和序号。每个枚举类型都有一个名为values()的静态方法,该方法按照常量在其主体中声明的顺序返回一个常量数组。清单 22-5 中的程序打印在Severity枚举类型中声明的所有枚举常量的名称和序号。

// ListEnumConstants.java
package com.jdojo.enums;
public class ListEnumConstants {
    public static void main(String[] args) {
        for(Severity s : Severity.values()) {
            String name = s.name();
            int ordinal = s.ordinal();
            System.out.println(name + "(" + ordinal + ")");
        }
    }
}
LOW(0)
MEDIUM(1)
HIGH(2)
URGENT(3)

Listing 22-5Listing Name and Ordinal of Enum Type Constants

枚举类型的超类

枚举类型类似于 Java 类类型。事实上,编译器会在编译枚举类型时创建一个类。出于各种实际目的,您可以将枚举类型视为类类型。但是,有一些规则仅适用于枚举类型。枚举类型也可以有构造器、字段和方法。我们不是说过枚举类型不能被实例化吗?(换句话说,new Severity()无效。)如果枚举类型不能实例化,为什么还需要构造器呢?

这就是为什么你需要一个枚举类型的构造器。枚举类型仅在编译器生成的代码中实例化。所有枚举常量都是同一枚举类型的对象。这些实例的创建和命名与编译器生成的代码中的枚举常量相同。编译器在耍花招。编译器为枚举类型生成代码,如下所示。下面的示例代码只是为了让您了解幕后发生的事情。编译器生成的实际代码可能与显示的不同。例如,valueOf()方法的代码给你一种感觉,它将名字与枚举常量名进行比较,并返回匹配的常量实例。实际上,编译器为调用Enum超类中的valueOf()方法的valueOf()方法生成代码:

// Transformed code for Severity enum type declaration
package com.jdojo.enums;
public final class Severity extends Enum {
    public static final Severity LOW;
    public static final Severity MEDIUM;
    public static final Severity HIGH;
    public static final Severity URGENT;
    // Create constants when class is loaded
    static {
         LOW    = new Severity("LOW", 0);
         MEDIUM = new Severity("MEDIUM", 1);
         HIGH   = new Severity("HIGH", 2);
         URGENT = new Severity("URGENT", 3);
    }
    // The private constructor to prevent direct instantiation
    private Severity(String name, int ordinal) {
        super(name, ordinal);
    }
    public static Severity[] values() {
        return new Severity[] { LOW, MEDIUM, HIGH, URGENT };
    }
    public static Severity valueOf(String name) {
        if (LOW.name().equals(name)) {
            return LOW;
        }
        if (MEDIUM.name().equals(name)) {
            return MEDIUM;
        }
        if (HIGH.name().equals(name)) {
             return HIGH;
        }
        if (URGENT.name().equals(name)) {
            return URGENT;
        }
        throw new IllegalArgumentException("Invalid enum constant " + name);
    }
}

通过查看Severity枚举声明的转换代码,可以得出以下几点:

表 22-1

所有枚举类型中都可用的枚举类中的方法列表

|

方法名

|

描述

| | --- | --- | | public final String name() | 返回枚举常量的名称,该名称与枚举类型声明中声明的名称完全相同。 | | public final int ordinal() | 返回枚举类型声明中声明的枚举常量的顺序(或位置)。 | | public final boolean equals(Object other) | 如果指定的对象等于枚举常量,则返回true。否则返回false。请注意,枚举类型不能直接实例化,它有固定数量的实例,这些实例等于它声明的枚举常量的数量。这意味着当对两个枚举常量使用==操作符和equals()方法时,它们返回相同的结果。 | | public final int hashCode() | 返回枚举常量的哈希代码值。 | | public final int``compareTo(E o) | 将此枚举常量的顺序与指定枚举常量的顺序进行比较。它返回此枚举常量和指定枚举常量的序数值之差。请注意,要比较两个枚举常量,它们必须是相同的枚举类型。否则,将引发运行时异常。 | | public final Class<E> getDeclaringClass() | 返回声明枚举常量的类的Class对象。如果此方法为两个枚举常量返回相同的类对象,则这两个枚举常量被视为具有相同的枚举类型。注意,由getClass()方法返回的Class对象(每个枚举类型都从Object类继承而来)可能与该方法返回的类对象不同。当枚举常量有主体时,该枚举常量的对象的实际类与声明类不同。实际上,它是声明类的一个子类。 | | public String toString() | 默认情况下,它返回枚举常量的名称,该名称与name()方法的返回值相同。请注意,此方法未声明为 final,因此您可以重写它,以便为每个枚举常量返回更有意义的字符串表示形式。 | | public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) | 返回指定枚举类型和名称的枚举常量。例如,您可以使用以下代码获取代码中Severity枚举类型的LOW枚举常量值:Severity lowSeverity = Enum.valueOf(Severity.class, "LOW") | | protected final Object clone() throws CloneNotSupportedException | Enum类重新定义了clone()方法。它将方法声明为 final,因此它不能被任何枚举类型重写。方法总是引发异常。这样做是为了防止克隆枚举常量。这确保每个枚举类型只存在一组枚举常量。 | | protected final void finalize() | Enum类被声明为 final,因此它不能被任何枚举类型覆盖。它提供了一个空的身体。因为除了常量之外,不能创建枚举类型的实例,所以为枚举类型创建一个finalize()方法是没有意义的。 |

  • 每个枚举类型都隐式扩展了java.lang.Enum类。这意味着在Enum类中定义的所有方法都可以用于所有枚举类型。表 22-1 列出了在Enum类中定义的方法。

  • 枚举类型是隐式最终类型。在某些情况下(稍后讨论),编译器不能像在Severity类的示例代码中那样将其声明为 final。

  • 编译器为每个枚举类型添加了两个静态方法,values()valueOf()values()方法按照枚举类型中声明的顺序返回枚举常量数组。您已经看到了清单 22-5 中values()方法的使用。valueOf()方法用于获取一个枚举类型的实例,使用常量名称作为字符串。例如,Severity.valueOf("LOW")将返回Severity.LOW常量。valueOf()方法有助于从字符串值到枚举类型值的反向查找。

  • Enum类实现了java.lang.Comparablejava.io.Serializable接口。这意味着每个枚举类型的实例都可以进行比较和序列化。Enum类确保在反序列化过程中,除了声明为枚举常量的实例之外,不会创建任何其他枚举类型的实例。您可以使用compareTo()方法来确定一个枚举常量是在另一个枚举常量之前还是之后声明的。请注意,您还可以通过比较两个枚举常量的序号来确定它们的顺序。compareTo()方法做了同样的事情,多了一项检查,即被比较的枚举常量必须是相同的枚举类型。下面的代码片段显示了如何比较两个枚举常量:

Severity s1 = Severity.LOW;
Severity s2 = Severity.HIGH;
// s1.compareTo(s2) returns s1.ordinal() - s2.ordinal()
int diff = s1.compareTo(s2);
if (diff > 0) {
    System.out.println(s1 + " occurs after " + s2);
} else {
    System.out.println(s1 + " occurs before " + s2);
}

在 switch 语句中使用枚举类型

您可以在switch语句中使用枚举类型。当switch表达式是枚举类型时,所有事例标签必须是相同枚举类型的非限定枚举常量。switch语句从表达式的类型中推导出枚举类型名。您可以包含一个默认标签。

清单 22-6 包含了使用switch语句的DefectUtil类的修改版本。现在,您不需要处理在getProjectedTurnaroundDays()方法的severity参数中接收null值的异常情况。如果switch语句的枚举表达式的计算结果为null,它将抛出一个NullPointerException

// DefectUtil.java
package com.jdojo.enums;
public class DefectUtil {
    public static int getProjectedTurnaroundDays(Severity severity) {
        int days = 0;
        switch (severity) {
            // Must use the unqualified name LOW, not Severity.LOW
            case LOW:
                days = 30;
                break;
            case MEDIUM:
                days = 15;
                break;
            case HIGH:
                days = 7;
                break;
            case URGENT:
                days = 1;
                break;
        }
        return days;
    }
}

Listing 22-6A Revised Version of the DefectUtil Class Using the Severity Enum

或者使用开关表达式:

    public static int getProjectedTurnaroundDays(Severity severity) {
        return switch (severity) {
            case LOW -> 30;
            case MEDIUM -> 15;
            case HIGH -> 7;
            case URGENT -> 1;
        };
    }

将数据和方法与枚举常量相关联

通常,你声明一个枚举类型只是为了拥有一些枚举常量,就像你在Severity枚举类型中所做的那样。因为一个枚举类型实际上是一个类类型,所以你可以在一个枚举类型主体中声明你可以在一个类主体中声明的几乎所有东西。让我们将一个数据元素,预计周转天数,与您的每个Severity枚举常量相关联。您将把增强的Severity枚举类型命名为SmartSeverity。清单 22-7 包含了SmartSeverity枚举类型的代码,这与Severity枚举类型的代码非常不同。

// SmartSeverity.java
package com.jdojo.enums;
public enum SmartSeverity {
    LOW(30), MEDIUM(15), HIGH(7), URGENT(1);
    // Declare an instance variable
    private int projectedTurnaroundDays;
    // Declare a private constructor
    private SmartSeverity(int projectedTurnaroundDays) {
        this.projectedTurnaroundDays = projectedTurnaroundDays;
    }
    // Declare a public method to get the turnaround days
    public int getProjectedTurnaroundDays() {
        return projectedTurnaroundDays;
    }
}

Listing 22-7A SmartSeverity Enum Type Declaration That Uses Fields, Constructors, and Methods

让我们讨论一下SmartSeverity枚举类型中的新内容:

  • 它声明了一个名为projectedTurnaroundDays的实例变量,该变量将存储每个枚举常量的预计周转天数的值:

  • 它定义了一个私有构造器,该函数接受一个int参数。它将其参数值存储在实例变量中。可以向一个枚举类型添加多个构造器。如果不添加构造器,则添加无参数构造器。不能向枚举类型添加公共或受保护的构造器。枚举类型声明中的所有构造器都要经过编译器的参数和代码转换,并且它们的访问级别被更改为 private。编译器会在枚举类型的构造器中添加或更改许多内容。作为一名程序员,您不需要知道编译器所做更改的细节:

// Declare an instance variable
private int projectedTurnaroundDays;

  • 它声明了一个公共方法getProjectedTurnaroundDays(),该方法返回 enum 常量(或者 enum 类型的实例)的预计周转天数的值。

  • 枚举常量声明已更改为LOW(30), MEDIUM(15), HIGH(7), URGENT(1);。此更改并不明显。现在,每个枚举常量名称后面都有一个圆括号中的整数值,例如,LOW(30)。这个语法是用int参数类型调用构造器的简写。创建枚举常量时,括号内的值将被传递给您添加的构造器。只需在常量声明中使用枚举常量的名称(例如,LOW)),就可以调用默认的无参数构造器。

// Declare a private constructor
private SmartSeverity(int projectedTurnaroundDays) {
    this.projectedTurnaroundDays = projectedTurnaroundDays;
}

清单 22-8 中的程序测试SmartSeverity枚举类型。它打印常量的名称、序数和预计周转天数。注意,计算预计周转天数的逻辑封装在枚举类型本身的声明中。SmartSeverity枚举类型结合了Severity枚举类型的代码和DefectUtil类中的getProjectedTurnaroundDays()方法。您不必再编写switch语句来获得预计的周转天数。每个 enum 常量都知道它的预计周转天数。

// SmartSeverityTest.java
package com.jdojo.enums;
public class SmartSeverityTest {
    public static void main(String[] args) {
        for (SmartSeverity s : SmartSeverity.values()) {
            String name = s.name();
            int ordinal = s.ordinal();
            int days = s.getProjectedTurnaroundDays();
            System.out.println("name=" + name + ", ordinal=" + ordinal
                    + ", days=" + days);
        }
    }
}
name=LOW, ordinal=0, days=30
name=MEDIUM, ordinal=1, days=15
name=HIGH, ordinal=2, days=7
name=URGENT, ordinal=3, days=1

Listing 22-8A Test Class to Test the SmartSeverity Enum Type

将主体与枚举常量关联

SmartSeverity是一个向枚举类型添加数据和方法的例子。对于所有的枚举常量,getProjectedTurnaroundDays()方法中的代码是相同的。您还可以将不同的主体与每个枚举常量相关联。主体可以有字段和方法。枚举常量的主体放在名字后面的大括号中。如果 enum 常量接受参数,则它的主体遵循其参数列表。将主体与枚举常量相关联的语法如下:

[access-modifier] enum <enum-type-name> {
    CONST1 {
        // Body for CONST1 goes here
    },
    CONST2 {
        // Body for CONST2 goes here
    },
    CONST3(arguments-list) {
        // Body of CONST3 goes here
    };
    // Other code goes here
}

当你添加一个主体到一个枚举常量时,这是一个有点不同的游戏。编译器创建一个匿名类,该类继承自枚举类型。它将枚举常量的主体移动到匿名类的主体。匿名类在本系列的第二卷中有更详细的介绍。我们简单地用它来完成枚举类型的讨论。现在,你可以把它看作是声明一个类,同时创建该类的对象的不同方式。

考虑一个ETemp枚举类型,如下所示:

public enum ETemp {
    C1 {
        // Body of constant C1
        public int getValue() {
            return 100;
        }
    },
    C2,
    C3;
}

ETemp枚举类型的主体声明了三个常量:C1C2C3。你在C1常量中增加了一个物体。编译器会将ETemp的代码转换成类似下面的代码:

public enum ETemp {
    public static final ETemp C1 = new ETemp() {
        // Body of constant C1
        public int getValue() {
            return 100;
        }
    };
    public static final ETemp C2 = new ETemp();
    public static final ETemp C3 = new ETemp();
    // Other code goes here
}

请注意,常量C1被声明为类型ETemp,并使用匿名类分配了一个对象。ETemp枚举类型不知道匿名类中定义的getValue()方法。因此,它对于所有的实际用途都是无用的,因为你不能像ETemp.C1.getValue()那样调用这个方法。

为了让客户端代码使用getValue()方法,您必须为ETemp枚举类型声明一个getValue()方法。如果你想让ETemp的所有常量覆盖并提供这个方法的实现,你需要将它声明为abstract。如果您希望它被一些(但不是全部)常量覆盖,您需要声明它是非抽象的,并为它提供一个默认实现。以下代码为ETemp枚举类型声明了一个getValue()方法,该方法返回 0:

public enum ETemp {
    C1 {
        // Body of constant C1
        public int getValue() {
            return 100;
        }
    },
    C2,
    C3;
    // Provide the default implementation for the getValue() method
    public int getValue() {
        return 0;
    }
}

C1常量有它的主体,它覆盖了getValue()方法并返回100。注意常量C2C3不一定要有体;他们不需要覆盖getValue()方法。现在,您可以在ETemp枚举类型上使用getValue()方法。

下面的代码重写了先前版本的ETemp并声明了getValue()方法abstract。一个 enum 类型的abstract方法强迫你为所有的常量提供一个主体并覆盖那个方法。现在所有的常量都有一个体。每个常量的主体覆盖并提供了getValue()方法的实现:

public enum ETemp {
    C1 {
        // Body of constant C1
        public int getValue() {
            return 100;
        }
    },
    C2 {
        // Body of constant C2
        public int getValue() {
            return 0;
        }
    },
    C3 {
        // Body of constant C3
        public int getValue() {
            return 0;
        }
    };
    // Make the getValue() method abstract
    public abstract int getValue();
}

让我们增强您的SmartSeverity枚举类型。您的枚举类型已经没有好的名称了。你将新的命名为SuperSmartSeverity。清单 22-9 有代码。

// SuperSmartSeverity.java
package com.jdojo.enums;
public enum SuperSmartSeverity {
    LOW("Low Priority", 30) {
        @Override
        public double getProjectedCost() {
            return 1000.0;
        }
    },

    MEDIUM("Medium Priority", 15) {
        @Override
        public double getProjectedCost() {
            return 2000.0;
        }
    },
    HIGH("High Priority", 7) {
        @Override
        public double getProjectedCost() {
            return 3000.0;
        }
    },
    URGENT("Urgent Priority", 1) {
        @Override
        public double getProjectedCost() {
            return 5000.0;
        }
    };
    // Declare instance variables
    private final String description;
    private final int projectedTurnaroundDays;
    // Declare a private constructor
    private SuperSmartSeverity(String description,
            int projectedTurnaroundDays) {
        this.description = description;
        this.projectedTurnaroundDays = projectedTurnaroundDays;
    }
    // Declare a public method to get the turn around days
    public int getProjectedTurnaroundDays() {
        return projectedTurnaroundDays;
    }
    // Override the toString() method in the Enum class to return description
    @Override
    public String toString() {
        return this.description;
    }
    // Provide getProjectedCost() abstract method, so all constants
    // override and provide implementation for it in their body
    public abstract double getProjectedCost();
}

Listing 22-9Using a Body for Enum Constants

以下是SuperSmartSeverity枚举类型中的新特性:

  • 它添加了一个抽象方法getProjectedCost()来返回每种严重程度的预计成本。

  • 它为每个常量提供了一个主体,为getProjectedCost()方法提供了实现。请注意,在枚举类型中声明抽象方法会强制您为其所有常量提供一个主体。

  • 它向构造器添加了另一个参数,这是严重性类型的一个更好的名称。

  • 它覆盖了Enum类中的toString()方法。Enum类中的toString()方法返回常量的名称。您的toString()方法为每个常量返回一个简短且更直观的名称。

    典型地,你不需要为一个枚举类型写这种复杂的代码。Java enum 非常强大。如果你需要的话,它有你可以利用的特性。

清单 22-10 中的代码演示了添加到SuperSmartSeverity枚举类型中的新特性的使用。

// SuperSmartSeverityTest.java
package com.jdojo.enums;
public class SuperSmartSeverityTest {
    public static void main(String[] args) {
        for (SuperSmartSeverity s : SuperSmartSeverity.values()) {
            String name = s.name();
            String desc = s.toString();
            int ordinal = s.ordinal();
            int projectedTurnaroundDays = s.getProjectedTurnaroundDays();
            double projectedCost = s.getProjectedCost();
            System.out.println("name=" + name
                    + ", description=" + desc
                    + ", ordinal=" + ordinal
                    + ", turnaround days="
                    + projectedTurnaroundDays
                    + ", projected cost=" + projectedCost);
        }
    }

}
name=LOW, description=Low Priority, ordinal=0, turnaround days=30, projected cost=1000.0
name=MEDIUM, description=Medium Priority, ordinal=1, turnaround days=15, projected cost=2000.0
name=HIGH, description=High Priority, ordinal=2, turnaround days=7, projected cost=3000.0
name=URGENT, description=Urgent Priority, ordinal=3, turnaround days=1, projected cost=5000.0

Listing 22-10A Test Class to Test the SuperSmartSeverity Enum Type

比较两个枚举常量

可以用三种方式比较两个枚举常量:

  • 使用Enum类的compareTo()方法

  • 使用Enum类的equals()方法

  • 使用==操作符

Enum类的compareTo()方法允许你比较相同枚举类型的两个枚举常量。它返回两个枚举常量的序数之差。如果两个枚举常量相同,则返回零。下面的代码片段将打印出-3,因为LOW(ordinal=0)URGENT(ordinal=3)的序数之差是–3。负值表示被比较的常量出现在被比较的常量之前:

Severity s1 = Severity.LOW;
Severity s2 = Severity.URGENT;
int diff = s1.compareTo(s2);
System.out.println(diff);
-3

假设您有另一个名为BasicColor的枚举,如清单 22-11 所示。

// BasicColor.java
package com.jdojo.enums;
public enum BasicColor {
    RED, GREEN, BLUE;
}

Listing 22-11A BasicColor Enum

下面的代码片段不会编译,因为它试图比较属于不同枚举类型的两个枚举常量:

int diff = BasicColor.RED.compareTo(Severity.URGENT); // A compile-time error

您可以使用Enum类的equals()方法来比较两个枚举常量是否相等。枚举常量只等于自身。注意,equals()方法可以在两个不同类型的枚举常量上调用。如果两个枚举常量来自不同的枚举类型,该方法返回false:

Severity s1 = Severity.LOW;
Severity s2 = Severity.URGENT;
BasicColor c = BasicColor.BLUE;
System.out.println(s1.equals(s1));
System.out.println(s1.equals(s2));
System.out.println(s1.equals(c));
true
false
false

您也可以使用相等运算符(==)来比较两个枚举常量是否相等。==运算符的两个操作数必须是相同的枚举类型。否则,您会得到一个编译时错误:

Severity s1 = Severity.LOW;
Severity s2 = Severity.URGENT;
BasicColor c = BasicColor.BLUE;
System.out.println(s1 == s1);
System.out.println(s1 == s2);
// A compile-time error. Cannot compare Severity and BasicColor enum types
//System.out.println(s1 == c);
true
false

嵌套枚举类型

可以有嵌套的枚举类型声明。可以在类、接口或其他枚举类型中声明嵌套枚举类型。嵌套枚举类型是隐式静态的。还可以在其声明中显式声明嵌套的枚举类型 static。由于枚举类型始终是静态的,无论是否声明,都不能声明局部枚举类型(例如,在方法体中)。对于嵌套的枚举类型,可以使用任何访问修饰符(publicprivateprotected或包级别)。清单 22-12 显示了在Person类中声明名为Gender的嵌套public枚举类型的代码。

// Person.java
package com.jdojo.enums;
public class Person {
    public enum Gender {MALE, FEMALE, NA}
}

Listing 22-12A Gender Enum Type as a Nested Enum Type Inside a Person Class

可以从同一个模块中的任何地方访问Person.Gender枚举类型,因为它已经被声明为公共的。在其他模块中访问它取决于模块可访问性规则。您需要导入枚举类型,以便在其他包中使用它的简单名称,如下面的代码所示:

// Test.java
package com.jdojo.enums.pkg1;
import com.jdojo.enums.Person.Gender;
public class Test {
    public static void main(String[] args) {
        Gender m = Gender.MALE;
        Gender f = Gender.FEMALE;
        System.out.println(m);
        System.out.println(f);
    }
}
MALE
FEMALE

通过使用静态导入来导入枚举常量,也可以使用枚举常量的简单名称。下面的代码片段使用了MALEFEMALE,它们是Person.Gender枚举类型的常量的简单名称。注意,需要第一个import语句来导入Gender类型本身,以便在代码中使用它的简单名称:

// Test.java
package com.jdojo.enums.pkg1;
import com.jdojo.enums.Person.Gender;
import static com.jdojo.enums.Person.Gender.*;
public class Test {
    public static void main(String[] args) {
        Gender m = MALE;
        Gender f = FEMALE;
        System.out.println(m);
        System.out.println(f);
    }
}
MALE
FEMALE

还可以将一个枚举类型嵌套在另一个枚举类型或接口中。以下是有效的枚举类型声明:

public enum OuterEnum {
    C1, C2, C3;
    public enum NestedEnum {
        C4, C5, C6;
    }
}
public interface MyInterface {
    int operation1();
    int operation2();
    public enum AnotherNestedEnum {
        CC1, CC2, CC3;
    }
}

实现枚举类型的接口

枚举类型可以实现接口。实现接口的枚举类型的规则与实现接口的类的规则相同。一个枚举类型永远不会被另一个枚举类型继承。因此,不能将枚举类型声明为抽象类型。这也意味着,如果一个枚举类型实现了一个接口,它也必须为该接口中的所有抽象方法提供实现。清单 22-13 中的程序声明了一个Command接口。

// Command.java
package com.jdojo.enums;
public interface Command {
    void execute();
}

Listing 22-13A Command Interface

清单 22-14 中的程序声明了一个名为CommandList的枚举类型,它实现了Command接口。每个枚举常量实现了Command接口的execute()方法。或者,您可以在枚举类型主体中实现execute()方法,并省略一些或所有枚举常量的实现。清单 22-15 演示了如何使用CommandList枚举类型中的枚举常量作为Command类型。

// CommandTest.java
package com.jdojo.enums;
public class CommandTest {
    public static void main(String... args) {
        // Execute all commands in the command list
        for(Command cmd : CommandList.values()) {
            cmd.execute();
        }
    }
}
Running...
Jumping...

Listing 22-15Using the CommandList Enum Constants as Command Types

// CommandList.java
package com.jdojo.enums;
public enum CommandList implements Command {
    RUN {
        @Override
        public void execute() {
            System.out.println("Running...");
        }
    },
    JUMP {
        @Override
        public void execute() {
            System.out.println("Jumping...");
        }
    };
    // Force all constants to implement the execute() method.
    @Override
    public abstract void execute();
}

Listing 22-14 A CommandList Enum Type Implementing the Command Interface

枚举常量的反向查找

如果知道枚举常量的名称或在列表中的位置,就可以得到它的引用。这被称为基于枚举常量的名称或序号的反向查找。您可以使用由编译器添加到枚举类型中的valueOf()方法,根据名称执行反向查找。您可以使用由values()方法返回的数组(由编译器添加到枚举类型中)按序号执行反向查找。由 values ()方法返回的数组中值的顺序与声明枚举常量的顺序相同。枚举常量的序号从零开始。这意味着枚举常量的序数值可以用作由values()方法返回的数组中的索引。下面的代码片段演示了如何反向查找枚举常量:

Severity low1 = Severity.valueOf("LOW"); // A reverse lookup using a name
Severity low2 = Severity.values()[0];    // A reverse lookup using an ordinal
System.out.println(low1);
System.out.println(low2);
System.out.println(low1 == low2);
LOW
LOW
true

枚举常量的反向查找区分大小写。如果在valueOf()方法中使用了无效的常量名,就会抛出一个IllegalArgumentException。例如,Severity.valueOf("low")将抛出一个IllegalArgumentException,声明在Severity枚举中不存在名为“low”的枚举常量。

枚举常量的范围

Java API 提供了一个java.util.EnumSet集合类来处理枚举类型的枚举常量范围。EnumSet类的实现非常高效。假设您有一个名为Day的枚举类型,如清单 22-16 所示。

// Day.java
package com.jdojo.enums;
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

Listing 22-16A Day Enum Type

您可以使用EnumSet类处理一系列日期;例如,您可以获得从MONDAYFRIDAY之间的所有天数。一个EnumSet只能包含一个枚举类型的枚举常量。清单 22-17 展示了如何使用EnumSet类来处理枚举常量的范围。

// EnumSetTest.java
package com.jdojo.enums;
import java.util.EnumSet;
public class EnumSetTest {
    public static void main(String[] args) {
        // Get all constants of the Day enum
        EnumSet<Day> allDays = EnumSet.allOf(Day.class);
        print(allDays, "All days: ");
        // Get all constants from MONDAY to FRIDAY of the Day enum
        EnumSet<Day> weekDays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
        print(weekDays, "Weekdays: ");
        // Get all constants that are not from MONDAY to FRIDAY of the Day enum.
        // Essentially, we will get days representing weekends.
        EnumSet<Day> weekends = EnumSet.complementOf(weekDays);
        print(weekends, "Weekends: ");
    }
    public static void print(EnumSet<Day> days, String msg) {
        System.out.print(msg);
        for (Day d : days) {
            System.out.print(d + " ");
        }
        System.out.println();
    }
}
All days: MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY
Weekdays: MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY
Weekends: SATURDAY SUNDAY

Listing 22-17A Test Class to Demonstrate How to Use the EnumSet Class

摘要

像类和接口一样,枚举在 Java 中定义了一个新的引用类型。枚举类型由一组预定义的有序值组成,这些值称为枚举类型的元素或常量。枚举类型的常量有一个名称和一个序号。您可以使用枚举常量的名称和序号来获取它的引用,反之亦然。通常,枚举类型用于定义类型安全常量。

枚举类型拥有类所拥有的一些东西。它有构造器、实例变量和方法。但是,枚举类型的构造器是隐式私有的。枚举类型也可以像类一样实现接口。

您可以声明枚举类型的变量。变量可以被赋值为null或者枚举类型的常量之一。每个枚举类型都隐式地继承自java.lang.Enum类。枚举类型可以实现接口。在switch语句或表达式中可以使用枚举类型。Java 提供了一个EnumSet类的有效实现,以处理一系列特定枚举类型的枚举常量。

QUESTIONS AND EXERCISES

  1. Java 中的枚举类型是什么?

  2. Java 中所有枚举的超类是什么?

  3. Java 中的一个枚举可以扩展另一个枚举吗?

  4. Java 中的 enum 可以实现一个或多个接口吗?

  5. 下面的枚举声明有效吗?如果是,它声明了多少个枚举常量?

    public enum Gender {
       MALE, FEMALE,
    }
    
    
  6. Consider the following declaration for an enum named Day:

    public enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    }
    
    

    给定一串“星期五”,你将如何查找这一天。星期五枚举常量?

  7. Consider the following declaration for an enum named Day:

    public enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    }
    
    

    你如何查找一天的序数?周日吗?

  8. 考虑下面名为Day :

    public enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    }
    
    

    的枚举的声明,完成下面的代码片段,它将打印 DAY 枚举中星期二的序数。它应该打印 1:

    String dayName = "TUESDAY";
    int ordinal = /* Complete this statement. */;
    System.out.println(ordinal);
    
    
  9. Consider the following declaration for an enum named Day:

    public enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    }
    
    

    使用 for-each 循环打印每一天的名称及其序号,如星期一(0)、星期二(1)等。

  10. 编写以下代码片段的输出:

```java
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
EnumSet<Day> es = EnumSet.range(Day.TUESDAY, Day.FRIDAY);
for(Day d : es) {
    System.out.printf("%s(%d)%n", d.name(), d.ordinal());
}

```

11. 编写以下代码片段的输出:

```java
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
EnumSet<Day> es =
    EnumSet.complementOf(EnumSet.range(Day.TUESDAY, Day.FRIDAY));
for(Day d : es) {
    System.out.printf("%s(%d)%n", d.name(), d.ordinal());
}

```

12. 考虑下面这个名为Country :

```java
public enum Country {
    BHUTAN("Bhutan", "BT"),
    BRAZIL("Brazil", "BR"),
    FIJI("Fiji", "FJ"),
    INDIA("India", "IN"),
    SPAIN("Spain", "ES");
    private final String fullName;
    private final String isoName;
    private Country(String fullName, String isoName) {
        this.fullName = fullName;
        this.isoName = isoName;
    }
    public String fullName() {
        return this.fullName;
    }
    public String isoName() {
        return this.isoName;
    }
    @Override
    public String toString() {
        return this.fullName;
    }
}

```

的枚举声明,当执行下面的代码片段时,写出输出:

```java
for(Country c : Country.values()) {
    System.out.printf("%s[%d, %s, %s]%n",
          c.name(), c.ordinal(), c, c.isoName());
}

```

13. 考虑下面对一个Gender枚举的声明:

```java
public enum Gender {
    MALE, FEMALE
}

```

修改性别枚举的代码,以便下面代码片段的输出如代码后面的预期输出部分所示。您应该更改性别枚举的代码,而不是下面的代码片段:

```java
for(Gender c : Gender.values()) {
    System.out.printf("%s%n", c);
}

```

预期输出:

```java
Male
female

```

14. 假设Color是一个枚举。下面的MyFavColor枚举声明是否有效?如果不是,请解释你的答案:

```java
public enum MyFavColor extends Color {
    WHITE, BLACK
}

```

15. 当下面的代码片段运行时,输出会是什么?

```java
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
Day[] days = {Day.FRIDAY, Day.MONDAY, Day.WEDNESDAY};
System.out.println(Arrays.toString(days));
Arrays.sort(days);
System.out.println(Arrays.toString(days));

```

16. 以下代码片段的输出会是什么?

```java
public enum Gender {
    MALE, FEMALE, NA
}
System.out.println(Gender.MALE == Gender.MALE);
System.out.println(Gender.MALE.equals(Gender.MALE));

```

二十三、Java Shell

在本章中,您将学习:

  • Java shell 是什么

  • 什么是 JShell 工具和 JShell API

  • 如何配置 JShell 工具

  • 如何使用 JShell 工具评估 Java 代码片段

  • 如何使用 JShell API 评估 Java 代码片段

本章中的所有示例程序都是清单 23-1 中声明的jdojo.jshell模块的成员。

// module-info.java
module jdojo.jshell {
    exports com.jdojo.jshell;
    requires jdk.jshell;
}

Listing 23-1The Declaration of a jdojo.jshell Module

在你开始阅读本章之前,让我们先弄清楚本章中经常使用的以下三个短语的用法:

  • JShell 命令行工具或 JShell 工具

  • jshell

  • JShell API

在这一章中,讨论的主要话题是 JShell,它既可以用作命令行工具,也可以用作 Java API。“JShell 命令行工具”指的是 JShell 被用作命令行工具的能力。JShell 命令行工具名为jshell(全小写),当你在 Windows 上安装 JDK 时,它作为一个jshell.exe文件安装在JDK_HOME\bin目录下。“JShell API”指的是 JShell 作为 Java API 的能力。

什么是 Java Shell?

被称为 JShell 的 Java shell 是一个命令行工具,它提供了一种访问 Java 编程语言的交互方式。它让您评估 Java 代码片段,而不是强迫您编写整个 Java 程序。这是 Java 的一个REPL (read-eval-print 循环)。JShell 也是一个 API,您可以使用它来开发应用程序,以提供与 JShell 命令行工具相同的功能。

READ-Eval-Print 循环(REPL)是一个命令行工具(也称为交互式语言外壳),它让用户快速评估代码片段,而不必编写完整的程序。REPL这个名字来源于 Lisp 中的三个原始函数——readevalprint——在一个循环中使用。read函数读取用户输入并将其解析成数据结构;eval函数评估已解析的用户输入以产生结果;print函数打印结果。打印结果后,该工具准备好再次接受用户输入,从而触发读取-评估-打印循环。术语REPL用于一个交互式工具,让你与编程语言进行交互。图 23-1 为REPL的概念图。UNIX shell 或 Windows 命令提示符的作用类似于REPL,它读取操作系统命令,执行它,打印输出,并等待读取另一个命令。

img/323069_3_En_23_Fig1_HTML.png

图 23-1

读取-评估-打印循环的概念图

为什么在 JDK 9 中增加了 JShell?其中一个主要原因是来自学术界的反馈,即 Java 有一个陡峭的学习曲线。其他编程语言如 Lisp、Python、Ruby、Groovy 和 Clojure 也支持REPL很久了。只为写一句“你好,世界!”用 Java 编写程序,你必须求助于编辑-编译-执行循环(ECEL ),包括编写一个完整的程序,编译它,然后执行它。如果您需要进行更改,您必须重复这些步骤。除了一些其他的内务工作,比如定义目录结构、编译和执行程序,下面是打印“Hello,world!”Java 程序中的消息:

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

这个程序在执行时会在控制台上打印一条消息:"Hello, world!"。编写一个完整的程序来计算这样一个简单的表达式是多余的。这是学术界不再将 Java 作为初始编程语言教授给学生的主要原因。Java 设计者听取了教学社区的反馈,并在 JDK 9 中引入了 JShell 工具。要实现与 HelloWorld 程序相同的功能,您只需在jshell命令提示符下编写一行代码:

jshell> System.out.println("Hello, world!")
Hello, world!
jshell>

第一行是您在jshell命令提示符下输入的代码;第二行是输出。打印输出后,jshell提示符返回,可以输入另一个 Java 表达式进行求值。

Tip

Java 11 引入了运行单一 Java 源代码文件的能力。您仍然需要用 main 方法定义一个类。例如,您可以用前面列出的源代码创建一个名为HelloWorld.java的文件,并在命令行上运行“java HelloWorld.java”。Java 将编译并执行代码。您也可以像使用普通 Java 命令一样使用--class-path--module-path这样的选项。您可以使用--source选项来指定源代码的 Java 版本兼容性。例如,要指定 Java 17,您可以在命令行上运行“java --source 17 HelloWorld.java”。

JDK 附带了一个 JShell 命令行工具和JShell API。API 也支持该工具支持的所有特性。也就是说,您可以使用该工具或使用 API 以编程方式运行代码片段。在这个讨论中,你应该能够利用上下文来区分这两者。这一章的大部分都在解释这个工具。最后,我们用一个例子来描述这个 API。

JShell 不是一种新的语言或新的编译器。它是一个工具和 API,用于交互式访问 Java 编程语言。对于初学者来说,它提供了一种快速探索 Java 编程语言的方法。对于有经验的开发人员来说,它提供了一种快速查看代码片段结果的方法,而不必编译和运行整个程序。它还提供了一种使用增量方法快速开发原型的方法。您添加一段代码,获得即时反馈,然后添加另一段代码,直到原型完成。

JShell 架构

Java 编译器本身不识别诸如方法声明或变量声明之类的片段。只有类和import语句可以是顶级构造,它们可以独立存在。其他类型的代码片段必须是类的一部分。JShell 允许您执行 Java 代码片段,并允许您对它们进行改进。

当前 JShell 架构的指导原则是使用现有的 Java 语言支持和 JDK 中的其他 Java 技术来保持它与该语言的当前和未来版本的兼容性。随着 Java 语言的不断发展,它在 JShell 中的支持也将不断发展,只需对 JShell 实现做很少或不做任何修改。图 23-2 展示了 JShell 的高层架构。

img/323069_3_En_23_Fig2_HTML.jpg

图 23-2

JShell 架构

JShell 工具使用 JLine 的版本 2,这是一个用于处理控制台输入的 Java 库。标准的 JDK 编译器不知道如何解析和编译 Java 代码片段。因此,JShell 实现有自己的解析器来解析代码片段并确定代码片段的类型,例如,方法声明、变量声明等。一旦确定了代码片段类型,就使用以下规则将代码片段包装在合成类中:

  • 导入语句按“原样”使用也就是说,所有的import语句都“按原样”放在合成类的顶部。

  • 变量、方法和类声明成为合成类的静态成员。

  • 表达式和语句包装在合成类的合成方法中。

所有合成类都属于一个名为REPL的包。一旦代码片段被包装,标准 Java 编译器就会使用编译器 API 对包装后的源代码进行分析和编译。编译器将包装后的字符串格式的源代码作为输入,并将其编译成字节码,存储在内存中。生成的字节码通过套接字发送到运行 JVM 的远程进程进行加载和执行。有时,加载到远程 JVM 中的现有代码片段需要被 JShell 工具替换,这是使用 Java 调试器 API 完成的。

启动 JShell 工具

JDK 17 附带了一个 JShell 工具,它位于JDK_HOME\bin目录中。这个工具被命名为jshell。如果你在 Windows 上的C:\java17目录下安装了 JDK 17,你会有一个名为C:\java17\bin\jshell.exe的可执行文件,这就是 JShell 工具。要启动 JShell 工具,您需要打开命令提示符并输入jshell命令:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell>

在命令提示符下输入jshell命令可能会给你一个错误:

C:\JavaFun>jshell
'jshell' is not recognized as an internal or external command,
operable program or batch file.
C:\JavaFun>

此错误表明JDK_HOME\bin目录没有包含在您计算机上的PATH环境变量中。如果你在C:\java17目录下安装了 JDK 17,那么JDK_HOME就是你的C:\java17。要修复这个错误,要么在PATH环境变量中包含C:\java17\bin目录,要么使用jshell命令的完整路径,即C:\java17\bin\jshell。以下命令序列显示了如何在 Windows 上设置PATH环境变量并运行JShell工具:

C:\JavaFun>SET PATH=C:\java17\bin;%PATH%
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell>

以下命令显示了如何使用jshell命令的完整路径来启动该工具:

C:\JavaFun>C:\java17\bin\jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell>

jshell成功启动时,它会打印一条包含其版本信息的欢迎消息。它还打印命令,即/help intro。您可以使用此命令打印工具本身的简短介绍:

jshell> /help intro
|
|  intro
|
|  The jshell tool allows you to execute Java code, getting immediate results.
|  You can enter a Java definition (variable, method, class, etc), like:  int x = 8
|  or a Java expression, like:  x + x
|  or a Java statement or import.
|  These little chunks of Java code are called 'snippets'.
|
|  There are also jshell commands that allow you to understand and
|  control what you are doing, like:  /list
|
|  For a list of commands: /help
jshell>

如果您需要工具方面的帮助,您可以在jshell上输入命令/help来打印命令列表及其简短描述:

jshell> /help
<The output is not shown here.>
jshell>

Tip

NetBeans 和其他 ide 集成了对 JShell 工具的支持。您可以从 NetBeans IDE 中打开 JShell 提示符,方法是选择“工具”“➤”“打开 Java 平台外壳”菜单。NetBeans JShell 提示符为您提供了jshell命令行工具的所有功能以及更多功能——全部使用 UI 选项。例如,NetBeans 允许您使用其“保存到类”工具栏选项将所有代码片段保存为一个类。它还允许您像在普通 Java 编辑器中一样自动完成代码。IntelliJ IDEA 有一个内置的 JShell,Eclipse 和 Visual Studio 代码有在 IDE 中使用 JShell 的扩展。

您可以在jshell命令中使用几个命令行选项,将值传递给工具本身。例如,您可以将值传递给用于解析和编译代码片段的编译器,以及用于执行/评估代码片段的远程 JVM。运行带有--help选项的jshell程序,查看所有可用标准选项的列表。使用--help-extra-X选项运行它,查看所有可用的非标准选项列表。例如,使用这些选项,您可以为JShell工具设置类路径和模块路径。我们将在本章后面解释这些选项。

您还可以使用命令行--start选项定制jshell工具的启动脚本。您可以使用DEFAULTPRINTING作为该选项的参数。DEFAULT参数以几个import语句开始jshell,所以在使用jshell时不需要导入常用的类。以下两个命令以同样的方式启动jshell:

  • jshell

  • jshell --start DEFAULT

您可以使用System.out.println()方法将消息打印到标准输出。您可以使用带有PRINTING参数的--start选项来启动jshell,这将包括所有版本的System.out.print()System.out.println()System.out.printf()方法作为print()println()printf()顶级方法。这将允许您在jshell上使用print()println()printf()方法,而不是它们更长的版本System.out.print()System.out.println()System.out.printf():

C:\JavaFun>jshell --start PRINTING
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> println("hello")
hello
jshell>

您可以在启动jshell时重复--start选项,以包含默认的import语句和打印方法:

C:\JavaFun>jshell --start DEFAULT --start PRINTING
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell>

退出 JShell 工具

要退出jshell,在jshell提示符下输入/exit并按回车键。该命令打印一条再见消息,退出该工具,并返回到命令提示符:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /exit
|  Goodbye
C:\JavaFun>

这个工具在很多方面都是宽容的。如果在 Java 结构中使用了不支持的关键字,它就会忽略它。您可以使用部分命令。如果您输入的部分命令可以自动完成为唯一的命令名,该工具将像您输入完整命令一样工作。例如,/edit/exit是两个以/e开头的命令。如果您输入的是/ex而不是/exitjshell会将其解释为/exit命令:

jshell> /ex
|  Goodbye
C:\JavaFun>

如果您输入/e,您将收到一个错误,因为有多个可能的命令以/e开头:

jshell> /e
|  Command: '/e' is ambiguous: /edit, /exit, /env
|  Type /help for help.
jshell>

什么是代码片段和命令?

您可以使用 JShell 工具来

  • 评估 Java 代码片段,在 JShell 术语中简称为片段

  • 执行命令,这些命令用于查询 JShell 状态和设置 JShell 环境。

为了区分命令和片段,所有命令都以斜杠(/)开头。你已经在前面的章节中看到了一些,比如/exit/help。命令用于与工具本身进行交互,例如自定义其输出、打印帮助、退出工具以及打印命令和代码片段的历史记录。这本书后面会解释更多的命令。如果你有兴趣学习所有可用的命令,使用/help命令。

使用 JShell 工具,您可以一次编写一段 Java 代码并对其进行评估。这些代码片段被称为片段。代码片段必须遵循在 Java 语言规范中指定的语法。片段可以是

  • 进口申报

  • 类声明

  • 接口声明

  • 方法声明

  • 字段声明

  • 声明

  • 公式

Tip

您可以在 JShell 中使用所有的 Java 语言结构,除了包声明。JShell 中的所有代码片段都出现在名为REPL的内部包和内部合成类中。

JShell 工具知道您何时完成了代码片段的输入。当您按 Enter 键时,如果代码片段完成,该工具将执行它,或者将您带到下一行,等待您完成代码片段。如果一行以...>开头,说明代码片段不完整,需要输入更多的文本来完成代码片段。更多输入的默认提示是...>,可以定制。这里有几个例子:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> 2 + 2
$1 ==> 4
jshell> 2 +
   ...> 2
$2 ==> 4
jshell> 2
$3 ==> 2
jshell>

当你输入2 + 2并按回车键时,jshell将其视为一个完整的片段(一个表达式)。它对表达式求值并输出反馈,表明表达式的值为 4,结果被赋给一个名为$1的变量。名为$1的变量是由工具自动生成的。这本书将在后面更详细地解释工具生成的变量。当您输入2 +并按回车键时,jshell会提示您输入更多内容,因为2 +在 Java 中并不是一个完整的代码片段。当您在第二行输入2时,代码片段就完成了;jshell评估片段并打印反馈。当您输入2并按回车键时,jshell会评估代码片段,因为2本身就是一个完整的表达式。

评估表达式

您可以在jshell中执行任何有效的 Java 表达式。以下示例计算两个数字相加和相乘的表达式:

jshell> 2 + 2
$1 ==> 4
jshell> 9.0 * 6
$2 ==> 54.0

评估表达式时,如果表达式评估为一个值,jshell会打印反馈。在这些情况下,2 + 2计算为 4,9.0 * 6计算为54.0。表达式的值被赋给一个变量。反馈包含变量的名称和表达式的值。在第一种情况下,反馈$1 ==> 4意味着表达式2 + 2的计算结果为4,结果被赋给一个名为$1的变量。类似地,表达式9.0 * 6被评估为54.0,并且值被分配给名为$2的变量。您可以在其他表达式中使用这些变量名。只需输入它们的名称,就可以打印它们的值:

jshell> $1
$1 ==> 4
jshell> $2
$2 ==> 54.0
jshell> System.out.println($1)
4
jshell> System.out.println($2)
54.0

Tip

jshell中,你不需要像在 Java 程序中那样用分号结束一个语句。该工具将为您插入缺少的分号。

在 Java 中,每个变量都有一个数据类型。在这些例子中,名为$1$2的变量的数据类型是什么?在 Java 中,2 + 2的计算结果是int,,而9.0 * 6的计算结果是double。因此,$1$2变量的数据类型应该分别是intdouble。你如何证实这一点?让我们先来硬的。您可以将$1$2强制转换为Object并对它们调用getClass()方法,这将为您提供IntegerDouble。请注意,在这些示例中,当您将intdouble类型的原始值转换为Object类型时,它们被装箱为IntegerDouble引用类型:

jshell> 2 + 2
$1 ==> 4
jshell> 9.0 * 6
$2 ==> 54.0
jshell> ((Object)$1).getClass()
$3 ==> class java.lang.Integer
jshell> ((Object)$2).getClass()
$4 ==> class java.lang.Double
jshell>

有一种更简单的方法来确定由jshell创建的变量的数据类型——您只需告诉jshell给你详细的反馈,它将打印它创建的变量的数据类型以及更多!以下命令将反馈模式设置为verbose,并评估相同的表达式:

jshell> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$1 ==> 4
|  created scratch variable $1 : int
jshell> 9.0 * 6
$2 ==> 54.0
|  created scratch variable $2 : double
jshell>

注意,jshell将名为$1$2的变量的数据类型分别打印为intdouble。对于初学者来说,使用-retain选项执行以下命令会很有帮助,因此详细反馈模式会在jshell会话中持续:

jshell> /set feedback -retain verbose

您也可以使用/vars命令列出在jshell中定义的所有变量:

jshell> /vars
|    int $1 = 4
|    double $2 = 54.0
jshell>

如果想再次使用normal反馈模式,使用以下命令:

jshell> /set feedback -retain normal
|  Feedback mode: normal
Jshell>

你并不局限于计算简单的表达式,比如2 + 2。你可以计算任何 Java 表达式。以下示例评估字符串串联表达式并使用String类的方法。它还向您展示了如何使用for循环:

jshell> "Hello " + "world! " + 2017
$1 ==> "Hello world! 2017"
jshell> $1.length()
$2 ==> 17
jshell> $1.toUpperCase()
$3 ==> "HELLO WORLD! 2017"
jshell> $1.split(" ")
$4 ==> String[3] { "Hello", "world!", "2017" }
jshell> for(String s : $4) {
   ...>     System.out.println(s);
   ...> }
Hello
world!
2017
Jshell>

列表片段

无论你在jshell中输入什么,都会成为片段的一部分。每个代码片段都分配有一个唯一的代码片段 ID,您可以使用它在以后引用代码片段,例如,删除代码片段。/list命令列出了所有代码片段。它有以下形式:

  • /list

  • /list -all

  • /list -start

  • /list <snippet-name>

  • /list <snippet-id>

不带参数/选项的/list命令打印所有用户输入的活动片段,这些片段也可能是使用/open命令从文件中打开的。

使用-all选项列出所有片段——活动的、非活动的、错误的和启动的。

使用-start选项仅列出启动代码片段。启动代码片段被缓存,-start选项打印缓存的代码片段。即使您在当前会话中删除了启动代码片段,它也会打印它们。

一些代码片段类型有一个名称(例如,变量/方法声明),所有代码片段都有一个 ID。使用带有/list命令的代码片段的名称或 ID 打印由该名称或 ID 标识的代码片段。/list命令以下列格式打印代码片段列表:

<snippet-id> : <snippet-source-code>
<snippet-id> : <snippet-source-code>
<snippet-id> : <snippet-source-code>
...

JShell工具生成唯一的代码片段 id。启动片段分别为s1s2s3...123...为有效片段;以及e1e2e3...为错误片段。下面的jshell会话向您展示了如何使用/list命令列出代码片段。示例使用/drop命令删除使用代码段名称和代码段 ID 的代码段:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /list
jshell> 2 + 2
$1 ==> 4
jshell> /list
   1 : 2 + 2
jshell> int x = 100
x ==> 100
jshell> /list
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -start
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
jshell> string str = "String type is misspelled as string"
|  Error:
|  cannot find symbol
|    symbol:   class string
|  string str = "String type is misspelled as string";
|  ^----^
jshell> /list
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
  e1 : string str = "String type is misspelled as string";
jshell> /drop 1
|  dropped variable $1
jshell> /list
   2 : int x = 100;
jshell> /drop x
|  dropped variable x
jshell> /list
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
  e1 : string str = "String type is misspelled as string";
jshell> /exit
|  Goodbye

变量、方法和类的名称成为代码片段的名称。请注意,Java 允许您拥有同名的变量、方法和类,因为它们出现在自己的名称空间中。您可以使用这些实体的名称,使用/list命令将其列出:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /list x
|  No such snippet: x
jshell> int x = 100
x ==> 100
jshell> /list x
   1 : int x = 100;
jshell> void x() {}
|  created method x()
jshell> /list x
   1 : int x = 100;
   2 : void x() {}
jshell> void x(int n) {}
|  created method x(int)
jshell> /list x
   1 : int x = 100;
   2 : void x() {}
   3 : void x(int n) {}
jshell> class x {}
|  created class x
jshell> /list x
   1 : int x = 100;
   2 : void x() {}
   3 : void x(int n) {}
   4 : class x {}
jshell> /exit
|  Goodbye

编辑片段

JShell 工具提供了几种编辑代码片段和命令的方法。您可以使用表 23-1 中列出的导航键在命令行上导航,同时在jshell中输入片段和命令。您可以使用表 23-2 中列出的按键来编辑jshell中一行输入的文本。

表 23-2

在 JShell 工具中修改文本的键

|

钥匙

|

描述

| | --- | --- | | 删除 | 删除光标下的字符。 | | 退格 | 删除光标前的字符。 | | Ctrl+K | 删除从光标到行尾的文本。 | | Meta+D (gold Alt+D) | 删除从光标到单词末尾的文本。 | | Ctrl+W 组合键 | 删除光标处前一个空白区域的文本。 | | Ctrl+Y | 将最近删除的文本粘贴(或拉)到行中。 | | Meta+Y(或 Alt+Y) | 在 Ctrl+Y 之后,该组合键循环显示先前删除的文本。 |

表 23-1

在 JShell 工具中编辑时的导航键

|

钥匙

|

描述

| | --- | --- | | 进入 | 进入当前行。 | | 向左箭头 | 向后移动一个字符。 | | 右箭头 | 向前移动一个字符。 | | Ctrl+A | 移动到行首。 | | Ctrl+E 组合键 | 移动到行尾。 | | Meta+B(或 Alt+B) | 向后移动一个单词。 | | Meta+F(或 Alt+F) | 向前移动一个单词。 |

很难在jshell中编辑多行代码片段,即使您可以访问丰富的编辑组合键。工具设计者意识到了这个问题,提供了内置的代码片段编辑器。您可以配置该工具,以使用您选择的特定于平台的代码片段编辑器。有关如何设置自己的编辑器的更多信息,请参考“设置代码片段编辑器”一节。

您需要使用/edit命令来开始编辑代码片段。该命令有三种形式:

  • /edit <snippet-name>

  • /edit <snippet-id>

  • /edit

您可以使用代码段名称或代码段 ID 来编辑特定的代码段。不带参数的/edit命令在编辑器中打开所有活动代码段进行编辑。默认情况下,/edit命令会打开一个名为 JShell Edit Pad 的内置编辑器,如图 23-3 所示。

img/323069_3_En_23_Fig3_HTML.png

图 23-3

内置的 JShell 编辑器称为 JShell Edit Pad

JShell Edit Pad 是用 Swing 编写的,它显示了一个带有一个JTextArea和三个JButtonJFrame,如果您编辑代码片段,请确保在退出窗口之前单击 Accept 按钮,以便编辑生效。如果您取消或退出编辑器而不接受更改,您的编辑将会丢失。

如果您知道变量、方法或类的名称,您可以使用其名称对其进行编辑。下面的jshell会话创建了一个具有相同名称x的变量、方法和类,并使用/edit x命令一次性编辑它们:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> int x = 100
x ==> 100
jshell> void x(){}
|  created method x()
jshell> void x (int n) {}
|  created method x(int)
jshell> class x{}
|  created class x
jshell> 2 + 2
$5 ==> 4
jshell> /edit x

/edit x命令打开 JShell 编辑板中所有名为x的代码片段,如图 23-4 所示。您可以编辑这些片段,接受更改,并退出编辑,继续进行jshell会话。

img/323069_3_En_23_Fig4_HTML.png

图 23-4

按名称编辑片段

重新运行以前的片段

在像jshell这样的命令行工具中,您可能经常想要重新运行之前的代码片段。您可以使用向上/向下箭头浏览代码片段/命令历史记录,然后在进入上一个代码片段/命令时按 Enter 键。您也可以使用以下三个命令之一来重新运行以前的代码片段(不是命令):

  • /!

  • /<snippet-id>

  • /-<n>

/!命令重新运行最后一段代码。/<snippet-id>命令重新运行由<snippet-id>标识的片段。/-<n>命令重新运行n的最后一个片段。例如,/-1重新运行最后一个代码段,/-2重新运行倒数第二个代码段,依此类推。/!/-1命令具有相同的效果——它们都重新运行上一个代码片段。

声明变量

您可以像在 Java 程序中一样在jshell中声明变量。变量声明可能出现在顶级、方法内部,或者作为类中的字段声明。顶级变量声明中不允许使用staticfinal修饰符。如果您使用它们,它们将被忽略并发出警告。static修饰符指定了一个类上下文,final修饰符限制你改变变量值。不允许使用这些修饰符,因为该工具允许您声明想要通过随时间改变其值来进行试验的独立变量。以下示例说明了如何声明变量:

jshell> int x
x ==> 0
jshell> int y = 90
y ==> 90
jshell> side = 90
|  Error:
|  cannot find symbol
|    symbol:   variable side
|  side = 90
|  ^--^
jshell> static double radius = 2.67
|  Warning:
|  Modifier 'static'  not permitted in top-level declarations, ignored
|  static double radius = 2.67;
|  ^----^
radius ==> 2.67
jshell> String str = new String("Hello")
str ==> "Hello"
jshell>

在顶级表达式中使用未声明的变量会产生错误。注意在前面的例子中使用了一个名为side的未声明变量,这产生了一个错误。稍后我们将向您展示,您可以在方法体中使用未声明的变量。

也可以改变变量的数据类型。您可以将一个名为x的变量声明为int,并在以后将其重新声明为doubleString。以下示例显示了此功能:

jshell> int x = 10;
x ==> 10
jshell> int y = x + 2;
y ==> 12
jshell> double x = 2.71
x ==> 2.71
jshell> y
y ==> 12
jshell> String x = "Hello"
x ==> "Hello"
jshell> y
y ==> 12
jshell>

请注意,当数据类型或x的值改变时,名为y的变量的值没有改变或没有被重新计算。

您也可以使用/drop命令删除一个变量,该命令将变量名作为一个参数。以下命令将删除名为x的变量:

jshell> /drop x

您可以使用/vars命令列出jshell中的所有变量。它将列出用户声明的变量和由jshell自动声明的变量,这发生在jshell评估结果承载表达式时。该命令具有以下形式:

  • /vars

  • /vars <variable-name>

  • /vars <variable-snippet-id>

  • /vars -start

  • /vars -all

不带参数的命令列出当前会话中的所有活动变量。如果您使用代码段名称或 ID,它会列出具有该代码段名称或 ID 的变量声明。如果将它与-start选项一起使用,它会列出添加到启动脚本中的所有变量。如果将它与-all选项一起使用,它会列出所有变量,包括失败、覆盖、丢弃和启动。以下示例向您展示了如何使用/vars命令:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /vars
jshell> 2 + 2
$1 ==> 4
jshell> /vars
|    int $1 = 4
jshell> int x = 20;
x ==> 20
jshell> /vars
|    int $1 = 4
|    int x = 20
jshell> String str = "Hello";
str ==> "Hello"
jshell> /vars
|    int $1 = 4
|    int x = 20
|    String str = "Hello"
jshell> double x = 90.99;
x ==> 90.99
jshell> /vars
|    int $1 = 4
|    String str = "Hello"
|    double x = 90.99
jshell> /drop x
|  dropped variable x
jshell> /vars
|    int $1 = 4
|    String str = "Hello"
jshell>

导入报表

可以在jshell中使用import语句。回想一下,在 Java 程序中,java.lang包中的所有类型都是默认导入的。要使用其他包中的类型,您需要在您的编译单元中添加适当的import语句。我们从一个例子开始。我们尝试创建三个对象:一个String、一个List<Integer>和一个ZonedDateTime。注意String类在java.lang包中;ListInteger类分别在java.utiljava.lang包中;ZonedDateTime类在java.time包中:

jshell> String str = new String("Hello")
str ==> "Hello"
jshell> List<Integer> nums = List.of(1, 2, 3, 4, 5)
nums ==> [1, 2, 3, 4, 5]
jshell> ZonedDateTime now = ZonedDateTime.now()
|  Error:
|  cannot find symbol
|    symbol:   class ZonedDateTime
|  ZonedDateTime now = ZonedDateTime.now();
|  ^-----------^
|  Error:
|  cannot find symbol
|    symbol:   variable ZonedDateTime
|  ZonedDateTime now = ZonedDateTime.now();
|                      ^-----------^
jshell>

如果您试图使用java.time包中的ZonedDateTime类,这些示例会产生一个错误。当我们试图创建一个List时,我们也会遇到类似的错误,因为它在java.util包中,默认情况下,它不会被导入到 Java 程序中。

JShell 工具的唯一目的是让开发人员在评估代码片段时更加轻松。为了实现这个目标,默认情况下,该工具从几个包中导入所有类型。那些类型被导入的默认包是什么?您可以使用/imports命令打印出jshell中所有活动imports的列表:

jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
jshell>

注意默认的import语句,它从java.util包中导入所有类型。这就是你可以不用进口就能使用List的原因。您也可以将自己的导入添加到jshell。下面的例子展示了如何导入并使用ZonedDateTime类。当jshell打印带有时区的当前日期值时,您将得到不同的输出:

jshell> /imports
|    import java.util.*
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.util.concurrent.*
|    import java.util.prefs.*
|    import java.util.regex.*
jshell> import java.time.*
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
|    import java.time.*
jshell> ZonedDateTime now = ZonedDateTime.now()
now ==> 2017-08-19T13:01:33.060708200-05:00[America/Chicago]
jshell>

请注意,当您退出会话时,您添加到jshell会话的任何导入都将丢失。您还可以删除import语句——默认导入和您添加的语句。您需要知道代码段 ID 才能删除代码段。启动片段的 id 有s1s2s3等。;对于用户定义的片段,它们是 1、2、3 等。以下示例向您展示了如何在jshell中添加和删除import语句:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> import java.time.*
jshell> List<Integer> list = List.of(1, 2, 3, 4, 5)
list ==> [1, 2, 3, 4, 5]
jshell> ZonedDateTime now = ZonedDateTime.now()
now ==> 2017-02-19T21:08:08.802099-06:00[America/Chicago]
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
jshell> /drop s5
jshell> /drop 1
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
jshell> List<Integer> list2 = List.of(1, 2, 3, 4, 5)
|  Error:
|  cannot find symbol
|    symbol:   class List
|  List<Integer> list2 = List.of(1, 2, 3, 4, 5);
|  ^--^
|  Error:
|  cannot find symbol
|    symbol:   variable List
|  List<Integer> list2 = List.of(1, 2, 3, 4, 5);
|                        ^--^
jshell> import java.util.*
|    update replaced variable list, reset to null
jshell> List<Integer> list2 = List.of(1, 2, 3, 4, 5)
list2 ==> [1, 2, 3, 4, 5]
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
  e1 : List<Integer> list2 = List.of(1, 2, 3, 4, 5);
   4 : import java.util.*;
   5 : List<Integer> list2 = List.of(1, 2, 3, 4, 5);
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
|    import java.util.*
jshell>

方法声明

可以在jshell中声明和调用方法。您可以声明顶级方法,这些方法是直接在jshell中输入的,并且不在任何类中。也可以用方法声明类(见下一节)。在本节中,我们将向您展示如何声明和调用顶级方法。也可以调用现有类的方法。下面的例子声明了一个名为square()的方法,并调用它:

jshell> long square(int n) {
   ...>    return n * n;
   ...> }
|  created method square(int)
jshell> square(10)
$2 ==> 100
jshell> long n2 = square(37)
n2 ==> 1369
jshell>

方法体中允许前向引用。也就是说,您可以在方法体中引用尚未声明的方法或变量。在定义所有缺少的引用之前,不能调用正在声明的方法:

jshell> long multiply(int n) {
   ...>     return multiplier * n;
   ...> }
|  created method multiply(int), however, it cannot be invoked until variable multiplier is declared
jshell> multiply(10)
|  attempted to call method multiply(int) which cannot be invoked until variable multiplier is declared
jshell> int multiplier = 2
multiplier ==> 2
jshell> multiply(10)
$6 ==> 20
jshell> void printCube(int n) {
   ...>     System.out.printf("Cube of %d is %d.%n", n, cube(n));
   ...> }
|  created method printCube(int), however, it cannot be invoked until method cube(int) is declared
jshell> long cube(int n) {
   ...>     return n * n * n;
   ...> }
|  created method cube(int)
jshell> printCube(10)
Cube of 10 is 1000.
jshell>

这个例子声明了一个名为multiply(int n)的方法。它将参数与一个名为multiplier的变量相乘,这个变量还没有声明。注意声明这个方法后的反馈。反馈明确指出,在声明multiplier变量之前,不能调用multiply()方法。调用方法会生成错误。后来,声明了multiplier变量,并成功调用了multiply()方法。

Tip

还可以使用前向引用声明递归方法。

类型声明

您可以像在 Java 中一样在jshell中声明所有类型,比如类、接口、枚举和注释。下面的jshell会话创建一个名为Counter的类,创建它的对象,并调用它的方法:

jshell> class Counter {
   ...>     private int counter;
   ...>     public synchronized int next() {
   ...>         return ++counter;
   ...>     }
   ...>
   ...>     public int current() {
   ...>         return counter;
   ...>     }
   ...> }
|  created class Counter
jshell> Counter c = new Counter();
c ==> Counter@25bbe1b6
jshell> c.current()
$3 ==> 0
jshell> c.next()
$4 ==> 1
jshell> c.next()
$5 ==> 2
jshell> c.current()
$6 ==> 2
jshell>

您可以使用/types命令来打印jshell中所有已声明类型的列表。该命令具有以下形式:

  • /types

  • /types <type-name>

  • /types <snippet-id>

  • /types -start

  • /types -all

不带参数的命令列出当前活动的jshell类、接口、枚举和注释。具有类型名称和代码段 ID 参数的命令分别列出具有指定名称和指定代码段 ID 的类型。带有-start选项的命令列出了自动添加的启动类型。带-all选项的命令列出所有类型,包括失败、覆盖、丢弃和启动。接下来的jshell是之前示例会话的延续;它显示了如何打印在jshell会话中定义的所有活动类型:

jshell> /types
|    class Counter
jshell>

Counter班小。您可能会很快意识到在命令行上输入较大类的源代码并不容易。您可能希望使用您喜欢的 Java 源代码编辑器(如 NetBeans)来编写源代码,并在jshell中快速测试您的类。您可以使用/open命令在jshell中打开一个源代码文件作为源输入。语法如下:

/open <file-path>

您可以在bj9f/src/jdojo.jshell/Counter.java文件中找到Counter类的源代码。下面的jshell会话向您展示如何在jshell中打开保存的Counter.java文件。假设你已经在 Windows 上的C:\中保存了这本书的源代码。如果您使用的是另一个操作系统,只需遵循您的操作系统和目录结构的文件路径命名约定,即可使用以下示例:

jshell> /open C:\bj9f\src\jdojo.jshell\Counter.java
jshell> Counter c = new Counter()
c ==> Counter@25bbe1b6
jshell> c.current()
$3 ==> 0
jshell> c.next()
$4 ==> 1
jshell> c.next()
$5 ==> 2
jshell> c.current()
$6 ==> 2
jshell>

注意,Counter类的源代码不包含包声明,因为jshell不允许在包中声明类(或任何类型)。在jshell中声明的所有类型都被认为是内部合成类的静态类型。但是,您可能想要测试您自己的包中的类。你可以在jshell中使用一个已经编译好的类,它在一个包中。当您使用库来开发您的应用程序,并且想要通过针对库类编写代码片段来试验您的应用程序逻辑时,您通常会需要它。您将需要使用/env命令设置类路径,这样您的类可能会被找到。

本书的源代码中包含了com.jdojo.jshell包中的一个Person类。类声明如清单 23-2 所示。

// Person.java
package com.jdojo.jshell;
public class Person {
    private String name;
    public Person() {
        this.name = "Unknown";
    }
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

Listing 23-2The Source Code for a Person Class

下面的jshell会话设置 Windows 上的类路径,假设这本书的源代码存储在C:\中。如果您的操作系统的类路径字符串和计算机上的源代码位置与假定的不同,请使用它们的语法:

jshell> /env -class-path C:\JavaFun\build\modules\jdojo.jshell
|  Setting new options and restoring state.
jshell> Person guy = new Person("Martin Guy Crawford")
|  Error:
|  cannot find symbol
|    symbol:   class Person
|  Person guy = new Person("Martin Guy Crawford");
|  ^----^
|  Error:
|  cannot find symbol
|    symbol:   class Person
|  Person guy = new Person("Martin Guy Crawford");
|                   ^----^

你知道这个错误的原因吗?我们使用了简单的类名Person,没有导入它;而jshell却找不到类。我们需要导入Person类或者使用它的完全限定名。以下是修复此错误的jshell会话的延续:

jshell> import com.jdojo.jshell.Person
jshell> Person guy = new Person("Martin Guy Crawford")
guy ==> com.jdojo.jshell.Person@192b07fd
jshell> guy.getName()
$9 ==> "Martin Guy Crawford"
jshell> guy.setName("Forrest Butts")
jshell> guy.getName()
$11 ==> "Forrest Butts"
jshell>

设置执行环境

在上一节中,您学习了如何使用/env命令设置类路径。该命令可用于设置执行上下文的许多其他组件,如模块路径。还可以用它来解析模块,这样就可以在jshell上的模块中使用类型。其完整语法如下:

/env [-class-path <path>] [-module-path <path>] [-add-modules <modules>]
[-add-exports <m/p=n>]

不带参数的/env命令打印当前执行上下文的值。-class-path选项设置类路径。-module-path选项设置模块路径。-add-modules选项将模块添加到默认的根模块集中,因此它们可以被解析。- add-exports选项将一个模块中未导出的包导出到一组模块中。这些选项的含义与使用javacjava命令时的含义相同。

Tip

在命令行上,这些选项必须以两个破折号(连字符)开头,例如--module-path。在jshell中,他们可以以一个破折号或两个破折号开始。比如-module-path--module-pathjshell都是允许的。

当您设置执行上下文时,当前会话将被重置,并且当前会话中以前执行的所有代码段都将以安静模式重播。也就是说,不会显示重播的片段。但是,将显示重放过程中的错误。

您可以使用/env/reset/reload命令设置执行上下文。这些命令中的每一个都有不同的效果。上下文选项如-class-path-module-path的意思是一样的。您可以使用命令/help context列出所有可用于设置执行上下文的选项。

让我们看一个使用/env命令来使用模块相关设置的例子。你在第三章创建了一个jdojo.intro模块。该模块包含一个名为com.jdojo.intro的包,但它不导出该包。现在,您想要调用非导出包中的Welcome类的静态main(String[] args)方法。以下是您需要在jshell中执行的步骤:

  1. 设置模块路径,以便找到该模块。

  2. 通过将模块添加到默认的根模块集中来解析该模块。您可以通过使用/env命令的-add-modules选项来完成此操作。

  3. 使用-add-exports命令导出包。在jshell中输入的代码片段在一个未命名的模块中执行,所以您需要使用ALL-UNNAMED关键字将包导出到所有未命名的模块。如果您没有在-add-exports选项中提供目标模块,则假定为ALL-UNNAMED,并且该包被导出到所有未命名的模块。

  4. 如果您想在代码片段中使用简单的名称,可以选择导入com.jdojo.intro.Welcome类。

  5. 现在,您将能够从jshell调用Welcome.main()方法。

下面的jshell会话将向您展示如何执行这些步骤。假设您正在以C:\JavaFun作为当前目录启动jshell会话,并且C:\JavaFun\build\modules\jdojo.intro目录包含了jdojo.intro模块的编译代码。如果您的目录结构和当前目录不同,请用您的目录路径替换会话中使用的目录路径:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /env -module-path build\modules\jdojo.intro
|  Setting new options and restoring state.
jshell> /env -add-modules jdojo.intro
|  Setting new options and restoring state.
jshell> /env -add-exports jdojo.intro/com.jdojo.intro=ALL-UNNAMED
|  Setting new options and restoring state.
jshell> import com.jdojo.intro.Welcome
jshell> Welcome.main(null)
Welcome to Java 17!
jshell> /env
|     --add-modules jdojo.intro
|     --module-path build\modules\jdojo.intro
|     --add-exports jdojo.intro/com.jdojo.intro=ALL-UNNAMED
jshell>

没有已检查的异常

在 Java 程序中,如果您调用一个抛出检查异常的方法,您必须使用一个try-catch块或通过添加一个throws子句来处理这些异常。JShell 工具应该是评估代码片段的一种快速而简单的方法,所以您不需要在代码片段中处理检查异常。如果一个代码片段在执行时抛出一个检查过的异常,jshell将打印堆栈跟踪并继续:

jshell> FileReader fr = new FileReader("secrets.txt")
|   java.io.FileNotFoundException thrown: secrets.txt (The system cannot find the file specified)
|        at FileInputStream.open0 (Native Method)
|        at FileInputStream.open (FileInputStream.java:196)
|        at FileInputStream.<init> (FileInputStream.java:139)
|        at FileInputStream.<init> (FileInputStream.java:94)
|        at FileReader.<init> (FileReader.java:58)
|        at (#1:1)
jshell>

这个代码片段抛出了一个FileNotFoundException,因为当前目录中不存在一个名为secrets.txt的文件。如果该文件存在,您可以创建一个FileReader,而不必使用try-catch块。请注意,如果您尝试在方法中使用这个代码片段,则适用普通的 Java 语法规则,并且您的方法声明将不会编译:

jshell> void readSecrets() {
   ...> FileReader fr = new FileReader("secrets.txt");
   ...> // More code goes here
   ...> }
|  Error:
|  unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
|  FileReader fr = new FileReader("secrets.txt");
|                  ^---------------------------^
jshell>

自动完成

JShell工具具有自动完成功能,您可以通过输入部分文本并按 Tab 键来调用该功能。当您输入命令或代码片段时,此功能可用。该工具将检测上下文并帮助您自动完成命令。当有多个可能性时,它会显示所有可能性,您需要手动输入其中一个。当它发现一个独特的可能性时,它将完成文本。要查看自动完成快捷方式的完整描述,请使用/help shortcuts命令。有三种自动完成的组合键:

  • 标签

  • Shift+Tab+V

  • Shift+Tab+I

在表达式中间按 Tab 键,jshell将完成表达式或显示可能的选项。按 Shift+Tab+V 可以将表达式转换为变量声明。快捷方式中的 V 代表变量。按 Shift+Tab+I 可以导入无法解析的标识符的类型。我们将详细讨论这些快捷方式的示例。

以下是该工具寻找多种可能性的示例。需要输入/e并按 Tab 键。命令中的<Tab>表示需要按 Tab 键:

jshell> /e <Tab>
/edit /env /exit
<press tab again to see synopsis>
jshell> /e <Tab>
/edit
edit a source entry referenced by name or id
/env
view or change the evaluation context
/exit
exit jshell
<press tab again to see full documentation>
jshell>

该工具检测到您正在尝试输入命令,因为您的文本以斜杠(/)开头。有三个命令(/edit/env/exit)以/e开头,它们是为你打印的。现在,您需要通过输入命令的其余部分来完成命令。对于命令,如果您输入足够的文本使命令名唯一并按 enter 键,该工具将执行该命令。在这种情况下,可以输入/ed/en/ex并按回车键分别执行/edit/env/exit命令。如果在按 Tab 键后显示多个选项,可以再次按 Tab 键查看所有选项的说明。第三次按 Tab 会显示所有选项的完整文档。如果您尝试这样自动完成一个 Java 表达式,您可以查看整个 Javadoc for Java 实体,比如一个类的方法。

您可以输入斜线(/)并按 Tab 键查看所有可用的jshell命令列表:

jshell> /
/!          /?          /drop       /edit       /env        /exit       /help       /history
/imports    /list       /methods    /open       /reload     /reset      /save       /set
/types      /vars
<press tab again to see synopsis>

下面的代码片段创建了一个名为strString变量,初始值为"GoodBye":

jshell> String str = "GoodBye"
str ==> "GoodBye"

继续此jshell会话,输入str.并按 Tab 键:

jshell> str.<Tab>
charAt(                chars()                codePointAt(           codePointBefore(
codePointCount(        codePoints()           compareTo(             compareToIgnoreCase(
concat(                contains(              contentEquals(         endsWith(
equals(                equalsIgnoreCase(      getBytes(              getChars(
getClass()             hashCode()             indexOf(               intern()
isEmpty()              lastIndexOf(           length()               matches(
notify()               notifyAll()            offsetByCodePoints(    regionMatches(
replace(               replaceAll(            replaceFirst(          split(
startsWith(            subSequence(           substring(             toCharArray()
toLowerCase(           toString()             toUpperCase(           trim()
wait(
jshell> str.

这个代码片段打印了您可以在变量str上调用的String类的所有方法名。请注意,一些方法名称以()结尾,而其他方法名称仅以(结尾。这不是 bug。如果一个方法没有参数,它的名字后面会有一个()。如果一个方法有参数,它的名字后面会跟一个(

继续这个例子,输入str.sub并按 Tab 键:

jshell> str.sub <Tab>
subSequence(   substring(

这一次,该工具在String类中找到了两个以sub开头的方法。您可以输入整个方法调用str.substring(0, 4),然后按Enter来评估代码片段:

jshell> str.substring(0, 4)
$2 ==> "Good"

或者,您可以通过输入str.subs让工具自动完成方法名。当您输入str.subs并按 Tab 键时,该工具会完成方法名,插入一个(,并等待您输入方法的参数:

jshell> str.substring(
substring(
jshell> str.substring(

现在,您可以输入方法的参数并按 enter 键来计算表达式:

jshell> str.substring(0, 4)
$3 ==> "Good"
jshell>

当一个方法接受参数时,您很可能希望看到这些参数的类型。输入完整的方法/构造器名和左括号后,按 Tab 键可以看到方法的概要。在前面的例子中,如果您输入str.substring(并按 Tab,该工具将打印substring()方法的概要:

jshell> str.substring(
Signatures:
String String.substring(int beginIndex)
String String.substring(int beginIndex, int endIndex)
<press tab again to see documentation>
jshell> str.substring(

注意输出。它说如果你再次按下 Tab,它会显示出substring()方法的 Javadoc。在下面的提示中,我们再次按 Tab 键来打印 Javadoc。如果需要显示更多 Javadoc,请再次按 Tab 键。

有时,您输入一个表达式,并希望将表达式的值赋给适当类型的变量。有时你知道类型,有时你不知道。在您输入完整的表达式后,JShell工具将帮助您自动完成赋值部分。输入完整的表达式,然后按 Shift+Tab。现在,按 V,这将通过添加适当的变量类型并将光标放在可以输入变量名的位置来自动完成表达式赋值。按键的顺序如下:

  1. 按 Shift 键。

  2. 一直按住 Shift 并按 Tab。

  3. 释放标签。

  4. 松开换档。

  5. 按 v。

让我们走完这些步骤。在jshell中输入表达式2 + 2:

jshell> 2 + 2

现在,按前面列出的按键顺序。jshell自动完成赋值表达式,并等待您输入变量名:

jshell> int  = 2 + 2

光标正好位于=符号之前。输入x作为变量名,并按回车键:

jshell> int x = 2 + 2
x ==> 4
jshell>

让我们使用 Shift+Tab+I 快捷键来导入一个未解析标识符的缺失导入。您需要按以下顺序按下组合键:

  1. 按 Shift 键。

  2. 一直按住 Shift 并按 Tab。

  3. 释放标签。

  4. 松开换档。

  5. 普里斯岛。

  6. jshell将打印选项编号为 0、1、2、3 等的可能的导入报表。jshell等待您输入选项。

  7. 输入选项号,jshell将执行import语句。

假设您想使用java.time包中的LocalDate类。下面的jshell会话将向您展示如何使用快捷键导入java.time.LocalDate类。在jshell上输入LocalDate后,需要按 Shift+Tab+I 快捷键:

jshell> LocalDate
0: Do nothing
1: import: java.time.LocalDate
Choice:
Imported: java.time.LocalDate
jshell> LocalDate.now()
$1 ==> 2017-08-19

代码片段和命令的历史记录

JShell 维护您在所有会话中输入的所有命令和代码片段的历史记录。您可以使用上下箭头键浏览历史记录。您也可以使用/history命令打印您在当前会话中输入的所有内容的历史记录:

jshell> 2 + 2
$1 ==> 4
jshell> System.out.println("Hello")
Hello
jshell> /history
2 + 2
System.out.println("Hello")
/history
jshell>

此时,按一下向上箭头显示/history,按两下显示System.out.println("Hello"),按三下显示2 + 2。第四次按向上箭头将显示上次jshell会话输入的命令/片段。如果要执行之前输入的代码片段/命令,请使用向上箭头,直到显示所需的命令/代码片段,然后按 Enter 键执行。按下向下箭头可以导航到列表中的下一个命令或代码片段。假设您按下向上箭头五次,导航到倒数第五个代码片段/命令。现在按下向下箭头将导航到倒数第四个代码片段/命令。当您位于第一个或最后一个代码片段/命令时,按下向上箭头或向下箭头没有任何作用。

正在读取 JShell 堆栈跟踪

jshell上输入的片段是合成类的一部分。Java 不允许你声明一个顶级方法。方法声明必须是类型的一部分。当 Java 程序中抛出异常时,堆栈跟踪会打印类型名和行号。在jshell中,一个代码片段可能会抛出一个异常。在这种情况下,打印合成的类名和行号会产生误导,对开发人员来说毫无意义。代码段在堆栈跟踪中的位置格式如下:

at <snippet-name> (#<snippet-id>:<line-number-in-snippet>)

请注意,有些片段可能没有名称。例如,输入一个片段2 + 2不会给它一个名字。有些代码段有名称,例如声明变量的代码段被赋予与变量名相同的名称;方法和类型声明也是如此。有时,您可能有两个同名的代码段,例如,通过用相同的名称声明一个变量和一个方法/类型。jshell为所有片段分配唯一的片段 ID。您可以使用/list -all命令找到代码片段的 ID。

下面的jshell会话声明了一个divide()方法,并打印了一个运行时ArithmeticException异常的异常堆栈跟踪,该异常在整数被零除时抛出:

jshell> int divide(int x, int y) {
   ...> return x/y;
   ...> }
|  created method divide(int,int)
jshell> divide(10, 2)
$2 ==> 5
jshell> divide(10, 0)
|  java.lang.ArithmeticException thrown: / by zero
|        at divide (#1:2)
|        at (#3:1)
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : int divide(int x, int y) {
       return x/y;
       }
   2 : divide(10, 2)
   3 : divide(10, 0)
jshell>

让我们尝试读取堆栈跟踪。最后一行at (#3:1),声明异常是在代码片段 3 的第 1 行引起的。注意在/list -all命令的输出中,代码片段 3 是导致异常的表达式divide(10, 0)。第二行at divide (#1:2)表示堆栈跟踪中的第二级位于名为divide的代码片段的第 2 行,其代码片段 ID 为 1,行号为 2。

重用 JShell 会话

您可以在一个jshell会话中输入许多片段和命令,并且可能希望在其他会话中重用它们。您可以使用/save命令将命令和代码片段保存到文件中,并使用/open命令加载之前保存的命令和代码片段。/save命令的语法如下:

/save <option> <file-path>

这里,<option>可以是-all-history-start中的一个选项。<file-path>是保存代码片段/命令的文件路径。

不带选项的/save命令保存当前会话中的所有活动片段。请注意,它不保存任何命令或失败的代码片段。

带有-all选项的/save命令将当前会话的所有代码片段保存到指定文件,包括失败和启动代码片段。请注意,它不保存任何命令。

-history选项的/save命令会保存你在jshell中输入的所有内容。

-start选项的/save命令将默认启动定义保存到指定文件。

您可以使用/open命令从文件中重新加载代码片段。该命令将文件名作为参数。

下面的jshell会话声明一个名为Counter的类,创建它的对象,并调用对象上的方法。最后,它将所有活动的代码片段保存到一个名为jshell.jsh的文件中。注意文件扩展名.jshjshell文件的惯用扩展名。您可以使用您想要的任何其他扩展名:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> class Counter {
   ...>    private int count;
   ...>    public synchronized int next() {
   ...>      return ++count;
   ...>    }
   ...>    public int current() {
   ...>      return count;
   ...>    }
   ...> }
|  created class Counter
jshell> Counter counter = new Counter()
counter ==> Counter@25bbe1b6
jshell> counter.current()
$3 ==> 0
jshell> counter.next()
$4 ==> 1
jshell> counter.next()
$5 ==> 2
jshell> counter.current()
$6 ==> 2
jshell> /save jshell.jsh
jshell> /exit
|  Goodbye

此时,您应该在当前目录中有一个名为jshell.jsh的文件,其内容如清单 23-3 所示。

class Counter {
   private int count;
   public synchronized int next() {
     return ++count;
   }
   public int current() {
     return count;
   }
}
Counter counter = new Counter();
counter.current()
counter.next()
counter.next()
counter.current()

Listing 23-3Contents of the jshell.jsh File

接下来的jshell会话打开jshell.jsh文件,该文件将重放在之前的会话中保存的所有片段。打开文件后,您可以开始调用counter变量上的方法:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /open jshell.jsh
jshell> counter.current()
$7 ==> 2
jshell> counter.next()
$8 ==> 3
jshell>

重置 JShell 状态

您可以使用/reset命令重置 JShell 的执行状态。执行此命令具有以下效果:

  • 您在当前会话中输入的所有代码片段都将丢失,因此在执行此命令之前要小心。

  • 重新执行启动代码片段。

  • 工具的执行状态被重新启动。

  • 使用/set命令设置的jshell配置被保留。

  • 使用/env命令设置的执行环境被保留。

下面的jshell会话声明一个变量,重置会话,并试图打印变量值。请注意,重置会话时,所有声明的变量都将丢失,因此找不到以前声明的变量:

jshell> int x = 987
x ==> 987
jshell> /reset
|  Resetting state.
jshell> x
|  Error:
|  cannot find symbol
|    symbol:   variable x
|  x
|  ^
jshell>

重新加载 JShell 状态

假设您在一个jshell会话中使用了许多代码片段,并退出了该会话。现在你想回去重放那些片段。一种方法是启动一个新的jshell会话,重新输入这些片段。在jshell中重新输入几个片段是一件麻烦事。有一种简单的方法可以实现这一点——使用/reload命令。/reload命令重置jshell状态,并以之前输入的相同顺序重放所有有效片段和/drop命令。您可以使用-restore-quiet选项自定义其行为。

不带任何选项的/reload命令重置jshell状态,并重放以下先前动作/事件之一的有效历史,以最后发生的为准:

  • 当前会话开始

  • 执行最后一个/reset命令的时间

  • 执行最后一个/reload命令的时间

您可以在/reload命令中使用-restore选项。它重置并重放以下两个动作/事件之间的历史记录,以最后两个动作/事件为准:

  • jshell的发射

  • 执行/reset命令

  • 执行/reload命令

-restore选项执行/reload命令的效果有点难以理解。它的主要目的是恢复以前的执行状态。如果您在每个jshell会话开始时执行该命令,从第二个会话开始,您的会话将包含您在jshell会话中执行过的所有代码片段!这是一个强大的功能。也就是说,您可以评估代码片段,关闭jshell,重启jshell,并执行/reload -restore命令作为您的第一个命令,并且您永远不会丢失您之前输入的任何代码片段。有时,您会在一个会话中执行两次/reset命令,并希望恢复这两次重置之间的状态。您可以通过使用此命令来实现这一结果。

下面的jshell会话在每个会话中创建一个变量,并通过在每个会话开始时执行/reload -restore命令来恢复前一个会话。该示例显示第四个会话使用了在第一个会话中声明的名为x1的变量:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> int x1 = 10
x1 ==> 10
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
jshell> int x2 = 20
x2 ==> 20
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
-: int x2 = 20;
jshell> int x3 = 30
x3 ==> 30
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
-: int x2 = 20;
-: int x3 = 30;
jshell> System.out.println("x1 is " + x1)
x1 is 10
jshell>

/reload命令显示它重放的历史。您可以使用-quiet选项抑制回放显示。您可以使用此选项,也可以不使用-restore选项。-quiet选项不抑制重放历史时可能产生的错误信息。下面的例子使用了两个jshell会话。第一个会话声明一个名为x1的变量。第二个会话使用带有/reload命令的-quiet选项。注意,这一次,您没有看到变量x1在第二个会话中被重新加载的重放显示,因为您使用了-quiet选项:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> int x1 = 10
x1 ==> 10
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /reload -restore -quiet
|  Restarting and restoring from previous state.
jshell> x1
x1 ==> 10
jshell>

配置 JShell

使用/set命令,您可以定制jshell会话,从启动代码片段和命令到设置特定于平台的代码片段编辑器。在本节中,我们将详细解释这些定制。

设置代码片段编辑器

JShell工具带有一个默认的代码片段编辑器。在jshell中,您可以使用/edit命令来编辑所有代码片段或特定的代码片段。/edit命令在编辑器中打开代码片段。代码片段编辑器是一个特定于平台的程序,比如 Windows 上的notepad.exe,它将被调用来编辑代码片段。您可以使用带有editor参数的/set命令来设置或删除编辑器设置。该命令的有效形式如下:

  • /set editor [-retain] [-wait] <command>

  • /set editor [-retain] -default

  • /set editor [-retain] -delete

如果使用-retain选项,该设置将在jshell个会话中保持不变。

如果指定命令,该命令必须是特定于平台的。也就是说,您需要在 Windows 上指定一个 Windows 命令,在 UNIX 上指定一个 UNIX 命令,等等。该命令可能包含标志。JShell工具将待编辑的片段保存在临时文件中,并将临时文件的名称附加到命令中。编辑器打开时,您不能使用jshell。如果您的编辑器立即退出,您应该指定-wait选项,这将使jshell一直等到编辑器关闭。以下命令将记事本设置为 Windows 上的编辑器:

jshell> /set editor -retain notepad.exe

-default选项将代码片段编辑器设置为默认编辑器。-delete选项删除当前的编辑器设置。如果-retain选项与-delete选项一起使用,保留的编辑器设置将被删除:

jshell> /set editor -retain -delete
|  Editor set to: -default
jshell>

在以下环境变量之一中设置的编辑器— JSHELLEDITORVISUALEDITOR—优先于默认编辑器。这些环境变量在编辑器中按顺序查找。如果没有设置这些环境变量,将使用默认编辑器。所有这些规则背后的意图是始终拥有一个编辑器,然后使用默认编辑器作为后备。没有任何参数和选项的/set editor命令打印关于当前编辑器设置的信息。

下面的jshell会话将记事本设置为 Windows 上的编辑器。请注意,此示例不能在 Windows 以外的平台上运行,在 Windows 中,您需要将特定于平台的程序指定为编辑器:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor -default
jshell> /set editor -retain notepad.exe
|  Editor set to: notepad.exe
|  Editor setting retained: notepad.exe
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor -retain notepad.exe
jshell> 2 + 2
$1 ==> 4
jshell> /edit
jshell> /set editor -retain -delete
|  Editor set to: -default
jshell> /exit
|  Goodbye
C:\JavaFun>SET JSHELLEDITOR=notepad.exe
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor notepad.exe
jshell>

设置反馈模式

当您执行片段或命令时,jshell会打印反馈。反馈的数量和格式取决于反馈模式。您可以使用四种预定义反馈模式之一或自定义反馈模式:

  • silent

  • concise

  • normal

  • verbose

silent模式完全不给你反馈,verbose模式给你的反馈最多。concise模式提供与normal模式相同的反馈,但形式更紧凑。默认反馈模式是normal

表 23-3 显示了每个内置反馈模式的细节。提示栏包含提示,其中\n表示新的一行。其他列显示反馈显示或不显示的位置,如果显示,则显示反馈的格式。“声明”、“更新”和“命令”列分别显示了声明、对现有代码片段的更新和命令的反馈。“带值的代码片段”列显示了输入结果代码片段时的反馈格式。

表 23-3

内置反馈模式的特性

|

方式

|

提示

|

申报

|

更新

|

命令

|

带有值的代码段

| | --- | --- | --- | --- | --- | --- | | 沉默的 | -> | 不 | 不 | 不 | 不 | | 简明的 | jshell> | 不 | 不 | 不 | name == >值(仅用于表达式) | | 标准 | \njshell> | 是 | 不 | 是 | name == >值 | | 冗长的 | \njshell> | 是 | 是 | 是 | name == >值(带描述) |

设置反馈模式的命令如下:

/set feedback [-retain] <mode>

这里,<mode>是四种反馈模式之一。如果您想在jshell会话中保持反馈模式,请使用-retain选项。

您也可以在特定的反馈模式下启动jshell:

jshell --feedback <mode>

以下命令在verbose反馈模式下启动jshell:

C:\JavaFun>jshell --feedback verbose

以下示例显示了如何设置不同的反馈模式:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> 2 + 2
$1 ==> 4
jshell> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$2 ==> 4
|  created scratch variable $2 : int
jshell> /set feedback concise
jshell> 2 + 2
$3 ==> 4
jshell> /set feedback silent
-> 2 + 2
-> System.out.println("Hello")
Hello
-> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$6 ==> 4
|  created scratch variable $6 : int

jshell中设置的反馈模式是临时的。它仅针对当前会话设置。要在jshell个会话中保持反馈模式,使用带有feedback参数和-retain选项的/set命令:

jshell> /set feedback -retain

该命令将保持当前的反馈模式。当您再次启动jshell时,它将配置您执行该命令前设置的反馈模式。仍然可以在会话中临时更改反馈模式。如果您想永久设置一个新的反馈模式,您需要使用/set feedback <mode>命令并再次执行该命令来保存新的设置。

也可以设置一个新的反馈模式,同时通过使用-retain选项为将来的会话保留该模式。以下命令会将反馈模式设置为verbose,并在以后的会话中保留:

jshell> /set feedback -retain verbose

要确定当前的反馈模式,执行带有feedback参数的/set命令。它将用于设置当前反馈模式的命令打印在第一行,后跟所有可用的反馈模式,如下所示:

jshell> /set feedback
|  /set feedback normal
|
|  Available feedback modes:
|     concise
|     normal
|     silent
|     verbose
jshell>

Tip

当学习jshell时,建议您在verbose反馈模式下开始,这样您可以获得许多关于命令和代码片段执行状态的细节。这将帮助您更快地学习该工具。

创建自定义反馈模式

四种预配置的反馈模式适合与jshell配合使用。它们为您提供不同的粒度级别来定制您的jshell。你可以有自己的自定义反馈模式。我们怀疑你是否需要自定义反馈模式,但是如果你需要的话,这个功能就在那里。创建自定义反馈模式稍微复杂一些。您必须编写几个定制步骤。最有可能的情况是,您希望在预定义的反馈模式中自定义一些项目。您可以从头开始创建自定义反馈模式,也可以从现有的反馈模式中复制一个,然后有选择地进行自定义。创建自定义反馈模式的语法如下:

/set mode <mode> [<old-mode>] [-command|-quiet|-delete]

这里,<mode>是自定义反馈模式的名称;例如,kverbose. <old-mode>是现有反馈模式的名称,其设置将被复制到新模式。使用-command选项显示模式设置时的信息,而使用-quiet选项不显示模式设置时的任何信息。-delete选项用于删除模式。

以下命令通过复制预定义的verbose反馈模式的所有设置,创建一个名为kverbose的新反馈模式:

/set mode kverbose verbose -command

以下命令将保留新的反馈模式以供将来使用:

/set mode kverbose -retain

您需要使用-delete选项来删除自定义反馈模式。您不能删除预定义的反馈模式。如果您保留了自定义反馈模式,您可以使用-retain选项将其从当前和所有未来会话中删除。以下命令将删除kverbose反馈模式:

/set mode kverbose -delete -retain

此时,预定义的verbose模式和自定义的kverbose模式没有区别。创建反馈模式后,您需要自定义三个设置:

  • 提示

  • 输出截断限值

  • 输出格式

Tip

一旦你完成了自定义反馈模式的定义,你需要使用/set feedback <new-mode>命令来开始使用它。

您可以为反馈模式设置两种类型的提示-主提示和继续提示。当jshell准备好读取新的片段/命令时,显示主提示。当您输入多行代码段时,继续提示会显示在行首。设置提示的语法如下:

/set prompt <mode> "<prompt>" "<continuation-prompt>"

这里,<prompt>是主提示,<continuation-prompt>是继续提示。

以下命令设置kverbose模式的提示:

/set prompt kverbose "\njshell-kverbose> " "more... "

您可以使用以下命令为反馈模式的每种类型的动作/事件设置显示的最大字符数:

/set truncation <mode> <length> <selectors>

这里,<mode>是您设置截断极限的反馈模式;<length>是指定选择器显示的最大字符数。<selectors>是一个逗号分隔的选择器列表,它决定了截断限制所适用的上下文。选择器是预定义的关键字,代表特定的上下文,例如,vardecl是一个选择器,代表一个变量声明,不需要初始化。使用以下命令了解有关设置截断限制和选择器的更多信息:

/help /set truncation

以下命令将所有内容的截断限制设置为 80 个字符,变量值或表达式的截断限制为 5 个字符:

/set truncation kverbose 80
/set truncation kverbose 5 expression,varvalue

请注意,最具体的选择器决定了要使用的实际截断限制。以下设置使用两个选择器,一个用于所有类型的代码段(80 个字符),另一个用于表达式和变量值(5 个字符)。对于表达式,第二个设置是最具体的设置。在这种情况下,如果变量的值超过五个字符,则在显示时会被截断为五个字符。

设置输出格式是一项复杂的工作。您需要根据动作/事件为所有类型的输出设置格式。我们不会定义所有类型的输出格式。有关设置输出格式的更多信息,请使用以下命令:

/help /set format

设置输出格式的语法如下:

/set format <mode> <field> "<format>" <selectors>

这里,<mode>是您正在设置输出格式的反馈模式的名称;<field>是要定义的上下文特定的格式;<format>用于显示输出。<format>可以在大括号中包含预定义字段的名称,例如{name}{type}{value}等。,它将被替换为基于上下文的实际值。<selectors>是决定使用这种格式的上下文的选择器。

当添加、修改或替换输入片段的表达式时,以下命令设置反馈的显示格式。整个命令在一行中输入:

/set format kverbose display "{result}{pre}created a temporary variable named {name} of type {type} and initialized it with {value}{post}" expression-added,modified,replaced-primary

下面的jshell会话通过复制预定义的verbose反馈模式的所有设置,创建一个名为kverbose的新反馈模式。它定制提示、截断限制和输出格式。它使用verbosekverbose反馈模式来比较jshell行为。请注意,以下示例中的所有命令都需要在一行中输入,即使它们有时会出现在书中的多行中:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set feedback
|  /set feedback -retain normal
|
|  Available feedback modes:
|     concise
|     normal
|     silent
|     verbose
jshell> /set mode kverbose verbose -command
|  Created new feedback mode: kverbose
jshell> /set mode kverbose -retain
jshell> /set prompt kverbose "\njshell-kverbose> " "more... "
jshell> /set truncation kverbose 5 expression,varvalue
jshell> /set format kverbose display "{result}{pre}created a temporary variable named {name} of type {type} and initialized it with {value}{post}" expression-added,modified,replaced-primary
jshell> /set feedback kverbose
|  Feedback mode: kverbose
jshell-kverbose> 2 +
more... 2
$2 ==> 4
|  created a temporary variable named $2 of type int and initialized it with 4
jshell-kverbose> 111111 + 222222
$3 ==> 33333
|  created a temporary variable named $3 of type int and initialized it with 33333
jshell-kverbose> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 +
   ...> 2
$4 ==> 4
|  created scratch variable $4 : int
jshell> 111111 + 222222
$5 ==> 333333
|  created scratch variable $5 : int
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set feedback
|  /set feedback -retain normal
|
|  Retained feedback modes:
|     kverbose
|  Available feedback modes:
|     concise
|     kverbose
|     normal
|     silent
|     verbose
jshell>

在这些jshell会话中,您为kverbose反馈模式将表达式和变量值的截断限制设置为五个字符。这就是为什么在kverbose反馈模式下,表达式111111 + 222222的值被打印为33333,而不是333333。这不是 bug。这是由您的设置造成的。

请注意,命令/set feedback显示了用于设置当前反馈模式的命令和可用反馈模式列表,其中列出了名为kverbose的新反馈模式。

创建自定义反馈模式时,了解现有反馈模式的所有设置会很有帮助。您可以使用以下命令打印所有反馈模式的所有设置列表:

/set mode

您还可以通过将模式名称作为参数传递给命令来打印特定反馈模式的所有设置列表。以下命令打印出silent反馈模式的所有设置列表。输出中的第一行是用于创建silent模式的命令:

jshell> /set mode silent
|  /set mode silent -quiet
|  /set prompt silent "-> " ">> "
|  /set format silent display ""
|  /set format silent err "%6$s"
|  /set format silent errorline "    {err}%n"
|  /set format silent errorpost "%n"
|  /set format silent errorpre "|  "
|  /set format silent errors "%5$s"
|  /set format silent name "%1$s"
|  /set format silent post "%n"
|  /set format silent pre "|  "
|  /set format silent type "%2$s"
|  /set format silent unresolved "%4$s"
|  /set format silent value "%3$s"
|  /set truncation silent 80
|  /set truncation silent 1000 expression,varvalue

设置启动代码片段

您可以使用带有start参数的/set命令来设置您的启动代码片段和命令。当您启动jshell时,启动代码片段和命令会自动执行。您已经看到了从一些常用的包中导入类型的默认启动代码片段。通常,您可以使用一个/env命令和import语句来设置启动脚本的类路径和模块路径。

您可以使用/list -start命令打印默认启动代码片段列表。请注意,该命令打印默认的启动代码片段,而不是当前的启动代码片段。请记住,您也可以删除启动代码片段。默认的启动片段包括您启动jshell时得到的内容。当前启动代码片段包括默认启动代码片段,不包括您在当前jshell会话中丢弃的代码片段。您可以使用以下形式的/set命令来设置启动片段/命令:

  • /set start [-retain] <file>

  • /set start [-retain] -default

  • /set start [-retain] -none

使用-retain选项是可选的。如果使用,该设置将在jshell会话中保持不变。

第一种形式用于从文件中设置启动代码片段/命令。当在当前会话中执行/reset/reload命令时,文件的内容将被用作启动片段/命令。一旦你从一个文件中设置了启动代码,jshell就会缓存该文件的内容以备将来使用。修改文件内容不会影响启动代码,直到您再次设置启动代码片段/命令。

第二种形式用于将启动片段/命令设置为内置默认值。

第三种形式用于设置空启动。也就是说,启动时不会执行任何代码片段/命令。

没有任何选项或文件的/set start命令显示当前的启动设置。如果从文件设置启动,它将显示文件名、启动代码片段和设置启动代码片段的时间。

考虑下面的场景。本书源代码中的JavaFun/build/modules/jdojo.jshell目录包含一个com.jdojo.jshell.Person类。让我们在jshell中测试这个类,并使用java.time包中的类型。为此,您的启动设置将类似于清单 23-4 中所示的内容。

/env -class-path C:\JavaFun\build\modules\jdojo.jshell
import java.io.*
import java.math.*
import java.net.*
import java.nio.file.*
import java.util.*
import java.util.concurrent.*
import java.util.function.*
import java.util.prefs.*
import java.util.regex.*
import java.util.stream.*
import java.time.*;
import com.jdojo.jshell.*;
void printf(String format, Object... args) { System.out.printf(format, args); }

Listing 23-4Contents of a File Named startup.jsh

将设置保存在当前目录下名为startup.jsh的文件中。如果您将它保存在任何其他目录中,则在使用本示例时,您可以使用该文件的绝对路径。注意,第一个命令是 Windows 的/env -class-path命令,假设您将源代码存储在C:\目录中。根据您的平台和本书源代码在您计算机上的位置来更改类路径值。

注意startup.jsh文件中的最后一段代码。它定义了一个名为printf()的顶级函数,它是System.out.printf()方法的包装器。默认情况下,printf()功能包含在JShell工具的初始版本中。后来,它被删除了。如果您想使用一个短的方法名,比如用printf()而不是System.out.printf()在标准输出中打印消息,您可以在启动脚本中包含这个代码片段。如果想默认使用jshell中的println()printf()顶层方法,需要如下启动jshell:

C:\JavaFun>jshell --start DEFAULT --start PRINTING

DEFAULT参数将包括所有默认的import语句,而PRINTING参数将包括print()println()printf()方法的所有版本。使用该命令启动jshell后,执行/list -start命令查看命令中使用的两个--start选项添加的所有启动import和方法。

以下jshell会话向您展示了如何从文件中设置启动设置及其在后续会话中的使用:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set start
|  /set start -default
jshell> /set start -retain startup.jsh
jshell> Person p;
|  created variable p, however, it cannot be referenced until class Person is declared
jshell> /reset
|  Resetting state.
jshell> Person p;
p ==> null
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set start
|  /set start -retain startup.jsh
|  ---- startup.jsh @ Aug 20, 2017, 9:58:11 AM ----
|  /env -class-path C:\JavaFun\build\modules\jdojo.jshell
|  import java.io.*
|  import java.math.*
|  import java.net.*
|  import java.nio.file.*
|  import java.util.*
|  import java.util.concurrent.*
|  import java.util.function.*
|  import java.util.prefs.*
|  import java.util.regex.*
|  import java.util.stream.*
|  import java.time.*;
|  import com.jdojo.jshell.*;
|  void printf(String format, Object... args) { System.out.printf(format, args); }
jshell> Person p;
p ==> null
jshell> LocalDate.now()
$15 ==> 2017-08-20
jshell> printf("2 + 2 = %d%n", 2 + 2)
2 + 2 = 4
jshell>

Tip

在您重新启动jshell、执行/reset或执行/reload命令之前,设置启动片段/命令不会生效。不要在启动文件中包含/reset/reload命令。这将导致一个无限循环时,你的启动文件加载。

有三个预定义脚本,其名称如下:

  • DEFAULT

  • PRINTING

  • JAVASE

DEFAULT脚本包含常用的导入语句,如您在“导入语句”一节中所见。PRINTING脚本定义了顶级 JShell 方法,这些方法重定向到PrintStream中的print()println()printf()方法,如本节所示。JAVASE脚本导入所有的 Java SE 包,这很大,需要几秒钟才能完成。以下命令显示了如何将这些脚本保存为启动脚本:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> println("Hello")
|  Error:
|  cannot find symbol
|    symbol:   method println(java.lang.String)
|  println("Hello")
|  ^-----^
jshell> /set start -retain DEFAULT PRINTING
jshell> /exit
|  Goodbye
C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> println("Hello")
Hello
jshell>

注意,第一次使用println()方法导致了一个错误。将PRINTING脚本保存为启动脚本并重启工具后,该方法就可以工作了。

使用 JShell 文档

JShell工具附带了大量文档。因为它是一个命令行工具,所以在命令行上阅读文档有点困难。您可以使用/help/?命令来显示命令列表及其简要描述:

jshell> /help
|  Type a Java language expression, statement, or declaration.
|  Or type one of the following commands:
|  /list [<name or id>|-all|-start]  -- list the source you have typed
|  /edit <name or id>  -- edit a source entry referenced by name or id
|  /drop <name or id>  -- delete a source entry referenced by name or id
|  ...

您可以使用一个特定的命令作为/help命令的参数来获取关于该命令的信息。以下命令打印关于/help命令本身的信息:

jshell> /help /help
|
|  /help
|
|  Display information about jshell.
|  /help
|       List the jshell commands and help subjects.
|
|  /help <command>
|       Display information about the specified command. The slash must be included.
|       Only the first few letters of the command are needed -- if more than one
|       each will be displayed.  Example:  /help /li
|
|  /help <subject>
|       Display information about the specified help subject. Example: /help intro

以下命令将显示关于/list/set命令的信息。未显示输出,因为它们很长:

jshell> /help /list
|...
jshell> /help /set
|...

有时,一个命令用于多个主题,例如,/set命令可用于设置反馈模式、片段编辑器、启动脚本等。如果你想打印一个命令的特定主题的信息,你可以使用以下格式的/help命令:

/help /<command> <topic-name>

以下命令打印关于设置反馈模式的信息:

jshell> /help /set feedback

以下命令打印有关创建自定义反馈模式的信息:

jshell> /help /set mode

使用带有主题作为参数的/help命令来打印关于主题的信息。目前有三个预定义的主题:introshortcutscontext。以下命令将打印 JShell 工具的介绍:

jshell> /help intro

以下命令将打印您可以在 JShell 工具中使用的快捷方式及其描述的列表:

jshell> /help shortcuts

以下命令将打印用于设置执行上下文的选项列表。这些选项与/env/reset/reload命令一起使用:

jshell> /help context

JShell API

JShell API 为您提供了对代码片段评估引擎的编程访问。作为开发人员,您可能不使用此 API。它旨在供 NetBeans IDE 之类的工具使用,NetBeans IDE 包括一个相当于 JShell 命令行工具的 UI,允许开发人员从 IDE 内部评估代码片段,而不是打开命令提示符来执行相同的操作。在这一节中,我们简要介绍 JShell API,并通过一个简单的例子展示它的用法。

JShell API 在jdk.jshell模块和jdk.jshell包中。如果你使用 JShell API,你的模块将需要读取jdk.jshell模块。JShell API 很简单。它主要由三个抽象类和一个接口组成:

  • JShell

  • Snippet

  • SnippetEvent

  • SourceCodeAnalysis

JShell类的一个实例代表一个代码片段评估引擎。这是JShell API 中的主类。一个JShell实例维护所有代码片段执行时的状态。

一个代码片段由一个Snippet类的实例表示。一个JShell实例在执行代码片段时生成代码片段事件。

片段事件由一个SnippetEvent接口的实例表示。snippet 事件包含 snippet 的当前和以前的状态、承载结果的 snippet 的值、导致事件的 snippet 的源代码、在 snippet 执行期间发生异常时的Exception对象等。

SourceCodeAnalysis类的一个实例为代码片段提供了源代码分析和建议功能。它回答了如下问题:

  • 是完整的片段吗?

  • 这个片段可以通过添加分号来完成吗?

一个SourceCodeAnalysis实例也提供了一个建议列表,例如,对于制表符结束和访问文档。该类旨在由提供 JShell 功能的工具使用。我们不会进一步讨论它。如果您有兴趣进一步研究它,请参考这个类的 Javadoc。

图 23-5 显示了 JShell API 不同组件的用例图。在随后的章节中,我们将解释这些类及其用途。我们将在最后一节向您展示一个完整的示例。

img/323069_3_En_23_Fig5_HTML.jpg

图 23-5

JShell API 组件的用例图

创建 JShell

JShell类是抽象的。它提供了两种创建其实例的方法:

  • 使用它的静态create()方法

  • 使用名为JShell.Builder的静态构建器类

create()方法返回一个预先配置好的JShell实例。下面的代码片段展示了如何使用create()方法创建一个JShell:

// Create a JShell instance
JShell shell = JShell.create()

通过让您指定代码片段 ID 生成器、临时变量名称生成器、用于打印输出的打印流、用于读取代码片段的输入流和用于记录错误的错误输出流,JShell.Builder类允许您配置JShell实例。您可以使用JShell类的builder()静态方法获得JShell.Builder类的实例。下面的代码片段展示了如何使用JShell.Builder类创建一个JShell,其中代码中的myXXXStream是对您的流对象的引用:

// Create a JShell instance
JShell shell = JShell.builder()
                     .in(myInputStream)
                     .out(myOutputStream)
                     .err(myErrorStream)
                     .build();

一旦有了一个JShell实例,就可以开始使用它的eval(String snippet)方法评估代码片段。您可以使用它的drop(PersistentSnippet snippet)方法删除一个代码片段。您可以使用它的addToClasspath(String path)方法将路径附加到类路径上。这三个方法改变了JShell实例的状态。

Tip

当您使用完一个JShell实例时,您需要调用它的close()方法来释放资源。JShell类实现了AutoCloseable接口,因此,使用try-with-resources块来处理JShell实例是确保它在不再使用时被关闭的最佳方式。一个JShell实例是可变的,并且不是线程安全的。

您可以使用JShell类的onSnippetEvent (Consumer<SnippetEvent> listener)onShutdown(Consumer<JShell> listener)方法注册代码片段事件处理程序和JShell关闭事件处理程序。当某个代码段的状态因第一次评估而发生更改,或者因评估另一个代码段而更新其状态时,将触发代码段事件。

JShell类中的sourceCodeAnalysis()方法返回了SourceCodeAnalysis类的一个实例,您可以用它来实现代码辅助功能。

JShell类中的其他方法用于查询状态。例如,snippets()types()methods()variables()方法分别返回所有代码段、所有带有活动类型声明的代码段、带有活动方法声明的代码段和带有活动变量声明的代码段的列表。

eval()方法是JShell类中最常用的方法。它评估/执行指定的代码片段并返回一个List<SnippetEvent>。您可以在列表中查询代码片段事件的执行状态。下面是使用eval()方法的一段代码:

// Create a snippet
String snippet = "int x = 100;";
// Evaluate the snippet
List<SnippetEvent> events = shell.eval(snippet);
// Process the results
events.forEach((SnippetEvent se) -> {
    /* Handle the snippet event here */
});

使用片段

Snippet类的一个实例代表一个片段。该类不提供创建其对象的方法。您将代码片段作为字符串提供给一个JShell,并接收作为代码片段事件一部分的Snippet类的实例。代码片段事件还为您提供代码片段的以前和当前状态。如果您有一个Snippet对象,您可以使用JShell类的status(Snippet s)方法查询它的当前状态,该方法返回一个Snippet.Status

Tip

Snippet类是不可变的和线程安全的。

Java 中有几种类型的代码片段,例如,变量声明、带初始化的变量声明、方法声明、类型声明等。Snippet类是一个抽象类,有一个子类来表示每个特定类型的代码片段。图 23-6 显示了Snippet类及其后代的类图。

img/323069_3_En_23_Fig6_HTML.jpg

图 23-6

Snippet 类及其后代的类图

Snippet类的子类的名字很直观。例如,PersistentSnippet的一个实例代表一个存储在JShell中的片段,可以重用,比如类声明或方法声明。Snippet类包含以下方法:

  • 字符串 id()

  • 字符串源()

  • 片段。善良善良()

  • 片段。子类子类()

id()方法返回代码片段的唯一 ID,而source()方法返回其源代码。kind()subKind()方法返回代码片段的类型和子类型。

片段的类型是Snippet.Kind枚举的常量之一,例如IMPORTTYPE_DECLMETHODVAR等。代码片段的子类型提供了关于其类型的更具体的信息,例如,如果代码片段是类型声明,它的子类型将告诉您它是类、接口、枚举还是注释声明。片段的子类型是Snippet.SubKind枚举的常量之一,如CLASS_SUBKINDENUM_SUBKIND等。Snippet.Kind枚举包含一个isPersistent属性,如果这种类型的代码片段是持久的,则该属性的值为true,否则为false

Snippet类的子类添加了更多的方法来返回关于特定类型代码片段的特定信息。例如,VarSnippet类包含一个typeName()方法,它返回变量的数据类型。MethodSnippet类包含parameterTypes()signature()方法,它们以字符串形式返回参数类型和方法的完整签名。

代码片段不包含其状态。A JShell执行并保持 a Snippet的状态。请注意,执行一个代码片段可能会影响其他代码片段的状态。例如,声明变量的代码片段可能会将声明方法的代码片段的状态从有效更改为无效,反之亦然(如果方法引用了变量)。如果您需要代码片段的当前状态,使用JShell类的status(Snippet s)方法,该方法返回Snippet.Status枚举的下列常量之一:

  • DROPPED:该代码片段是不活动的,因为它是使用JShell类的drop()方法删除的。

  • NONEXISTENT:该代码段不活动,因为它尚不存在。

  • OVERWRITTEN:该代码片段无效,因为它已被新代码片段替换。

  • RECOVERABLE_DEFINED:代码段是包含未解析引用的声明代码段。该声明具有有效的签名,并且对其他代码段可见。当其他代码片段将其状态更改为VALID时,可以恢复并使用它。

  • RECOVERABLE_NOT_DEFINED:代码段是包含未解析引用的声明代码段。该代码段的签名无效,并且对其他代码段不可见。当它的状态变为VALID时,可以使用它。

  • REJECTED:该代码片段是不活动的,因为它在初始评估时编译失败,并且它不能随着对JShell状态的进一步改变而变得有效。

  • VALID:该片段在当前JShell状态的上下文中有效。

处理代码片段事件

一个JShell实例生成片段事件作为片段评估或执行的一部分。您可以通过使用JShell类的onSnippetEvent()方法注册事件处理程序,或者通过使用JShell类的eval()方法的返回值(这是一个List<SnippetEvent>)来处理片段事件。以下代码片段向您展示了如何使用eval()方法的返回值来处理代码片段事件:

try (JShell shell = JShell.create()) {
    // Create a snippet
    String snippet = "int x = 100;";
    shell.eval(snippet)
         .forEach((SnippetEvent se) -> {
              Snippet s = se.snippet();
              System.out.printf("Snippet: %s%n", s.source());
              System.out.printf("Kind: %s%n", s.kind());
              System.out.printf("Sub-Kind: %s%n", s.subKind());
              System.out.printf("Previous Status: %s%n", se.previousStatus());
              System.out.printf("Current Status: %s%n", se.status());
              System.out.printf("Value: %s%n", se.value());
        });
}

一个例子

让我们看看 JShell API 的实际应用。清单 23-5 包含了一个名为JShellApiTest的类的完整代码,它是jdojo.jshell模块的成员。

// JShellApiTest.java
package com.jdojo.jshell;
import jdk.jshell.JShell;
import jdk.jshell.Snippet;
import jdk.jshell.SnippetEvent;
public class JShellApiTest {
    public static void main(String[] args) {
        // Create an array of snippets to evaluate/execute
        // them sequentially
        String[] snippets = {"int x = 100;",
            "double x = 190.89;",
            "long multiply(int value) {return value * multiplier;}",
            "int multiplier = 2;",
            "multiply(200)",
            "mul(99)"
        };
        try (JShell shell = JShell.create()) {
            // Register a snippet event handler
            shell.onSnippetEvent(JShellApiTest::snippetEventHandler);
            // Evaluate all snippets
            for (String snippet : snippets) {
                shell.eval(snippet);
                System.out.println("------------------------");
            }
        }
    }
    public static void snippetEventHandler(SnippetEvent se) {
        // Print the details of this snippet event
        Snippet snippet = se.snippet();
        System.out.printf("Snippet: %s%n", snippet.source());
        // Print the cause of this snippet event
        Snippet causeSnippet = se.causeSnippet();
        if (causeSnippet != null) {
            System.out.printf("Cause Snippet: %s%n", causeSnippet.source());
        }
        System.out.printf("Kind: %s%n", snippet.kind());
        System.out.printf("Sub-Kind: %s%n", snippet.subKind());
        System.out.printf("Previous Status: %s%n", se.previousStatus());
        System.out.printf("Current Status: %s%n", se.status());
        System.out.printf("Value: %s%n", se.value());
        Exception e = se.exception();
        if (e != null) {
            System.out.printf("Exception: %s%n", se.exception().getMessage());
        }
    }
}
Snippet: int x = 100;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 100
---------------------------------------------------------------
Snippet: double x = 190.89;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: VALID
Current Status: VALID
Value: 190.89
Snippet: int x = 100;
Cause Snippet: double x = 190.89;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: VALID
Current Status: OVERWRITTEN
Value: null
---------------------------------------------------------------
Snippet: long multiply(int value) {return value * multiplier;}
Kind: METHOD
Sub-Kind: METHOD_SUBKIND
Previous Status: NONEXISTENT
Current Status: RECOVERABLE_DEFINED
Value: null
---------------------------------------------------------------
Snippet: int multiplier = 2;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 2
Snippet: long multiply(int value) {return value * multiplier;}
Cause Snippet: int multiplier = 2;
Kind: METHOD
Sub-Kind: METHOD_SUBKIND
Previous Status: RECOVERABLE_DEFINED
Current Status: VALID
Value: null
---------------------------------------------------------------
Snippet: multiply(200)
Kind: VAR
Sub-Kind: TEMP_VAR_EXPRESSION_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 400
---------------------------------------------------------------
Snippet: mul(99)
Kind: ERRONEOUS
Sub-Kind: UNKNOWN_SUBKIND
Previous Status: NONEXISTENT
Current Status: REJECTED
Value: null
---------------------------------------------------------------

Listing 23-5A JShellApiTest Class to Test the JShell API

main()方法创建以下六个代码片段,并将它们存储在一个String数组中:

  • "int x = 100;"

  • "double x = 190.89;"

  • "long multiply(int value) {return value * multiplier;}"

  • "int multiplier = 2;"

  • "multiply(200)"

  • "mul(99)"

一个try-with-resources块用于创建一个JShell实例。snippetEventHandler()方法被注册为一个片段事件处理程序。该方法打印关于片段的细节,例如其源代码、导致片段状态更新的片段的源代码、片段的先前和当前状态、其值等。最后,使用一个for-each循环遍历所有代码片段,并调用eval()方法来执行它们。

让我们浏览一下执行每个代码片段时JShell引擎的状态:

  • 当执行代码片段#1 时,该代码片段不存在,所以它从NONEXISTENT状态转换到VALID状态。它是一个变量声明片段,其计算结果为100

  • 当代码片段#2 被执行时,它已经存在了。注意,它用不同的数据类型声明了同一个名为x的变量。它以前的状态是VALID,现在的状态也是VALID。这个代码片段的执行改变了代码片段#1 的状态,它的状态从VALID变为OVERWRITTEN,因为不能有两个同名的变量。

  • 代码片段#3 声明了一个名为multiply()的方法,该方法在其主体中使用了一个名为multiplier的未声明变量,因此其状态从NONEXISTENT变为RECOVERABLE_DEFINED。该方法已定义,这意味着它可以被引用,但不能被调用,直到定义了适当类型的名为multiplier的变量。

  • 代码片段#4 定义了一个名为multiplier的变量,这使得代码片段#3 有效。

  • 代码片段#5 计算一个调用multiply()方法的表达式。该表达式是有效的,其计算结果为400

  • 代码片段#6 计算一个调用mul()方法的表达式,这个方法您从未定义过。该片段是错误的,因此被拒绝。

通常,您不会一起使用 JShell API 和 JShell 工具。然而,让我们一起使用它们只是为了好玩。JShell API 只是 Java 中的另一个 API,它也可以在 JShell 工具内部使用。下面的jshell会话实例化一个JShell,注册一个代码片段事件处理程序,并评估两个代码片段:

C:\JavaFun>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
jshell> /set feedback silent
-> import jdk.jshell.*
-> JShell shell = JShell.create()
-> shell.onSnippetEvent(se -> {
>>  System.out.printf("Snippet: %s%n", se.snippet().source());
>>  System.out.printf("Previous Status: %s%n", se.previousStatus());
>>  System.out.printf("Current Status: %s%n", se.status());
>>  System.out.printf("Value: %s%n", se.value());
>> });
-> shell.eval("int x = 100;");
Snippet: int x = 100;
Previous Status: NONEXISTENT
Current Status: VALID
Value: 100
-> shell.eval("double x = 100.89;");
Snippet: double x = 100.89;
Previous Status: VALID
Current Status: VALID
Value: 100.89
Snippet: int x = 100;
Previous Status: VALID
Current Status: OVERWRITTEN
Value: null
-> shell.close()
-> /exit
C:\JavaFun>

摘要

被称为 JShell 的 Java shell 是一个命令行工具,它提供了一种访问 Java 编程语言的交互方式。它让您评估 Java 代码片段,而不是强迫您编写整个 Java 程序。它是 Java 的一个REPL。JShell 也是一个 API,为其他工具(如 ide)的 Java 代码提供对REPL功能的编程访问。

您可以通过运行安装 JDK 时复制到JDK_HOME\bin目录的jshell程序来启动 JShell 命令行工具。该工具支持执行代码片段和命令。片段是 Java 代码的片断。在评估/执行代码片段时,JShell 会保持其状态。它还跟踪所有输入片段的状态。您可以使用命令查询 JShell 状态并配置jshell环境。为了区分命令和片段,所有的命令都以斜杠(/)开始。

JShell 包含几个特性,可以提高开发人员的工作效率,并提供更好的用户体验,比如自动完成代码和在工具中显示 Javadoc。它试图使用 JDK 中已经存在的功能,例如编译器 API 来解析、分析和编译代码片段,以及 Java 调试器 API 来用 JVM 中的新代码片段替换现有的代码片段。它的设计使得无需对 JShell 工具本身进行任何修改或稍加修改就可以在 Java 语言中使用新的构造。

EXERCISES

  1. 什么是 Java shell?

  2. 您使用什么命令来启动 JShell 命令行工具?

  3. 您使用什么命令来退出 JShell 命令行工具?

  4. 在 JShell 工具中,使用什么命令来打印帮助?

  5. JShell 工具如何区分代码片段和命令?

  6. 为什么在您的代码片段中不能有一个在jshell中输入的包声明?

  7. 您使用什么命令来列出所有活动代码片段、所有代码片段和所有启动代码片段?

  8. 在 JShell 工具中用什么命令来设置模块路径和类路径?

  9. 如何运行jshell中的前一个片段?

  10. 当你执行jshell中的一个片段时,抛出一个检查过的异常会发生什么?

  11. 在 JShell 工具中,您使用什么键来自动完成代码?

  12. 你用什么组合键把一个表达式自动转换成适当类型的变量声明?

  13. 您使用什么组合键来自动导入代码片段中未解析的类型?

  14. JShell 工具内置的四种反馈模式是什么?学习 JShell 工具时,您应该使用哪种反馈模式?可以自定义内置反馈模式吗?

  15. 编写将当前和所有未来会话的反馈模式设置为verbose的命令。

  16. 执行/reset命令有什么效果?

  17. 执行/reload命令有什么效果?

  18. 您使用什么命令将jshell会话中的片段保存到文件中,并将片段从文件加载到jshell会话中?

  19. 描述 JShell API 中的JShellSnippetSnippetEvent类的作用。

  20. 如何创建一个JShell类的实例?

  21. 如何在你的程序中获得一个Snippet类的实例?

  22. 如何启动 JShell 工具,以便可以使用println()函数打印消息,而不是使用System.out.println()。显示在 JShell 工具中使该设置永久化的命令。