JEP 222: jshell:Java Shell(读取-求值-打印循环)
概述
提供一个交互式工具,用于评估 Java 编程语言的声明、语句和表达式,并附带一个 API,以便其他应用程序可以利用此功能。
目标
JShell API 和工具将提供一种在 JShell 状态下交互式评估 Java 编程语言的声明、语句和表达式的方式。JShell 状态包括不断发展的代码和执行状态。为了促进快速调查和编码,语句和表达式不必出现在方法内,变量和方法也不必出现在类内。
jshell
工具将是一个命令行工具,具有简化交互的功能,包括:带有编辑功能的历史记录、Tab 键自动补全、自动添加所需的终端分号,以及可配置的预定义导入和定义。
非目标
一个新的交互式语言并不是目标:所有被接受的输入都必须符合《Java 语言规范》(JLS)中的语法规则。此外,在适当的上下文环境中,所有被接受的输入都必须是有效的 Java 代码(JShell 会自动提供该上下文环境——即“包装”)。也就是说,如果 X
是 JShell 接受的输入(而不是因错误而拒绝的输入),那么必然存在 A
和 B
,使得 AXB
是 Java 编程语言中的一个有效程序。
超出范围的是图形界面和调试器支持。JShell API 旨在允许在 IDE 和其他工具中使用 JShell 功能,但 jshell
工具并非旨在成为 IDE。
动机
在学习编程语言及其 API 时,即时反馈非常重要。学校放弃使用 Java 作为教学语言的首要原因在于,其他语言拥有“REPL”,并且编写初始的 "Hello, world!"
程序的门槛要低得多。Read-Eval-Print Loop(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 来判断输入何时不完整(这时需要提示用户补充更多内容),何时在添加分号后会变得完整(这种情况下工具会自动添加分号),以及在通过 Tab 请求补全时如何完成输入。该工具将拥有一组用于查询、保存和恢复工作以及配置的命令。这些命令将通过前导斜杠与代码片段区分开来。
文档
JShell 模块 API 规范可以在此处找到:
其中包含主要的 JShell API(jdk.jshell
包)规范:
jshell
工具参考:
是 Java 平台标准版工具参考的一部分:
此链接为 Oracle 官方文档,提供 Java SE 9 工具与命令参考相关内容。请根据需要访问查看详细信息。
术语
在本文档中,“类”这一术语是在《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 是片段标识符。如果表达式是无效的(例如 println
示例),或者表达式的值已经可以通过一个简单名称引用(如上面的 'a' 和 'a=1' 的情况;所有其他示例都为它们隐式创建了变量),则不会隐式创建变量。
所有语句都被视为代码片段,除了 break
、continue
和 return
。然而,代码片段中可以包含 break
、continue
或 return
语句,只要它们符合 Java 编程语言对封闭上下文的常规规则。例如,此代码片段中的 return
语句是有效的,因为它被包含在一个 lambda 表达式中:
() -> { return 42; }
声明片段(ClassDeclaration、InterfaceDeclaration、MethodDeclaration 或 FieldDeclaration)是指显式引入一个可被其他片段引用的名称的代码片段。声明片段需遵循以下规则:
- 访问修饰符(
public
、protected
和private
)被忽略(所有声明片段对所有其他片段都是可访问的) - 修饰符
final
被忽略(允许未来的更改/继承) - 修饰符
static
被忽略(没有用户可见的包含类) - 修饰符
default
和synchronized
不被允许 - 修饰符
abstract
仅允许用于类。
所有代码片段(ImportDeclaration 形式的除外)均可包含嵌套声明。例如,作为类实例创建表达式的代码片段可以指定一个带有嵌套方法声明的匿名类主体。Java 编程语言的常规规则适用于嵌套声明上的修饰符,而不是上述规则。例如,以下类代码片段是被接受的,并且嵌套方法声明中的 private
修饰符会被尊重,因此代码片段 "new C().secret()
" 将不被接受:
class C {
private void secret() { }
}
class C {
int answer() { return 2 * secret(); }
private int secret() { return 21; }
}
代码段不得声明包或模块。 所有 JShell 代码都放在一个未命名模块中的单个包内。 包的名称由 JShell 控制。
在 jshell
工具中,如果代码片段的末尾分号是输入的最后一字符(不包括空白字符和注释),则可以省略该分号。
状态
JShell 的状态保存在 JShell
的一个实例中。代码片段通过 JShell
中的 eval(...)
方法进行求值,可能会生成错误、声明代码,或者执行语句和表达式。对于带有初始化器的变量,声明和执行会同时发生。JShell
的一个实例包含先前定义和修改过的变量、方法和类,先前定义的导入声明,先前输入的语句和表达式(包括变量初始化器)的副作用,以及外部代码库。
修改
由于预期的用途是探索,声明(变量、方法和类)必须能够随着时间的推移而演变,同时保留已评估的数据。一种选择是,在某些或所有情况下,将更改后的声明作为一个新的附加实体,但这肯定会令人困惑,并且不便于探索声明之间的交互。在 JShell 中,每个唯一的声明键在任何给定时间都只有一个声明。对于变量和类,唯一声明键是名称;对于方法,唯一声明键是名称和参数类型(以允许重载)。由于这是 Java,变量、方法和类各自都有其自己的命名空间。
前向引用
在 Java 编程语言中,在类的主体内,可以引用后面才会出现的成员;这就是前向引用。由于 JShell 是按顺序输入和评估代码,这些引用将暂时无法解析。在某些情况下,例如相互递归,就需要前向引用。这在探索性编程中输入代码时也可能发生,例如,意识到应该调用另一个(到目前为止尚未编写)的方法。JShell 支持方法体、返回类型和参数类型、变量类型以及类内部的前向引用。由于语义要求它们立即执行,所以不支持变量初始化器中的前向引用。
代码片段依赖
代码状态保持最新并具有一致性;也就是说,当一个代码片段被评估时,任何对依赖片段的更改都会立即传播。
当一个代码片段成功声明后,声明将分为三种类型:新增(Added)、修改(Modified) 或 替换(Replaced)。如果这是使用该键的首次声明,则代码片段为新增。如果代码片段的键与之前的代码片段匹配,但它们的签名不同,则代码片段为替换。如果代码片段的键与之前的代码片段匹配且它们的签名也匹配,则代码片段为修改;在这种情况下,不会影响任何依赖的代码片段。在修改和替换的情况下,先前的代码片段不再是代码状态的一部分。
当一个代码片段被添加时,它可能会提供一个未解析的引用。当一个代码片段被替换时,它可能会更新现有的代码片段。例如,如果一个方法的返回类型被声明为类 C
,然后类 C
被替换,那么该方法的签名就发生了变化,并且该方法也必须被替换。注意:这可能会导致之前有效的方法或类变得无效。
希望尽可能持久保存用户数据。除了变量 Replace 的情况外,这一目标是可以实现的。当一个变量被替换时(无论是用户直接替换,还是通过依赖更新间接替换),该变量会被设置为其默认值(null
,因为这种情况只会发生在引用变量上)。
当声明由于前向引用或因更新而变得无效时,该声明会被“隔离”。被隔离的声明可以用于其他声明和代码中,但是,如果尝试执行它,则会发生运行时异常,该异常将解释未解析的引用或其他问题。
包装
在 Java 编程语言中,变量、方法、语句和表达式必须嵌套在其他结构中,最终嵌套在一个类中。当 JShell 的实现将变量、方法、语句和表达式片段编译为 Java 代码时,需要一个人为的上下文环境,如下所示:
- 变量、方法和类
- 作为合成类的静态成员
- 表达式和语句
- 作为合成类中合成静态方法内的表达式和语句
这种包装还支持片段更新,因此,请注意片段类也会被包装在合成类中。
模块化环境配置
jshell
工具具有以下用于控制模块化环境的选项:
--module-path
--add-modules
--add-exports
模块化环境也可以通过直接添加到编译器和运行时选项来配置。编译器标志可以通过 -C
选项添加。运行时标志可以通过 -R
选项添加。
所有 jshell
工具选项均在工具参考中有文档说明(见上文)。
模块化环境可以通过 JShell.Builder
上的 compilerOptions
和 remoteVMOptions
方法在 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 进行的。然而,该工具的命令测试和健全性测试也是必需的。该工具内置了用于测试框架的钩子,这些钩子被用来进行工具测试。
测试由三个部分组成:
-
API 的测试。这些测试涵盖了正面和负面的情况。每个公共方法都必须通过测试覆盖,其中包括添加变量、方法和类,重新定义它们等。
-
jshell 工具的测试。这些测试检查
jshell
命令以及 Java 代码的编译和执行是否具有正确的行为。 -
压力测试。为了确保 JShell 能够编译所有允许的 Java 代码片段,将使用来自 JDK 本身的正确 Java 代码。这些测试解析源代码,将代码块传递给 API,并测试 API 的行为。
依赖
该实现将尽一切努力利用 JDK 中现有语言支持的准确性和工程工作。JShell 状态被建模为一个 JVM 实例。代码分析和可执行代码的生成(jdk.jshell
API)将通过编译器 API 由 Java 编译器(javac
)执行。代码替换(jdk.jshell.execution
)将使用 Java 调试接口(JDI)。
解析原始代码片段(即,未被包装的代码片段)将使用编译器 API 进行,并对解析器进行一个小的子类化以允许原始代码片段。所得信息将用于将代码片段包装到一个有效的编译单元中,其中包括带有先前已评估代码导入的类声明。类文件的进一步分析和生成将使用未经修改的 Java 编译器实例完成。生成的类文件将保存在内存中,而不会写入存储设备。jdk.jshell.spi
SPI 用于配置执行引擎。默认的执行引擎行为如下:类文件将通过套接字发送到远程进程。远程代理将处理加载和执行。替换将通过 JDI 的 VirtualMachine.redefineClasses()
功能完成。
制表符补全分析(jdk.jshell
API)也将使用编译器 API。补全检测将使用 javac
词法分析器、自定义代码和表格驱动的代码。
jshell
工具(jdk.internal.jshell.tool
)将使用 jline2
进行控制台输入、编辑和历史记录管理。jline2
已被私下集成到 JDK 中。