Java 9 揭秘(11. Java Shell下)

Tips
做一个终身学习的人。

Java 9

二十二. 配置JShell

使用/set命令,可以自定义jshell会话,从启动片段和命令到设置平台特定的片段编辑器。

1. 设置代码编辑器

JShell工具附带一个默认的代码编辑器。 在jshell中,可以使用/edit命令来编辑所有的片段或特定的片段。 /edit命令在编辑器中打开该片段。 代码编辑器是一个特定于平台的程序,如Windows上的notepad.exe,将被调用来编辑代码段。 可以使用/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>

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

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

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>SET JSHELLEDITOR=notepad.exe
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor notepad.exe
jshell>

2. 设置反馈模式

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

  • silent
  • concise
  • normal
  • verbose

silent模式根本不给任何反馈,verbose模式提供最多的反馈。 concise模式给出与normal模式相同的反馈,但是格式紧凑。 设置反馈模式的命令如下:

/set feedback [-retain] <mode>

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

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

jshell --feedback <mode>

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

C:\Java9Revealed>jshell --feedback verbose

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

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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会话中持续反馈模式,使用以下命令:

jshell> /set feedback -retain

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

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

jshell> /set feedback -retain verbose

要确定当前的反馈模式,只需使用反馈参数执行`/se命令。 它打印用于在第一行设置当前反馈模式的命令,然后是所有可用的反馈模式,如下所示:

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

Tips
当学习jshell时,建议以verbose反馈模式启动它,因此可以获得有关命令和代码段执行状态的详细信息。 这将有助于更快地了解该工具。

3. 创建自定义反馈模式

这四个预配置的反馈模式很适合使用jshell。 它们提供不同级别的粒度来自定义您shell。 当然,可以拥有自己的自定义反馈模式。必须编写几个定制步骤。 很可能,将需要在预定义的反馈模式中自定义一些项目。 可以从头开始创建自定义反馈模式,或者通过从现有的反馈模式中复制自定义反馈模式,并有选择地进行自定义。 创建自定义反馈模式的语法如下:

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

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

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

/set mode kverbose verbose -command

以下命令将持续使用名为kverbose的新反馈模式以备将来使用:

/set mode kverbose -retain

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

/set mode kverbose -delete -retain

在这一点上,预定义的详细模式和自定义kverbose模式之间没有区别。 创建反馈模式后,需要自定义三个设置:

  • 提示
  • 输出截断限制
  • 输出格式

Tips
完成定制反馈模式之后,需要使用/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个字符,并将变量值或表达式设置为五个字符:

/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}创建一个类型为{type}的名为{name}的临时变量,并使用{value} {post}”初始化“表达式添加,修改,替换原来的信息。

以下jshell会话通过从预定义的详细反馈模式复制所有设置来创建一个名为kverbose的新反馈模式。 它自定义提示,截断限制和输出格式。 它使用verbose和kverbose反馈模式来比较jshell行为。 请注意,以下示例中的所有命令都需要以一行形式输入。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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。这不是一个错误。 这是由你的设置造成的。

请注意,命令/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
jshell>

4. 设置启动代码片段

可以使用/set命令和start参数来设置启动代码片段和命令。 启动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命令显示当前启动设置。 如果启动是从文件设置的,它会显示文件名,启动片段以及启动片段的设置时间。

请考虑以下情况。 com.jdojo.jshell目录包含一个com.jdojo.jshell.Person类。 在jshell中测试这个类,并使用java.time包中的类型。 为此,启动设置将如下所示。

/env -class-path C:\Java9Revealed\com.jdojo.jshell\build\classes
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); }

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

注意startup.jsh文件中的最后一个片段。 它定义了printf()的顶层函数,它是System.out.printf()方法的包装。 默认情况下,printf()函数包含在JShell工具的初始构建中。 后来被删除了。 如果要使用简短的方法名称(如printf())而不是System.out.printf(),以便在标准输出上打印消息,则可以将此代码段包含在启动脚本中。 如果希望在jshell中使用println()printf()顶层方法,则需要启动jshell,如下所示:

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

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

以下jshell会话将显示如何从文件中设置启动信息及其在子序列会话中的用法:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set start
|  /set start -retain startup.jsh
|  ---- startup.jsh @ Feb 20, 2017, 10:06:47 AM ----
|  /env -class-path C:\Java9Revealed\com.jdojo.jshell\build\classes
|  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()
$2 ==> 2016-11-15
jshell>
jshell> printf("2 + 2 = %d%n", 2 + 2)
2 + 2 = 4
jshell>

Tips
直到重新启动jshell,执行/reset/reload命令之前,设置启动片段/命令才会生效。 不要在启动文件中包含/reset或者/reload命令。 当启动文件加载时,它将导致无限循环。

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

  • DEFAULT
  • PRINTING
  • JAVASE

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

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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命令与主题作为参数打印有关主题的信息。 目前,有三个预定义的主题:intro,shortcuts和context。 以下命令将打印JShell工具的介绍:

jshell> /help intro

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

jshell> /help shortcuts

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

jshell> /help context

二十四. Shell API

JShell API可让你对片段求值引擎进行编程访问。 作为开发人员,不能使用此API。 这意味着要被诸如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接口的实例表示。 片段事件包含片段的当前和先前状态,片段的值,导致事件的片段的源代码,如果在片段执行期间发生异常,则为异常对象等。

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

  • 这是一个完整的片段吗?
  • 这个代码片段可以通过附加一个分号来完成吗?

SourceCodeAnalysis实例还提供了一些建议列表,例如Tab补全和访问文档。 此类旨在由提供JShell功能的工具使用。

下图显示了JShell API的不同组件的用例图。 在接下来的部分,解释这些类及其用途。 最后一节中给出了一个完整的例子。

用例图

1. 创建JShell类

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

  • 使用静态create()方法
  • 使用内部构建类JShell.Builder

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

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

JShell.Builder类允许通过指定代码段ID生成器,临时变量名称生成器,打印输出的打印流,读取代码片段的输入流以及错误输出流来记录错误来配置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实例的状态。

Tips
完成使用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()方法的代码示例代码:

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 */
});

2. 使用代码片段

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

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

Java中有几种类型的片段,例如变量声明,具有初始化的变量声明,方法声明,类型声明等。Snippet类是一个抽象类,并且有一个子类来表示每个特定类型的片段。 以下列表显示代表不同类型代码片段的类的继承层次结构:

  • Snippet
    • ErroneousSnippet
    • ExpressionSnippet
    • StatementSnippet
    • PersistentSnippet
      • ImportSnippet
      • DeclarationSnippet
        • MethodSnippet
        • TypeDeclSnippet
        • VarSnippet

Snippet类的子类的名称是直观的。 例如,PersistentSnippet的一个实例表示保存在JShell中的代码段,可以重用,如类声明或方法声明。 Snippet类包含以下方法:

String id()
String source()
Snippet.Kind kind()
Snippet.SubKind subKind()

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

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

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

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

  • DROPPED:该代码片片段由于使用JShell类的drop()方法删除而处于非有效状态。
  • NONEXISTENT:该代码段无效,因为它不存在。
  • OVERWRITTEN:该代码片段已被替换为新的代码片段,因此无效。
  • RECOVERABLE_DEFINED:该片段是包含未解析引用的声明片段。 该声明具有有效的签名,并且对其他代码段可见。 当其他代码段将其状态更改为VALID时,可以恢复并使用它。
  • RECOVERABLE_NOT_DEFINED:该片段是包含未解析引用的声明片段。 该代码段具有无效的签名,而其他代码片段不可见。 当其状态更改为VALID时,可以稍后使用。
  • REJECTED:代码片段无效,因为初始求值时编译失败,并且无法进一步更改JShell状态。
  • VALID:该片段在当前JShell状态的上下文中有效。

3. 处理代码片段事件

JShell会生成片段事件作为片段求职或执行的一部分。 可以通过使用JShell类的onSnippetEvent()方法注册事件处理程序或使用JShell类的eval()方法的返回值来执行代码段事件,返回类型是List <SnippetEvent>。 以下显示如何处理片段事件:

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());
        });
}

4. 一个实例

我们来看看JShell API的操作。 下面包含名为com.jdojo.jshell.api的模块的模块声明。

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

下面包含JShellApiTest类的完整代码,它是com.jdojo.jshell.api模块的成员。

// JShellApiTest.java
package com.jdojo.jshell.api;
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());
        }
    }
}

输出结果:

A JShellApiTest Class to Test the JShell API
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
------------------------
The main() method creates the following six snippets and stores them in a String array:
1.
"int x = 100;"
 
2.
"double x = 190.89;"
 
3.
"long multiply(int value) {return value * multiplier;}"
 
4.
"int multiplier = 2;"
 
5.
"multiply(200)"
 
6.
"mul(99)"
 

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

当执行这些代码片段时,让我们来看看JShell引擎的状态:

  • 执行代码段1时,代码段不存在,因此从NONEXISTENT转换为VALID状态。 它是一个变量声明片段,它的计算结果为100。
  • 当代码段2被执行时,它已经存在。 请注意,它使用不同的数据类型声明名为x的同一个变量。 其以前的状态为VALID,其当前状态也为VALID。 执行此代码段会将状态从VALID更改为OVERWRITTEN,因为不能使用同一名称的两个变量。
  • Snippet 3声明一个multiply()的方法,它使用一个multiplier的未声明变量,其状态从NONEXISTENT更改为RECOVERABLE_DEFINED。 定义了方法,这意味着它可以被引用,但不能被调用,直到定义了适当类型的multiplier变量。
  • Snippet 4定义了multiplier变量,使代码段3有效。
  • Snippet 5调用multiply()方法。 该表达式是有效的,结果为400。
  • Snippet 6调用mul()方法的,但从未定义过。 该片段是错误的并被拒绝。

通常,JShell API和JShell工具不会一起使用。 但是,让我们一起使用它们只是为了乐趣。 JShell API只是Java中的另一个API,也可以在JShell工具中使用。 以下jshell会话实例化一个JShell,注册一个片段事件处理器,并对两个片段求值。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  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:\Java9Revealed>

二十五. 总结

Java Shell在JDK 9中称为JShell,是一种提供交互式访问Java编程语言的命令行工具。 它允许对Java代码片段求值,而不是强制编写整个Java程序。 它是Java的REPL。 JShell也是一个API,可以为其他工具(如IDE)的Java代码提供对REPL功能的编程访问。

可以通过运行保存在JDK_HOME\bin目录下的jshell程序来启动JShell命令行工具。 该工具支持执行代码片段和命令。 片段是Java代码片段。 片段可以用来执行和求值,JShell维护其状态。 它还跟踪所有输入的片段的状态。 可以使用命令查询JShell状态并配置jshell环境。 为了区分命令和片段,所有命令都以斜杠(/)开头。

JShell包含几个功能,使开发人员更有效率,并提供更好的用户体验,例如自动补全代码并在工具中显示Javadoc。 JShell尝试使用JDK中已有的功能(如编译器API)来解析,分析和编译代码段,以及使用Java Debugger API将现有代码片段替换为JVM中的新代码片段。 JShell的设计使得可以在Java语言中使用新的构造,而不会对JShell工具本身进行很少或不用改动。

推荐阅读更多精彩内容