本文要点
- 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 年的企业及初创公司企业级软件开发经验。他拥有计算机科学学士和硕士学位,热爱学习。要想了解更多信息,可以阅读他的博客。
评论