Skip to content

JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)

摘要

提供一个交互式工具,用于评估 Java 编程语言的声明、语句和表达式,同时提供 API,以便其他应用程序可以利用这个功能。

目标

JShell API 和工具将提供一种交互式评估 Java 编程语言的声明、语句和表达式的方法。JShell 状态包括正在发展的代码和执行状态。为了方便快速调查和编码,语句和表达式不需要出现在一个方法内部,变量和方法也不需要出现在一个类内部。

jshell工具将是一个命令行工具,具有简化交互的功能,包括:带编辑功能的历史记录、制表符补全、自动添加所需的终端分号以及可配置的预定义导入和定义。

非目标

不是目标创建一种新的交互式语言:所有接受的输入必须符合 Java 语言规范(JLS)中的语法产生式。此外,在适当的上下文中,所有接受的输入必须是有效的 Java 代码(JShell 将自动提供该上下文——"包装")。也就是说,如果X是 JShell 接受的输入(而不是报错拒绝的输入),那么存在AB,使得AXB是 Java 编程语言中的一个有效程序。

图形界面和调试器支持超出了本文档的范围。JShell API 旨在允许在集成开发环境(IDE)和其他工具中使用 JShell 功能,但jshell工具不打算成为一个 IDE。

动机

当学习一门编程语言及其 API 时,即时反馈非常重要。学校转向其他教学语言的首要原因是其他语言具有"REPL"并且对于初始的"Hello, world!"程序有更低的门槛。读取 - 求值 - 打印循环(REPL)是一种交互式编程工具,它循环地读取用户输入、评估输入并打印输入的值或输入导致的状态变化的描述。Scala、Ruby、JavaScript、Haskell、Clojure 和 Python 都有 REPL,并且都允许编写小型的初始程序。JShell 为 Java 平台添加了 REPL 功能。

对于原型代码或调查新 API 的开发人员来说,探索编码选项也很重要。与编辑/编译/执行和System.out.println相比,交互式评估在这方面要高效得多。

没有class Foo { public static void main(String[] args) { ... } }这样的仪式感,学习和探索变得更加简化。

描述

功能

JShell API 将提供所有 JShell 的评估功能。输入到 API 的代码片段被称为"snippets"。jshell工具还将使用 JShell 完成 API 来确定输入是否不完整(用户必须提示更多输入)、如果添加一个分号就会完整(在这种情况下,工具将添加分号),以及如何在请求使用制表符作为补全时完成输入。该工具将具有一组用于查询、保存和还原工作以及配置的命令。命令与代码片段以斜杠开头进行区分。

文档

JShell 模块 API 规范可以在以下位置找到:

其中包括主要的 JShell API (jdk.jshell包) 规范:

jshell工具参考:

是 Java 平台标准版工具参考的一部分:

术语

在本文档中,“类”一词是指 Java 虚拟机规范(JVMS)中使用的含义,其中包括 Java 语言规范(JLS)中的类、接口、枚举和注解类型。如果意图是指其他含义,文本会明确说明。

代码片段

代码片段必须对应于以下 JLS 语法产生式之一:

  • Expression(表达式)
  • Statement(语句)
  • ClassDeclaration(类声明)
  • InterfaceDeclaration(接口声明)
  • MethodDeclaration(方法声明)
  • FieldDeclaration(字段声明)
  • ImportDeclaration(导入声明)

在 JShell 中,"变量"是一个存储位置,并且具有关联的类型。变量可以通过显式的FieldDeclaration代码片段创建:

java
int a = 42;

或者通过表达式隐式创建(见下文)。变量具有一小部分字段的语义/语法(例如,允许volatile修饰符)。但是,变量没有用户可见的封装它们的类,通常会像局部变量一样被查看和使用。

所有表达式都被接受作为代码片段。这包括没有副作用的表达式,如常量、变量访问和 lambda 表达式:

java
1
a
2+2
Math.PI
x -> x+1
(String s) -> s.length()

以及具有副作用的表达式,如赋值和方法调用:

