跳到主要内容

JEP 222:jshell:Java Shell(读取-求值-打印循环)

概括

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

目标

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

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

非目标

新的交互式语言不是目标:所有接受的输入都必须与 Java 语言规范 (JLS) 中的语法产生式相匹配。此外,在适当的周围上下文中,所有接受的输入都必须是有效的 Java 代码(JShell 将自动提供该周围上下文 - “包装”)。也就是说,如果X是 JShell 接受的输入(而不是错误地拒绝),则存在一个AB,它AXB是 Java 编程语言中的有效程序。

图形界面和调试器支持超出了范围。 JShell API 旨在允许 IDE 和其他工具中使用 JShell 功能,但该jshell工具并不旨在成为 IDE。

动机

在学习编程语言及其 API 时,即时反馈非常重要。学校放弃将 Java 作为教学语言的首要原因是其他语言有“REPL”,并且初始"Hello, world!"程序的门槛要低得多。读取-评估-打印循环 (REPL) 是一种交互式编程工具,它循环不断地读取用户输入、评估输入并打印输入的值或输入引起的状态更改的描述。 Scala、Ruby、JavaScript、Haskell、Clojure 和 Python 都有 REPL,并且都允许小型初始程序。 JShell 将 REPL 功能添加到 Java 平台。

探索编码选项对于开发人员构建代码原型或研究新 API 也很重要。在这方面,交互式评估比编辑/编译/执行和System.out.println.

没有仪式感class Foo { public static void main(String[] args) { ... } },学习和探索就会变得简单。

描述

功能性

JShell API 将提供 JShell 的所有评估功能。输入到 API 的代码片段称为“片段”。该jshell工具还将使用 JShell 完成 API 来确定输入何时不完整(并且必须提示用户输入更多信息)、何时添加分号(在这种情况下该工具将附加分号)以及如何完成输入。当使用选项卡请求完成时完成输入。该工具将有一组用于查询、保存和恢复工作以及配置的命令。命令将通过前导斜杠与片段区分开。

文档

JShell 模块 API 规范可在此处找到:

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

工具jshell参考:

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

条款

在本文档中,术语“类”是指 Java 虚拟机规范 (JVMS) 中使用的含义,其中包括 Java 语言规范 (JLS) 类、接口、枚举和注释类型。文本清楚地表明是否有不同的含义。

片段

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

  • 表达
  • 陈述
  • 类声明
  • 接口声明
  • 方法声明
  • 字段声明
  • 进口报关

在 JShell 中,“变量”是一个存储位置并具有关联的类型。使用_FieldDeclaration_片段显式创建变量:

int a = 42;

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

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

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

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

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

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

除了“break”、“continue”和“return”之外,所有语句都被接受为片段。但是,代码片段可能包含“break”、“continue”或“return”语句,它们满足 Java 编程语言封闭上下文的通常规则。例如,此代码片段中的 return 语句是有效的,因为它包含在 lambda 表达式中:

() -> { return 42; }

声明片段(ClassDeclarationInterfaceDeclarationMethodDeclaration_或_FieldDeclaration)是显式引入可由其他片段引用的名称的片段。声明片段须遵守以下规则:

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

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

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

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

在该jshell工具中,如果片段的终端分号是输入的最后一个字符(不包括空格和注释),则可以省略该分号。

状态

JShell 状态保存在 的实例中JShell。代码片段在JShellwitheval(...)方法中进行求值,产生错误,声明代码,或者执行语句或表达式。对于带有初始值设定项的变量,声明和执行都会发生。的实例JShell包含先前定义和修改的变量、方法和类、先前定义的导入声明、先前输入的语句和表达式(包括变量初始值设定项)的副作用以及外部代码库。

修改

由于所需的用途是探索,因此声明(变量、方法和类)必须能够随着时间的推移而演变,同时保留评估的数据。一种选择是在某些或所有情况下将更改后的声明作为新的附加实体,但这肯定会造成混乱,并且不利于探索声明之间的交互。在 JShell 中,每个唯一的声明键在任何给定时间都只有一个声明。对于变量和类,唯一声明键是名称,而方法的唯一声明键是名称和参数类型(以允许重载)。由于这是 Java,变量、方法和类都有自己的名称空间。

前向参考

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

代码段依赖项

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

成功声明代码片段后,声明将是以下三种类型之一:AddedModified_或_Replaced。如果它是带有该键的第一个声明,则会_添加_一个片段。如果某个片段的密钥与之前的片段匹配,但它们的签名不同,则该片段将被_替换_。如果一个片段的密钥与之前的片段匹配并且它们的签名匹配,则该片段被_修改_;在这种情况下,不会影响任何依赖片段。在_修改_和_替换的_情况下,先前的代码片段不再是代码状态的一部分。

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

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

当声明无效时,无论是由于前向引用还是通过更新而变得无效,该声明都会被“限制”。围栏声明可以在其他声明和代码中使用,但是,如果尝试执行它,将会发生运行时异常,这将解释未解决的引用或其他问题。

包装

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

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

这种包装还支持代码片段更新,因此请注意,代码片段类也包装在合成类中。

模块化环境配置

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

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

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

所有jshell工具选项均记录在工具参考中(见上文)。

可以使用上的compilerOptions和方法在 API 级别配置模块化环境。remoteVMOptions``JShell.Builder

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 项目
    • 库拉

备择方案

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

另一种选择是维持现状:使用另一种语言或使用第三方 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 中现有语言支持的准确性和工程工作。 JShell 状态被建模为 JVM 实例。代码分析和可执行代码(API)的生成将由Java编译器( )通过Compiler APIjdk.jshell来执行。javac代码替换 ( jdk.jshell.execution) 将使用 Java 调试接口 (JDI)。

原始片段(即尚未包装的片段)的解析将使用编译器 API 和解析器的一个小子类来完成,以允许原始片段。生成的信息将用于将代码片段包装到有效的编译单元中,其中包括带有先前评估代码的导入的类声明。类文件的进一步分析和生成将使用未修改的 Java 编译器实例来完成。生成的类文件将保存在内存中,并且永远不会写入存储。jdk.jshell.spiSPI 的存在是为了配置执行引擎。默认执行引擎的行为如下。类文件将通过套接字发送到远程进程。远程代理将处理加载和执行。更换将通过 JDI 设施进行VirtualMachine.redefineClasses()

Tab 补全分析 ( jdk.jshellAPI) 也将使用 Compiler API。完成检测将使用javac词法分析器、自定义和表驱动代码。

jshell工具 ( jdk.internal.jshell.tool) 将使用“jline2”进行控制台输入、编辑和历史记录。jline2已私下集成到 JDK 中。