内容简介: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年的企业及初创公司企业级软件开发经验。他拥有计算机科学学士和硕士学位,热爱学习。要想了解更多信息,可以阅读 他的博客 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Markdown 在线编辑器
Markdown 在线编辑器
HEX HSV 转换工具
HEX HSV 互换工具