java
a = 1
System.out.println("Hello world");
new BufferedReader(new InputStreamReader(System.in))

某些形式的表达式片段隐式创建一个变量来存储表达式的值,以便其他片段稍后使用该值。默认情况下,隐式创建的变量的名称为$X,其中X是代码片段的标识符。如果表达式是 void 类型的(如println的例子),或者表达式的值已经可以通过简单名称引用(如上述'a'和'a=1'的情况),则不会隐式创建变量(所有其他示例都隐式为它们创建变量)。

所有语句都被接受为代码片段,除了'break'、'continue'和'return'。然而,代码片段可以包含符合 Java 编程语言通常规则的'break'、'continue'或'return'语句。例如,下面代码片段中的 return 语句是有效的,因为它被包含在一个 lambda 表达式中:

java
() -> { return 42; }

声明片段(ClassDeclarationInterfaceDeclarationMethodDeclarationFieldDeclaration)是明确引入可以由其他片段引用的名称的片段。声明片段受到以下规则的限制:

  • 访问修饰符(publicprotectedprivate)将被忽略(所有声明片段对于所有其他片段都是可访问的)。
  • final修饰符将被忽略(允许将来的更改/继承)。
  • static修饰符将被忽略(没有用户可见的包含类)。
  • 不允许使用defaultsynchronized修饰符。
  • abstract修饰符仅允许出现在类上。

除了ImportDeclaration形式的片段之外,所有片段都可以包含嵌套的声明。例如,作为类实例创建表达式的片段可以指定带有嵌套方法声明的匿名类体。通常使用 Java 编程语言中的规则来修改嵌套声明上的修饰符,而不是上述规则。例如,下面的类片段是被接受的,并且将尊重嵌套方法声明上的 private 修饰符,因此不会接受片段"new C().secret()":

java
class C {
  int answer() { return 2 * secret(); }
  private int secret() { return 21; }
}

片段不能声明包或模块。所有的 JShell 代码都被放置在一个未命名模块中的单个包中。包的名称由 JShell 控制。

jshell工具中,片段的终端分号如果是输入的最后一个字符(排除空白和注释),可以省略该分号。

状态

JShell 的状态保存在一个JShell实例中。使用eval(...)方法在JShell中评估代码片段,可以产生错误、声明代码或执行语句或表达式。对于具有初始化程序的变量,同时进行声明和执行。JShell的一个实例包含了先前定义和修改的变量、方法和类、先前定义的导入声明、先前输入的语句和表达式(包括变量初始化程序)的副作用以及外部代码库。

修改

由于期望的使用是探索性的,声明(变量、方法和类)必须能够随时间演变,同时保留已评估的数据。一种选择是在某些或所有情况下将更改的声明作为新的附加实体,但这肯定会导致混乱,并且与探索声明之间的交互不协调。在 JShell 中,每个唯一的声明键在任何给定时刻都有一个声明。对于变量和类,唯一的声明键是名称,对于方法,唯一的声明键是名称和参数类型(以允许重载)。由于这是 Java,变量、方法和类各自有自己的命名空间。

前向引用

在 Java 编程语言中,类的主体内部可以出现稍后出现的成员的引用;这称为前向引用。当代码按顺序输入和评估时,这些引用将临时未解析。在某些情况下,例如互相递归,前向引用是必需的。在输入代码时进行探索性编程时,这也可能发生,例如意识到应该调用另一个(迄今为止未编写的)方法。JShell 支持方法体、返回类型和参数类型中的前向引用,支持变量类型和类内部的前向引用。由于语义要求它们立即执行,因此不支持变量初始化程序中的前向引用。

代码片段依赖

代码状态保持最新和一致;也就是说,当评估代码片段时,任何对依赖片段的更改都会立即传播。

当成功声明一个代码片段时,声明将属于以下三种类型之一:添加修改替换。如果代码片段是具有该键的第一个声明,则它是添加的。如果代码片段的键与先前的代码片段匹配,但它们的签名不同,则它是替换的。如果代码片段的键与先前的代码片段匹配且它们的签名匹配,则它是修改的;在这种情况下,不会影响任何依赖的代码片段。在修改替换的情况下,先前的代码片段不再是代码状态的一部分。

