JShell:Java REPL综合指南

栏目: Java · 发布时间: 6年前

内容简介:Java Shell或JShell是官方提供的读取-求值-打印-循环,通常称为REPL,是在Java 9中引入的。它提供了一个交互式shell,用于快速原型、调试、学习Java及Java API,所有这些都不需要public static void main方法,也不需要在执行之前编译代码。此外,随着Java 10引入了var关键词,JShell简单了许多(而且更实用了)。注意:在这份指南中,为了使用关键词var,我们将使用Java 10,因此,为了跟着这份指南操作,你务必要确保至少已经安装了Java 1

本文要点

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

查看英文原文: JShell: A Comprehensive Guide to the Java REPL


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

无界

无界

(美)艾米莉·内格尔·格林(Emily Nagle Green) / 卞斌 / 机械工业出版社 / 2011-5 / 39.00元

"数十亿人身在其中、数十万亿美元的新生意,你我此生最大的科技革命,这次转型将如何改变我们的生活? 又如何使我们做生意的方式起革命性的变化? 无界会比你所想更快降临,将创造数兆美元的新价值。你的行动够快吗?这本放眼未来的著作,结合专家的洞见、战术性工具,以及扬基集团独有的无界趋势数据,提供你需要的一切。" 未来的世界和企业,会走向无界的状态,也就是人、构想和产品经由一张全球性的数字......一起来看看 《无界》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具