1. 程式人生 > >JShell:Java REPL綜合指南

JShell:Java REPL綜合指南

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