当一个代码片段被添加时,它可能提供了一个未解决的引用。当一个代码片段被替换时,它可能会更新现有的代码片段。例如,如果一个方法的返回类型被声明为类C,然后类C替换,那么方法的签名已经改变,方法必须被替换。注意:这可能导致先前有效的方法或类变得无效。

期望用户数据尽可能保留。除了变量替换的情况外,这是可以实现的。当变量被替换时,无论是直接由用户还是间接通过依赖项更新,变量都被设置为其默认值(因为只有引用变量才能发生这种情况,所以默认值为null)。

当一个声明无效时,要么是因为前向引用,要么是通过更新变为无效,该声明将被"收容"。收容的声明可以在其他声明和代码中使用,但如果试图执行它,将会发生运行时异常,该异常将解释未解析的引用或其他问题。

包装

在 Java 编程语言中,变量、方法、语句和表达式必须嵌套在其他构造中,最终是一个类。当 JShell 的实现将变量、方法、语句和表达式片段作为 Java 代码编译时,需要一个人工上下文,如下所示:

  • 变量、方法和类
    • 作为合成类的静态成员
  • 表达式和语句
    • 作为合成类内部合成静态方法中的表达式和语句

这种包装还可以实现代码片段的更新,因此请注意,代码片段类也包装在一个合成类中。

模块化环境配置

jshell工具具有以下选项来控制模块化环境:

  • --module-path
  • --add-modules
  • --add-exports

模块化环境也可以通过将其直接添加到编译器和运行时选项来进行配置。编译器标志可以使用-C选项添加。运行时标志可以使用-R选项添加。

所有jshell工具选项都在工具参考中有文档(参见上面)。

可以使用JShell.Builder上的compilerOptionsremoteVMOptions方法在 API 级别对模块化环境进行配置。

JShell 的无名模块读取的模块集合与无名模块的默认根模块集合相同,由 JEP 261 "根模块"确立:

命名

  • 模块
    • jdk.jshell
  • 工具启动器
    • jshell
  • API 包
    • jdk.jshell
  • SPI 包
    • jdk.jshell.spi
  • 执行引擎"库"包
    • jdk.jshell.execution
  • 工具启动 API 包
    • jdk.jshell.tool
  • 工具实现包
    • jdk.internal.jshell.tool
  • OpenJDK 项目
    • Kulla

替代方案

一个更简单的替代方案是仅提供批处理脚本封装,而不支持交互/更新。

另一种选择是保持现状:使用另一种语言或使用第三方 REPL,例如BeanShell,尽管这个特定的 REPL 已经多年没有更新,它基于 JDK 1.3,并对语言进行了任意更改。

许多集成开发环境(IDE),例如 NetBeans 调试器和 BlueJ 的 CodePad,提供交互式评估表达式的机制。保留的上下文和代码仍然是基于类的,不支持方法粒度。它们使用特殊设计的解析器/解释器。

测试

该 API 便于进行详细的测试。测试框架可使编写测试变得简单。

由于工具的评估和查询功能是基于 API 构建的,因此大多数测试都是针对 API 的。然而,还需要对命令进行测试和对工具进行健全性测试。工具使用测试工具集来构建,这在进行工具测试时使用。

测试由三个部分组成:

  1. API 的测试。这些测试涵盖了正向和反向情况。每个公共方法都必须有其对应的测试,包括添加变量、方法和类,重新定义它们等。
  2. jshell 工具的测试。这些测试检查jshell命令、编译和执行 Java 代码的正确行为。
  3. 压力测试。为了确保 JShell 能够编译所有允许的 Java 代码片段,将使用 JDK 本身的正确 Java 代码进行测试。这些测试解析源代码,将代码块传递给 API 进行测试,并测试 API 的行为。

依赖关系

实现将尽最大努力利用 JDK 中现有语言支持的准确性和工程工作。