本文要点

  • Java Shell或JShell是官方提供的读取-求值-打印-循环,通常称为REPL,是在Java 9中引入的。
  • JShell提供了一个交互式shell,用于快速原型、调试、学习Java及Java API,所有这些都不需要public static void main方法,也不需要在执行之前编译代码。
  • 随着Java 10引入了“var”关键词,JShell简单了许多(而且更实用了)。
  • 本文将对JShell做一个全面的介绍,了解它所有的命令、用法以及它最有效的使用方法。
  • JShell非常适合提供中间反馈。它看上去可能没什么大不了的,但是,所有这些小事情(如在IDE中编译或运行单元测试)会随着时间推移慢慢积累。

JShell是什么?

Java Shell或JShell是官方提供的读取-求值-打印-循环,通常称为REPL,是在Java 9中引入的。它提供了一个交互式shell,用于快速原型、调试、学习Java及Java API,所有这些都不需要public static void main方法,也不需要在执行之前编译代码。此外,随着Java 10引入了var关键词,JShell简单了许多(而且更实用了)。

入门

注意:在这份指南中,为了使用关键词var,我们将使用Java 10,因此,为了跟着这份指南操作,你务必要确保至少已经安装了Java 10。

JShell的启动很容易,在命令行输入jshell即可。你会看到一条欢迎信息,而shell会等待你输入命令或任何合法的Java表达式。

$ jshell
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

让我们执行第一条命令。在shell提示符下,输入var greeting = “hello”,按下<enter>。你会看到下面的输出:

jshell> var greeting = "hello"
greeting ==> "hello"

你会注意到,它回显了greeting的值,确认当前值为hello。你可能还会注意到,你的表达式不需要分号。这是一个小而漂亮的特性!

为了完成我们的问候语,我们需要的一位听众。输入var audience =并按下<enter>。这次,JShell认识到,你的表达式不完整,并允许你在下一行继续输入。输入”world”并按下<enter>完成表达式。和前面一样,JShell会回显确认已设置的值。

jshell> var audience =
  ...> "world"
audience ==> "world"

Tab补全

你首先注意到的其中一件事情是,它完美集成了tab补全。

让我们把字符串greeting和audience串联起来,组成一个新变量saying。先输入var saying = gr,然后按下<tab>。你会看到,变量greeting自动补全了。用同样的方法输入变量audience,按下<enter>,就可以看到串联结果了。

jshell> var saying = gr<tab> + aud<tab>
saying ==> "helloworld"

Tab补全就和你预想的一样,自动补全唯一值或者在不确定时提供可能的值。它对之前输入的任何表达式、对象和方法均有效。注意,它对内置关键词无效。

如果你想要把变量saying变成大写,但是又没记住方法的具体名称,那么你只要输入saying.to,然后按下<tab>,就可以看到所有以to开头的所有有效方法了。

jshell> saying.to<tab>
toCharArray()   toLowerCase( toString()      toUpperCase(

可能有参数的方法显示时使用开括号,而没有参数的方法显示时使用闭括号。

错误

如果你不小心犯了个错误或者输入了一个非法表达式、方法或命令,那么JShell会立即反馈,显示错误,标注问题。

jshell> saying.subString(0,1)
|  Error:
|  cannot find symbol
|    symbol:   method subString(int,int)
|  saying.subString(0,1)
|  ^--------------^

方法签名

让我们调用toUpperCase方法,但推迟添加任何额外的参数,或者以一个圆括号结束。再次按下<tab>。这次,你会看到toUpperCase方法所有可用的方法签名;一个有一个Locale参数,另一个没有任何参数。

jshell> saying.toUpperCase(
Signatures:
String String.toUpperCase(Locale locale)
String String.toUpperCase()

<press tab again to see documentation>

文档(JavaDoc)

如果你第三次按下<tab>,你就会看到toUpperCase(Locale)方法的JavaDoc文档。

jshell> saying.toUpperCase(
String String.toUpperCase(Locale locale)
Converts all of the characters in this String to upper case ... (shortened for brevity)

继续按下<tab>,就可以依次查看所有可用的方法签名及其相关文档。

导入

让我们把这个例子扩展到其他的听众,如Universe和Galaxy,而不仅仅是hello world。首先创建一个名为audiences的列表,其中有三个不同的听众:world、universe、galaxy。使用List构造函数和Java 9提供的静态工厂方法,只需要一行代码即可实现。

jshell> var audiences = new ArrayList<>(List.of("world", "universe", "galaxy"))
audiences ==> [world, universe, galaxy]

注意,你不必使用完整限定类名(FQCN)来引用ArrayList,也不必import java.util包。这是因为,在默认情况下,JShell启动时会自动执行一些预定义导入,减少导入常用包或输入FQCN的麻烦。

下面是默认导入的包:

  • java.io.*
  • java.math.*
  • java.net.*
  • java.nio.file.*
  • java.util.*
  • java.util.concurrent.*
  • java.util.function.*
  • java.util.prefs.*
  • java.util.regex.*
  • java.util.stream.*

正如你所料,你也可以根据需要输入import <pkg_name>定义自己的导入,其中<pkg_name>是类路径上一个有效的软件包。

方法

现在,让我们定义一个方法getRandomAudience,用于随机选取一名听众。该方法接收一个听众列表(List<String>),随机返回列表中的一名听众。你可以直接在命令行中定义方法,就像你在类中定义方法一样,不过,你不需要定义一个类!

jshell> public String getRandomAudience(List<String> audiences) {
  ...> return audiences.get(new Random().nextInt(audiences.size()));
  ...> }
|  created method getRandomAudience(List<String>)

如果一切顺利,JShell会显示方法已经成功创建,可以使用了。

让我们尝试调用这个方法,并传递听众列表。多调用几次,确保每次获得不同的结果。

jshell> getRandomAudience(audiences)
$7 ==> "world"

jshell> getRandomAudience(audiences)
$8 ==> "universe"

jshell> getRandomAudience(audiences)
$9 ==> "galaxy"

这里有一件很有趣的事需要注意,在方法体中,你可以引用之前定义的任何变量和尚未定义的变量(稍后会详细介绍)。

让我们创建getRandomAudience方法的另外一个版本,它不接收参数,直接在方法体内使用我们的听众列表。

jshell> public String getRandomAudience() {
  ...> return audiences.get(new Random().nextInt(audiences.size()));
  ...> }
|  created method getRandomAudience()

再次执行几遍。

jshell> getRandomAudience()
$10 ==> "galaxy"

jshell> getRandomAudience()
$11 ==> "world"

jshell> getRandomAudience()
$12 ==> "galaxy"

我上面提到过,方法还可以使用尚未定义的变量。让我们定义一个名为getSeparator的新方法,返回一个可以用来分隔单词的值。不过,这一次,我们将使用一个未定义的变量wordSeparator。

jshell> public String getSeparator() {
  ...> return wordSeparator;
  ...> }
|  created method getSeparator(), however, it cannot be invoked until variable wordSeparator is declared

注意,JShell创建了getSeparator方法, 但告诉我们,在我们声明或定义wordSeparator变量之前,该方法不能使用。任何调用它的尝试都会产生一条类似的错误信息。

jshell> getSeparator()
|  attempted to call method getSeparator() which cannot be invoked until variable wordSeparator is declared

把变量wordSeparator简单地定义成一个空格,再次尝试调用它。

jshell> var wordSeparator = " "
wordSeparator ==> " "

jshell> getSeparator()
$13 ==> " "

有一点需要特别注意,你无法创建一个“顶级”静态方法。如果你这样做,就会收到一条警告信息,告诉你static关键词被忽略了。

jshell> public static String foobar(String arg) {
  ...> return arg;
  ...> }
|  Warning:
|  Modifier 'static'  not permitted in top-level declarations, ignored
|  public static void foobar(String arg) {
|  ^-----------^

临时变量

除了显式声明和定义的变量外,JShell会自动为任何未赋值表达式创建变量。在上一节调用getSeparator和getRandomAudience方法时,你可能已经注意到这些变量,我们称为“临时变量(Scratch Variables)”。

临时变量遵循一个固定的模式,以$开头,后面跟一个递增的数字。你可以像引用其他任何变量一样引用它们。例如,我们再次调用getRandomAudience方法,把结果作为System.out.println的参数。

jshell> getRandomAudience()
$14 ==> "galaxy"

jshell> System.out.println($14)
galaxy

在JShell中,你可以像创建方法一样创建类,一行一行输入,直到类结束。JShell会提醒你,类已创建。

jshell> public class Foo {
  ...> private String bar;
  ...> public String getBar() {
  ...> return this.bar;
  ...> }
  ...> }
|  created class Foo

在JShell中创建类(和方法)非常费力。没有格式,犯错会令人沮丧,因为在你完成这个类之前你都不知道自己已经犯错了。要了解更好的类创建方式,请查阅下一节里JShell命令的/open命令。

扩展类库

到目前为止,我们对JShell有了基本的了解,你可能会想知道,如何在JShell中使用外部类库(jars),如公司内部库或像Apache Commons这样的公共库。幸运的是,这很容易。你只要在启动JShell时使用–class-path参数。该参数使用带有分隔符的标准类路径格式。

$ jshell --class-path /path/to/foo.jar

JShell命令

到目前为止,我们仅仅使用了Java表达式,但JShell还提供了若干内置命令。让我们换个角度,探索下JShell中可用的命令。要查看所有可用命令的列表,在提示符下输入/help。注意,tab补全也适用于命令。

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
|  /drop <name or id>
|       delete a source entry
(shortened for brevity)

如果你想了解有关特定命令的详细信息,你可以输入/help <command>,用命令的名字代替<command>。


jshell> /help list
|
|                                   /list
|                                   =====
|
|  Show the snippets, prefaced with their snippet IDs.

让我们看一些最有用的命令。

List命令

/list命令输出之前输入的所有代码片段,而且每一段都有一个独一无二的标识,称为片段ID。

jshell> /list
  1 : var greeting = "hello";
  2 : var audience = "world";
  3 : var saying = greeting + audience;
  4 : saying.toUpperCase()

在默认情况下,输出不包含任何产生了错误的片段。只有有效的语句或表达式才会显示。

要查看之前输入的所有代码,包括错误,则可以给/list命令传入参数-all。

s1 : import java.io.*;
s2 : import java.math.*;
s3 : import java.net.*;
(shortened for brevity)
s10 : import java.util.stream.*;
1 : var greeting = "hello";
2 : var audience = "world";
3 : var saying = greeting + audience;
4 : saying.toUpperCase()
e1 : var thisIsAnError

输出会包含任何启动代码(稍后详细介绍)以及任何有效或无效的片段。JShell会根据片段的类型给每个片段ID添加一个前缀。下面是快速确定其意义的方法:

  • s:片段ID以s开头的是启动代码。
  • e:片段ID以e开头的产生了错误。
  • 片段ID没有前缀的是有效片段。

Vars、Methods、Types、Imports和Reset命令

JShell提供了多个命令帮助你查看shell的当前状态或上下文。它们都有恰当的名称,而且简单易懂,但是完备起见,我们把它们都列在这里。

你可以使用/vars查看声明的所有变量和它们的值。

jshell> /vars
|    String greeting = "hello"
|    String audience = "world"
|    String saying = "helloworld"

你可以使用/methods命令列出声明的所有方法和它们的签名。

jshell> /methods
|    String getRandomAudience(List<String>)
|    String getRandomAudience()

你可以使用/types命令列出所有类型声明。

jshell> /types
|    class Foo

你可以使用/imports命令列出当前声明的所有导入。

jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
(shortened for brevity)

最后,你可以使用/reset命令重置和清理包括变量、方法和类型在内的所有状态。

jshell> /reset
|  Resetting state.

jshell> /vars
(no variables exist after reset)

Edit命令

/edit用于编辑之前输入的片段。Edit命令适用于所有类型的片段,包括有效的、无效的和启动片段。它特别适合编辑产生了错误的多行代码,使你不必重新输入任何东西。

在上文中,当把变量greeting和audience串联成变量saying时,“hello”和“world”之间少了个空格。你可以通过输入/edit和片段ID来编辑。JShell Edit Pad会弹出来,你可以根据需要做任何修改。你还可以使用变量名称代替片段ID。

jshell> /edit 3
(... new JShell Edit Pad window opens ...)

jshell> /edit saying
(... new JShell Edit Pad window opens ...)

编辑完成后,你可以点击Accept按钮,JShell将对编辑后的片段重新求值。如果重新求值发现片段没有包含任何错误,则给编辑后的片段赋予一个新的片段ID。

你还可以给/edit 传入一个范围或多个ID,一次编辑多个片段。

jshell> /edit 1-4
(... new JShell Edit Pad window opens with snippets 1 through 4 ...)

jshell> /edit 1 3-4
(... new JShell Edit Pad window opens with snippets 1 and 3 through 4 ...)

Drop命令

/drop用于删除之前的任何片段。

除了编辑行,你还可以选择使用/drop命令删除它。它的用法和edit命令一样,你可以使用片段ID、变量、范围或者它们的组合作为参数。

jshell> /drop 3
|  dropped variable $3

jshell> /drop saying
|  dropped variable saying

jshell> /drop 3-4
|  dropped variable saying
|  dropped variable $4

Save命令

/save使你可以把之前输入的片段的输出保存到一个文件。

除了保存输出的文件,/save命令还接收另外的参数,用于指定需要保存的片段ID。该参数的用法和/edit及/drop命令的一样,位于文件名参数之前。

如果未指定任何片段ID,则保存之前输入的所有片段。

jshell> /save output.txt

jshell> /save 3-4 output.txt

/save和/open命令(下文介绍)搭配使用会非常有用,可以用于保存当前会话,并稍后恢复。要保存当前会话,包括所有的错误,调用/save命令,传入参数-all。

jshell> /save -all my_jshell_session.txt

Open命令

/open命令可以打开之前保存的任何输出,并对其重新求值(包括错误!)

jshell> /open my_jshell_session.txt

为方便使用,JShell还提供了一些预定义的“文件名”:

  • DEFAULT——包含默认导入片段的文件;
  • PRINTING——包含若干预定义打印方法的文件;
  • JAVASE——包含所有Java SE程序包导入的文件。

例如,如果你不想每次都使用System.out.println打印东西,那么你可以打开PRINTING文件,该文件定义了许多快捷方法,其中有一个名为print。

jshell> /open PRINTING

jshell> print("hello")
hello

常见和有效的用法

为了充分利用JShell,你应该了解其中一些常见和有效的用法。

JShell特别适合于以下场景:

  • 学习和提升Java语言知识;
  • 探索或发现JDK内外的新API;
  • 快速原型化想法或概念。

用JShell学习

对于Java,我们都有可以提高的地方。不管是泛型,还是多线程,JShell都是一个非常有效的学习工具。

JShell之所以会成为一个很棒的学习工具是因为它提供了一个持续不断的反馈循环。你输入一个命令,它告诉你结果。就是这么简单。而且,虽然很简单,但很有效。像俗话说的那样,它让你可以“快速行动,推陈出新”。

用JShell发现或探索

Java语言不断发展和增加新API(比过去任何时候都快)。

例如,考虑下Java 8中引入的Streams API。这是JDK的一个重要补充。有许多东西需要探索。但是,在Java 8中,Streams API还不完善。Streams API是一个处于不断演化中的API,Java 9 和Java 10都添加了新特性和功能。

下次,你想要探索Java的新特性时,可以考虑使用JShell。

用JShell快速创建原型

我们都会遇到原型化想法的情况。在那些情况下,你通常发现自己在创建一个新的测试项目,编写JUnit测试,或者编写一个具有main方法的简单Java类。有点仪式化,实际上有点麻烦!

JShell是一个非常有效的测试新想法的工具。你不必编写单元测试,或者是具有main方法的简单Java类,你可以使用JShell,借助命令行,或者/open命令和一个预先编写好的文件。借助JShell,下面这些事情你就不需要做了:

  • 编译代码;
  • 给类和文件起一样的名字;
  • 准备多个源文件或嵌套类/内部类。

总之,所有这些都相当于加速了“想法转化”。

JShell使用技巧

命令行使用技巧

JShell使用JLine2驱动命令行。这相当于Java中的GNU ReadLine,使你可以编辑或浏览在命令行上输入的命令。所有现代化的shell,如Bash,都使用它(这就是你为什么不能使用CTRL-V在shell中粘贴)。这就是说,JShell有一些非常强大的“快捷方式”。

以下是其中最常用的一些:

  • CTRL-A——把光标移到当前行的开头;
  • CTRL-E——把光标移到当前行的结尾;
  • ALT-F——向前移动一个单词;
  • ALT-B——向后移动一个单词;
  • CTRL-K——剪切到行尾;
  • CTRL-U——剪切至行首;
  • CTRL-W——剪切把光标前的单词;
  • CTRL-Y——粘贴剪贴板中的最后一项;
  • CTRL-R——向后搜索历史记录;
  • CTRL-S——向前搜索历史记录。

类路径使用技巧

在加载外部类库时,如果要输入完整的路径会非常恼人。因此,你可以把当前路径改成所有外部类库所在的路径,从那个目录启动jshell,使用星号(用引号引起来)包含所有的jar包。这适用于所有操作系统。

$ jshell --class-path "*"

同样的命令也适用于路径。该命令同样适用于所有的操作系统。

$ jshell --class-path "libs/*"

还有一个不错的建议:如果你已经输入了若干命令,但启动时忘了设置类路径,那么你可以使用/env命令设置类路径。

jshell> /env --class-path foo.jar
|  Setting new options and restoring state.

节省时间的技巧

对于JShell,你可以维护一个常用类库、命令或片段的专用目录,从而节省大量的时间。

对于新手,你可以从我GitHub上的示例库生成分支。

那个库包含如下几个目录:

  • imports
  • libs
  • startups
  • utils

让我们逐个看一下。

Imports

该目录包含预先定义好的常用导入。

随着使用JShell越来越多,你会发现,在想要使用或试验一个特定的外部类库时,重新输入一堆导入语句会变得非常痛苦。

为此,你可以把所有必要的导入语句保存到一个文件中,然后利用/open命令把它们引入进来。

定义导入文件的粒度由你决定。你可以选择针对每个库定义(例如guava-imports)或针对每个项目定义(例如my-project-imports),或者其他最适合你的方式。

jshell> /open imports/guava-imports

jshell> /imports
(shortened for brevity)
|    import java.util.stream.*
|    import com.google.common.collect.*

Libs

该目录几乎不需要再多加说明了,其中包含你可能在JShell中使用的所有外部类库。你可以选择任何你认为最有意义的方式组织你的库,不管是全部在一个目录中,还是一个项目一个目录。

不管你的组织策略是什么,使所有外部类库都以一种易于加载的方式组织最终会为你节省大量的时间,就像我们在类路径使用技巧部分看到的那样。

Startups

你可以使用这个目录存储任何启动或初始化代码。JShell使用参数–startup直接提供了对这一特性的支持。

$ jshell --startup startups/custom-startup

#####################
Loaded Custom Startup
#####################

|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

jshell>

本质上讲,这些文件和位于imports目录中的文件类型类似,但是,它们不只是导入。这些文件旨在包含初始化JShell环境所需的任何必要的命令、导入、片段、方法、类等。

如果你熟悉Bash的话,你会发现,启动文件和.bash_profile文件非常像。

Utils

我们都知道Java可以多繁琐。这个目录,正如它的名字那样,是为了包含任何工具或“快捷代码”,使你可以更愉快地使用JShell。这里,你存储的文件类型和JShell专门提供的PRINTING文件很相似,它定义了若干用于文本打印的快捷方法。

例如,如果你大量使用大数值,你每次想要加、乘、减一个数时都得输入类型new BigInteger,那你很快就会厌烦。为此,你可以创建一个工具文件,其中包含可以简化代码的辅助程序或快捷方法。

jshell> /open big-integer-utils



jshell> var result = add(bi("123456789987654321"),bi("111111111111111111"))
result ==> 234567901098765432

我的JShell之旅

我得承认,当我第一次听说JShell时,我没怎么考虑它。我一直在使用其他语言的REPL,更多的是把它看作一种“玩具”而不是工具。不过,我用的越多,我就越认识到它的好处以及如何为我所用。

对我而言,我发现JShell最大的用处是学习语言新特性、加深对现有特性的理解、调式代码、试用新类库。在我的程序开发职业生涯中,我学会了一件事,就是我应该尽力缩短反馈循环,越短越好。我就是这样最大限度地工作和学习的。我发现,JShell非常适合缩短反馈循环。它看上去可能没什么大不了的,但是,所有这些小事情(如在IDE中编译或运行单元测试)会随着时间推移慢慢积累。

我希望你会发现JShell的好处,和我一样愉快地使用它!

非常乐于听到你关于JShell的评论、想法或经验。请务必和我分享!

关于作者

Dustin Schultz 是Pluralsight的一名编辑、首席软件工程师。他骨子里是一名技术布道者。他热衷于软件工程,有超过15年的企业及初创公司企业级软件开发经验。他拥有计算机科学学士和硕士学位,热爱学习。要想了解更多信息,可以阅读他的博客

Comments are closed.