Java 9 揭秘(11. Java Shell)


Tips
做一個終身學習的人。

Java 9

在本章節中,主要介紹以下內容:

  • 什么是Java shell
  • JShell工具和JShell API是什么
  • 如何配置JShell工具
  • 如何使用JShell工具對Java代碼片段求值
  • 如何使用JShell API對Java代碼片斷求值

一. 什么是Java shell

Java Shell在JDK 9中稱為JShell,是一種提供交互式訪問Java編程語言的命令行工具。 它允許對Java代碼片段求值,而不是強制編寫整個Java程序。 它是Java的REPL(Read-Eval-Print loop)。 JShell也是一個API,可用於開發應用程序以提供與JShell命令行工具相同的功能。

REPL(Read-Eval-Print loop)是一種命令行工具(也稱為交互式編程語言環境),可讓用戶快速求出代碼片段的值,而無需編寫完整的程序。 REPL來自Lisp語言的循環語句中read,eval和print中使用的三個原始函數。 read功能讀取用戶輸入並解析成數據結構;eval函數評估已解析的用戶輸入以產生結果;print功能打印結果。 打印結果以后,該工具已准備好再次接受用戶輸入,從而Read-Eval-Print 循環。 術語REPL用於交互式工具,可與編程語言交互。 圖下顯示了REPL的概念圖。 UNIX shell或Windows命令提示符的作用類似於讀取操作系統命令的REPL,執行它,打印輸出,並等待讀取另一個命令。

REPL概念圖

為什么JDK 9中引入了JShell? 將其包含在JDK 9中的主要原因之一是來自學術界的反饋,其學習曲線陡峭。 再是其他編程語言(如Lisp,Python,Ruby,Groovy和Clojure)一直支持REPL已經很長一段時間。 只要在Java中編寫一個“Hello,world!”程序,你就必須使用一個編輯 - 編譯 - 執行循環(Edit-Compile-Execute loop)來編寫一個完整的程序,編譯它並執行它。 如果需要進行更改,則必須重復以下步驟。 除了定義目錄結構,編譯和執行程序等其他內務工作外,以下是使用JDK 9中的模塊化Java程序打印“Hello,world!”消息的最低要求:

// module-info.java
module HelloWorld {
}
// HelloWorld.java
package com.jdojo.intro;
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

此程序執行時,會在控制台上打印一條消息:“Hello,world!”。 編寫一個完整的程序來對一個簡單表達式求值,如這樣就是過分的。 這是學術界不會將Java作為初始編程語言教授給學生的主要原因。 Java設計人員聽取了教學團體的反饋意見,並在JDK 9中介紹了JShell工具。要實現與此程序相同的操作,只需在jshell命令提示符下只寫一行代碼:

jshell> System.out.println("Hello, world!")
Hello, world!
jshell>

第一行是在jshell命令提示符下輸入的代碼; 第二行是輸出。 打印輸出后,jshell提示符返回,可以輸入另一個表達式進行求值。

Tips
JShell不是一種新的語言或編譯器。 它是一種交互式訪問Java編程語言的工具和API。 對於初學者,它提供了一種快速探索Java編程語言的方法。 對於有經驗的開發人員,它提供了一種快速的方式來查看代碼段的結果,而無需編譯和運行整個程序。 它還提供了一種使用增量方法快速開發原型的方法。 添加一段代碼,獲取即時反饋,並添加另一個代碼片段代碼,直到原型完成。

JDK 9附帶了一個JShell命令行工具和JShell API。 該工具支持的所有功能API也同樣支持。 也就是說,可以使用工具運行代碼片段或使用API以編程方式運行代碼段。

二. JShell架構

Java編譯器不能自己識別代碼段,例如方法聲明或變量聲明。 只有類和import語句可以是頂層結構,它們可以自己存在。 其他類型的片段必須是類的一部分。 JShell允許執行Java代碼片段,並進行改進。

目前JShell架構的指導原則是使用JDK中現有的Java語言支持和其他Java技術來保持與當前和將來版本的語言兼容。 隨着Java語言隨着時間的推移而變化,對JShell的支持也將受到JShell實現而修改。 圖下顯示了JShell的高層次體系結構。

JShell 架構

JShell工具使用版本2的JLine,它是一個用於處理控制台輸入的Java庫。 標准的JDK編譯器不知道如何解析和編譯Java代碼片斷。 因此,JShell實現具有自己的解析器,解析片段並確定片段的類型,例如方法聲明,變量聲明等。一旦確定了片段類型,包裝在合成類的代碼片段遵循以下規則:

  • 導入語句作為“as-is”使用。 也就是說,所有導入語句都按原樣放置在合成類的頂部。
  • 變量,方法和類聲明成為合成類的靜態成員。
  • 表達式和語句包含在合成類中的合成方法中。

所有合成類都屬於REPL的包。 一旦片段被包裝,包裝的源代碼由標准Java編譯器使用Compiler API進行分析和編譯。 編譯器將包裝的源代碼以字符串格式作為輸入,並將其編譯為字節碼,該字節碼存儲在內存中。 生成的字節碼通過套接字發送到運行JVM的遠程進程,用於加載和執行。 有時,加載到遠程JVM中的現有代碼片段需要由JShell工具替代,該工具使用Java Debugger API來實現。

三. JShell 工具

JDK 9帶一個位於JDK_HOME\bin目錄中的JShell工具。 該工具名為jshell。 如果在Windows上的C:\java9目錄中安裝了JDK 9,那么將有一個C:\java9\bin\jshell.exe的可執行文件,它是JShell工具。 要啟動JShell工具,需要打開命令提示符並輸入jshell命令:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell>

在命令提示符下輸入jshell命令可能會報出一個錯誤:

C:\Java9Revealed>jshell
'jshell' is not recognized as an internal or external command,
operable program or batch file.
C:\Java9Revealed>

此錯誤表示JDK_HOME\bin目錄未包含在計算機上的PATH環境變量中。 在C:\java9目錄中安裝了JDK 9,所以JDK_HOME是C:\java9。 要解決此錯誤,可以在PATH環境變量中包含C:\java9\bin目錄,或者使用jshell命令的完整路徑:C:\java9\bin\jshell。 以下命令序列顯示如何在Windows上設置PATH環境變量並運行JShell工具:

C:\Java9Revealed>SET PATH=C:\java9\bin;%PATH%
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell>

當jshell成功啟動時,它會打印一個歡迎消息及其版本信息。 它還有一個打印命令,這是/ help intro。 可以使用此命令打印工具本身的簡短介紹:

jshell> /help intro
|
|  intro
|
|  The jshell tool allows you to execute Java code, getting immediate results.
|  You can enter a Java definition (variable, method, class, etc), like:  int x = 8
|  or a Java expression, like:  x + x
|  or a Java statement or import.
|  These little chunks of Java code are called 'snippets'.
|
|  There are also jshell commands that allow you to understand and
|  control what you are doing, like:  /list
|
|  For a list of commands: /help
jshell>

如果需要關於該工具的幫助,可以在jshell上輸入命令/ help,以簡短描述打印一份命令列表:

jshell> /help
<<The output is not shown here.>>
jshell>

可以使用幾個命令行選項與jshell命令將值傳遞到工具本身。例如,可以將值傳遞給用於解析和編譯代碼段的編譯器,以及用於執行/求值代碼段的遠程JVM。使用--help選項運行jshell程序,以查看所有可用的標准選項的列表。使用--help-extra-X選項運行它以查看所有可用的非標准選項的列表。例如,使用這些選項,可以為JShell工具設置類路徑和模塊路徑。

還可以使用命令行--start選項自定義jshell工具的啟動腳本。可以使用DEFAULTPRINTING作為此選項的參數。 DEFAULT參數使用多個import語句啟動jshell,因此在使用jshell時不需要導入常用的類。以下兩個命令以相同的方式啟動jshell:如果需要對該工具的幫助,可以在jshell上輸入命令/help,以簡單描述打印命令列表:

  • jshell
  • jshell --start DEFAULT

可以使用System.out.println()方法將消息打印到標准輸出。 可以使用帶有PRINTING參數的--start選項啟動jshell,該參數將包括System.out.print(),System.out.println()和System.out.printf()方法的所有版本作為print()println()printf()的上層方法。 這將允許在jshell上使用print()println()printf()方法,而不是使用更長版本的System.out.print()System.out.println()System.out.printf()

C:\Java9Revealed>jshell --start PRINTING
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> println("hello")
hello
jshell>

當啟動jshell以包括默認的導入語句和打印方法時,可以重復--start選項:

C:\Java9Revealed>jshell --start DEFAULT --start PRINTING
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell>

四. 退出JShell工具

要退出jshell,請在jshell提示符下輸入/exit,然后按Enter鍵。 該命令打印一個再見消息,退出該工具,並返回到命令提示符:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /exit
|  Goodbye
C:\Java9Revealed>

五. 什么是片段和命令?

你可以使用JShell工具:

  • 對Java代碼片段求值,這在JShell術語中簡稱為片段。
  • 執行命令,用於查詢JShell狀態並設置JShell環境。

要區分命令和片段,所有命令都以斜杠(/)開頭。 您已經在之前的部分中看到過其中的一些,如/exit/help。 命令用於與工具本身進行交互,例如定制其輸出,打印幫助,退出工具以及打印命令和代碼段的歷史記錄。了解有關可用命令的全部信息,請使用/help命令。

使用JShell工具,一次編寫一個Java代碼片段並對其進行求值。 這些代碼段被稱為片段。 片段必須遵循Java語言規范中指定的語法。 片段可以是:

  • 導入聲明
  • 類聲明
  • 接口聲明
  • 方法聲明
  • 字段聲明
  • 語句
  • 表達式

Tips
可以使用JShell中的所有Java語言結構,但包聲明除外。 JShell中的所有片段都出現在名為REPL的內部包中,並在內部合成類中。

JShell工具知道何時完成輸入代碼段。 當按Enter鍵時,該工具將執行該片段,如果它完成或帶你到下一行,並等待完成該片段。 如果一行以...開頭,則表示代碼段不完整,需要輸入更多文本才能完成代碼段。 可以自定義更多輸入的默認提示,即...>。 以下是幾個例子:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> 2 + 2
$1 ==> 4
jshell> 2 +
   ...> 2
$2 ==> 4
jshell> 2
$3 ==> 2
jshell>

當輸入2 + 2並按Enter鍵時,jshell將其視為完整的片段(表達式)。 它對表達式求值並將反饋打印到4,並將結果分配給名為$1的變量。 名為$1的變量由JShell工具自動生成。 當輸入2 +並按Enter鍵時,jshell會提示輸入更多內容,因為2 +不是Java中的完整代碼段。 當在第二行輸入2時,代碼段已完成; jshell對片段求值並打印反饋。 當輸入2並按Enter鍵時,jshell對代碼片段求值,因為2本身是一個完整的表達式。

六. 表達式求值

可以在jshell中執行任何有效的Java表達式。 比如以下示例:

jshell> 2 + 2
$1 ==> 4
jshell> 9.0 * 6
$2 ==> 54.0

在第一個表達式中 計算結果4被分配給臨時變量$1,第二個表達式的結果分配給了$2, 你可以直接使用這些變量:

jshell> $1
$1 ==> 4
jshell> $2
$2 ==> 54.0
jshell> System.out.println($1)
4
jshell> System.out.println($2)
54.0

Tips
在jshell中,不需要使用分號來終止語句。 工具將為你插入缺少的分號。

在Java中,每個變量都有一個數據類型。 在這些示例中,$1$2的變量的數據類型是什么? 在Java中,2 + 2計算結果為int9.0 * 6求值為double類型。 因此,$1$2變量的數據類型應分別為intdouble。 你如何驗證這個? 可以將$1$2轉換成Object對象,並調用它們的getClass()方法,結果應為IntegerDouble對象。 當把它們轉換為Object時,基本類型intdouble類型進行自動裝箱:

jshell> 2 + 2
$1 ==> 4
jshell> 9.0 * 6
$2 ==> 54.0
jshell> ((Object)$1).getClass()
$3 ==> class java.lang.Integer
jshell> ((Object)$2).getClass()
$4 ==> class java.lang.Double
jshell>

有一個更簡單的方法來確定由jshell創建的變量的數據類型 ——只需要告訴jshell給你詳細的反饋,它將打印它創建的變量的數據類型的更多信息! 以下命令將反饋模式設置為詳細並對相同的表達式求值:

jshell> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$1 ==> 4
|  created scratch variable $1 : int
jshell> 9.0 * 6
$2 ==> 54.0
|  created scratch variable $2 : double
jshell>

jshell分別為$1$2的變量的數據類型打印為intdouble。 初學者使用-retain選項執行以下命令獲得更多幫助,因此詳細的反饋模式將在jshell會話中持續存在:

jshell> /set feedback -retain verbose

還可以使用/vars命令列出在jshell中定義的所有變量:

jshell> /vars
|    int $1 = 4
|    double $2 = 54.0
jshell>

如果要再次使用正常的反饋模式,請使用以下命令:

jshell> /set feedback -retain normal
|  Feedback mode: normal
Jshell>

不限於評估簡單的表達式,例如2 + 2。可以對任何Java表達式求值。 以下示例字符串連接表達式並使用String類的方法。 它還顯示了如何使用for循環:

jshell> "Hello " + "world! " + 2016
$1 ==> "Hello world! 2016"
jshell> $1.length()
$2 ==> 17
jshell> $1.toUpperCase()
$3 ==> "HELLO WORLD! 2016"
jshell> $1.split(" ")
$4 ==> String[3] { "Hello", "world!", "2016" }
jshell> for(String s : $4) {
   ...> System.out.println(s);
   ...> }
Hello
world!
2016
jshell>

七. 列表片段

無論在jshell中輸入的內容最終都是片段的一部分。 每個代碼段都會分配一個唯一的代碼段ID,可以稍后引用該代碼段,例如刪除該代碼段。 /list命令列出所有片段。 它有以下形式:

/list
/list -all
/list -start
/list <snippet-name>
/list <snippet-id>

沒有參數或選項的/list命令打印所有用戶輸入的有效代碼片段,這些片段也可能是使用/open命令從文件中打開的。

使用-all選項列出所有片段 —— 有效的,無效的,錯誤的和啟動時的。

使用-start選項僅列出啟動時代碼片段。 啟動片段被緩存,並且-start選項打印緩存的片段。 即使在當前會話中刪除它們,它也會打印啟動片段。

一些片段類型有一個名稱(例如,變量,方法聲明),所有片段都有一個ID。 /list命令使用代碼片段的名稱或ID將打印由該名稱或ID標識的片段。

/list命令以以下格式打印片段列表:

<snippet-id> : <snippet-source-code>
<snippet-id> : <snippet-source-code>
<snippet-id> : <snippet-source-code>
...

JShell工具生成唯一的代碼段ID。 它們是s1,s2,s3 ...,用於啟動片段,1,2,3 ...等都是有效的片段,e1,e2,e3 ...用於錯誤的片段。 以下jshell會話將顯示如何使用/list命令列出片段。 這些示例演示了/drop命令來使用代碼段名稱和代碼段ID來刪除片段。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /list
jshell> 2 + 2
$1 ==> 4
jshell> /list
   1 : 2 + 2
jshell> int x = 100
x ==> 100
jshell> /list
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -start
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
jshell> string str = "using invalid type string"
|  Error:
|  cannot find symbol
|    symbol:   class string
|  string str = "using invalid type string";
|  ^----^
jshell> /list
   1 : 2 + 2
   2 : int x = 100;
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
  e1 : string str = "using invalid type string";
jshell> /drop 1
|  dropped variable $1
jshell> /list
   2 : int x = 100;
jshell> /drop x
|  dropped variable x
jshell> /list
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : 2 + 2
   2 : int x = 100;
  e1 : string str = "using invalid type string";
jshell>

變量,方法和類的名稱成為代碼段名稱。 請注意,Java允許使用與變量,方法和具有相同名稱的類,因為它們出現在其自己的命名空間中。 可以使用這些實體的名稱通過/list命令列出它們:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /list x
|  No such snippet: x
jshell> int x = 100
x ==> 100
jshell> /list x
   1 : int x = 100;
jshell> void x(){}
|  created method x()
jshell> /list x
   1 : int x = 100;
   2 : void x(){}
jshell> void x(int n) {}
|  created method x(int)
jshell> /list x
   1 : int x = 100;
   2 : void x(){}
   3 : void x(int n) {}
jshell> class x{}
|  created class x
jshell> /list x
   1 : int x = 100;
   2 : void x(){}
   3 : void x(int n) {}
   4 : class x{}
jshell>

八. 編輯代碼片段

JShell工具提供了幾種編輯片段和命令的方法。 可以使用下表中列出的導航鍵在命令行中導航,同時在jshell中輸入代碼段和命令。

鍵盤按鍵 描述
Enter 進入當前行
左箭頭 向后移動一個字符
右箭頭 向前移動一個字符
Ctrl-A 移動到行首
Ctrl-E 移動到行末
Meta-B (or Alt-B) 向后移動一個單詞
Meta-F (or Alt-F) 向前移動一個單詞

可以使用下表列出的鍵來編輯在jshell中的一行輸入的文本。

鍵盤按鍵 描述
Delete 刪除光標后的字符
Backspace 刪除光標前的字符
Ctrl-K 刪除從光標位置到行末的文本
Meta-D (or Alt-D) 刪除光標位置后面的單詞
Ctrl-W 刪除光標位置到前面最近的空格之間的文本
Ctrl-Y 將最近刪除的文本粘貼到行中
Meta-Y (or Alt-Y) 在Ctrl-Y之后,此組合鍵將循環選擇先前刪除的文本

即使可以訪問豐富的編輯鍵的組合,也很難在JShell工具中編輯多行片段。 工具設計人員意識到了這個問題,並提供了一個內置的代碼段編輯器。 可以將JShell工具配置為使用選擇的特定於平台的代碼段編輯器。

需要使用/edit命令開始編輯代碼段。 該命令有三種形式:

/edit <snippet-name>
/edit <snippet-id>
/edit

可以使用片段名稱或代碼段ID來編輯特定的片段。 沒有參數的/edit命令打開編輯器中的所有有效代碼片段進行編輯。 默認情況下,/edit命令打開一個名為JShell Edit Pad內置編輯器,如圖所示。

JShell Edit Pad

JShell Edit Pad是用Swing編寫的,它顯示了一個帶有JTextArea和三個JButton的JFrame控件。 如果編輯片段,請確保在退出窗口之前單擊接受按鈕,以使編輯生效。 如果在不接受更改的情況下取消或退出編輯器,編輯的內容將會丟失。

如果知道變量,方法或類的名稱,則可以使用其名稱進行編輯。 以下jshell會話創建一個變量,方法和具有相同名稱x的類,並使用/edit x命令一次編輯它們:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> int x = 100
x ==> 100
jshell> void x(){}
|  created method x()
jshell> void x (int n) {}
|  created method x(int)
jshell> class x{}
|  created class x
jshell> 2 + 2
$5 ==> 4
jshell> /edit x

/edit x命令在JShell Edit Pad中打開名稱為x的所有片段,如下圖所示。 可以編輯這些片段,接受更改並退出編輯,以繼續執行jshell會話。

根據名字編輯代碼片段

九. 重新運行上一個片段

在像jshell這樣的命令行工具中,通常需要重新運行以前的代碼段。 可以使用向上/向下箭頭來瀏覽片段/命令歷史記錄,然后在上一個代碼段/命令時按Enter鍵。 還可以使用三個命令之一來重新運行以前的代碼段(而不是命令):

/!
/<snippet-id>
/-<n>

/! 命令重新運行最后一個代碼段。 /<snippet-id>命令重新運行由<snippet-id>標識的片段。 / -<n>命令重新運行第n個最后一個代碼段。 例如,/ -1重新運行最后一個代碼段, /-2重新運行第二個代碼段,依此類推。 /!/- 1命令具有相同的效果,它們都重新運行最后一個代碼段。

十. 聲明變量

可以像在Java程序中一樣在jshell中聲明變量。 一個變量聲明可能發生在頂層,方法內部,或者類中的字段聲明。 頂級變量聲明中不允許使用static和final修飾符。 如果使用它們,它們將被忽略並發出警告。 static修飾符指定一個類上下文,final修飾符限制更改變量的值。 不能使用這些修飾符,因為該工具允許通過隨時間更改其值來聲明你想要嘗試的獨立變量。 以下示例說明如何聲明變量:

c:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> int x
x ==> 0
jshell> int y = 90
y ==> 90
jshell> side = 90
|  Error:
|  cannot find symbol
|    symbol:   variable side
|  side = 90
|  ^--^
jshell> static double radius = 2.67
|  Warning:
|  Modifier 'static'  not permitted in top-level declarations, ignored
|  static double radius = 2.67;
|  ^----^
radius ==> 2.67
jshell> String str = new String("Hello")
str ==> "Hello"
jshell>

在頂級表達式中使用未聲明的變量會生成錯誤。 請注意在上一個示例中使用未聲明的變量side,這會產生錯誤。 稍后會介紹,可以在方法體中使用未聲明的變量。

也可以更改變量的數據類型。 可以將一個名為x的變量聲明為int,然后再將其聲明為doubleString。 以下示例顯示了此功能:

jshell> int x = 10;
x ==> 10
jshell> int y = x + 2;
y ==> 12
jshell> double x = 2.71
x ==> 2.71
jshell> y
y ==> 12
jshell> String x = "Hello"
x ==> "Hello"
jshell> y
y ==> 12
jshell>

還可以使用/drop命令刪除變量,該命令將變量名稱作為參數。 以下命令將刪除名為x的變量:

jshell> /drop x

可以使用/vars命令在jshell中列出所有變量。 它將列出用戶聲明的變量和由jshell自動聲明的變量。該命令具有以下形式:

/vars
/vars <variable-name>
/vars <variable-snippet-id>
/vars -start
/vars -all

沒有參數的命令列出當前會話中的所有有效變量。 如果使用代碼段名稱或ID,則會使用該代碼段名稱或ID來列出變量聲明。 如果使用-start選項,它將列出添加到啟動腳本中的所有變量。 如果使用-all選項,它將列出所有變量,包括失敗,覆蓋,刪除和啟動。 以下示例說明如何使用/vars命令:

c:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /vars
jshell> 2 + 2
$1 ==> 4
jshell> /vars
|    int $1 = 4
jshell> int x = 20;
x ==> 20
jshell> /vars
|    int $1 = 4
|    int x = 20
jshell> String str = "Hello";
str ==> "Hello"
jshell> /vars
|    int $1 = 4
|    int x = 20
|    String str = "Hello"
jshell> double x = 90.99;
x ==> 90.99
jshell> /vars
|    int $1 = 4
|    String str = "Hello"
|    double x = 90.99
jshell> /drop x
|  dropped variable x
jshell> /vars
|    int $1 = 4
|    String str = "Hello"
jshell>

十一. import語句

可以在jshell中使用import語句。在Java程序中,默認情況下會導入java.lang包中的所有類型。 要使用其他包中的類型,需要在編譯單元中添加適當的import語句。 我們將從一個例子開始。 創建三個對象:一個String,一個List<Integer>和一個ZonedDateTime。 請注意,String類在java.lang包中; ListInteger類分別在java.util和java.lang包中; ZonedDateTime類在java.time包中。

jshell> String str = new String("Hello")
str ==> "Hello"
jshell> List<Integer> nums = List.of(1, 2, 3, 4, 5)
nums ==> [1, 2, 3, 4, 5]
jshell> ZonedDateTime now = ZonedDateTime.now()
|  Error:
|  cannot find symbol
|    symbol:   class ZonedDateTime
|  ZonedDateTime now = ZonedDateTime.now();
|  ^-----------^
|  Error:
|  cannot find symbol
|    symbol:   variable ZonedDateTime
|  ZonedDateTime now = ZonedDateTime.now();
|                      ^-----------^
jshell>

如果嘗試使用java.time包中的ZonedDateTime類,這些示例會生成錯誤。 當我們嘗試創建一個List時,也期待着類似的錯誤,因為它在java.util包中,默認情況下它不會在Java程序中導入。

JShell工具的唯一目的是在對代碼片段求值時使開發人員的生活更輕松。 為了實現這一目標,該工具默認從幾個包導入所有類型。 那些導入類型的默認包是什么? 可以使用/imports命令在jshell中打印所有有效導入的列表:

jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
jshell>

注意從java.util包導入所有類型的默認import語句。 這是可以創建List而不用導入的原因。 也可以將自己的導入添加到jshell。 以下示例說明如何導入ZonedDateTime類並使用它。 當jshell使用時區打印當前日期的值時,將獲得不同的輸出。

jshell> /imports
|    import java.util.*
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.util.concurrent.*
|    import java.util.prefs.*
|    import java.util.regex.*
jshell> import java.time.*
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
|    import java.time.*
jshell> ZonedDateTime now = ZonedDateTime.now()
now ==> 2016-11-11T10:39:10.497234400-06:00[America/Chicago]
jshell>

注意,當退出會話時,添加到jshell會話的任何導入都將丟失。 還可以刪除import語句 ——包括導入和添加的。 需要知道代碼段ID才能刪除代碼段。 啟動片段的ID為s1,s2,s3等,對於用戶定義的片段,它們為1,2,3等等。以下示例說明如何在jshell中添加和刪除import語句:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> import java.time.*
jshell> List<Integer> list = List.of(1, 2, 3, 4, 5)
list ==> [1, 2, 3, 4, 5]
jshell> ZonedDateTime now = ZonedDateTime.now()
now ==> 2017-02-19T21:08:08.802099-06:00[America/Chicago]
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
jshell> /drop s5
jshell> /drop 1
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
jshell> List<Integer> list2 = List.of(1, 2, 3, 4, 5)
|  Error:
|  cannot find symbol
|    symbol:   class List
|  List<Integer> list2 = List.of(1, 2, 3, 4, 5);
|  ^--^
|  Error:
|  cannot find symbol
|    symbol:   variable List
|  List<Integer> list2 = List.of(1, 2, 3, 4, 5);
|                        ^--^
jshell> import java.util.*
|    update replaced variable list, reset to null
jshell> List<Integer> list2 = List.of(1, 2, 3, 4, 5)
list2 ==> [1, 2, 3, 4, 5]
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : import java.time.*;
   2 : List<Integer> list = List.of(1, 2, 3, 4, 5);
   3 : ZonedDateTime now = ZonedDateTime.now();
  e1 : List<Integer> list2 = List.of(1, 2, 3, 4, 5);
   4 : import java.util.*;
   5 : List<Integer> list2 = List.of(1, 2, 3, 4, 5);
jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*
|    import java.util.*
jshell>

十二. 方法聲明

可以在jshell中聲明和調用方法。 可以聲明頂級方法,這些方法直接在jshell中輸入,不在任何類中。 也可以在類中聲明方法。 在本節中,展示如何聲明和調用頂級方法。 也可以調用現有類的方法。 以下示例聲明一個名為square()的方法並調用它:

jshell> long square(int n) {
   ...>    return n * n;
   ...> }
|  created method square(int)
jshell> square(10)
$2 ==> 100
jshell> long n2 = square(37)
n2 ==> 1369
jshell>

在方法體中允許向前引用。 也就是說,可以在方法體中引用尚未聲明的方法或變量。 在定義所有缺少的引用方法和變量之前,無法調用聲明的方法。

jshell> long multiply(int n) {
   ...>     return multiplier * n;
   ...> }
|  created method multiply(int), however, it cannot be invoked until variable multiplier is declared
jshell> multiply(10)
|  attempted to call method multiply(int) which cannot be invoked until variable multiplier is declared
jshell> int multiplier = 2
multiplier ==> 2
jshell> multiply(10)
$6 ==> 20
jshell> void printCube(int n) {
   ...>     System.out.printf("Cube of %d is %d.%n", n, cube(n));
   ...> }
|  created method printCube(int), however, it cannot be invoked until method cube(int) is declared
jshell> long cube(int n) {
   ...>     return n * n * n;
   ...> }
|  created method cube(int)
jshell> printCube(10)
Cube of 10 is 1000.
jshell>

此示例聲明一個名為multiply(int n)的方法。 它將參數與名為multiplier的變量相乘,該變量尚未聲明。 注意在聲明此方法后打印的反饋。 反饋清楚地表明,在聲明乘數變量之前,不能調用multiply()方法。 調用該方法會生成錯誤。 后來,multiplier 變量被聲明,並且multiply()方法被成功調用。

Tips
可以使用向前引用的方式聲明遞歸方法。

十三. 類型聲明

可以像在Java中一樣在jshell中聲明所有類型,如類,接口,枚舉和注解。 以下jshell會話創建一個Counter類,創建對象並調用方法:

jshell> class Counter {
   ...>     private int counter;
   ...>     public synchronized int next() {
   ...>         return ++counter;
   ...>     }
   ...>
   ...>     public int current() {
   ...>         return counter;
   ...>     }
   ...> }
|  created class Counter
jshell> Counter c = new Counter();
c ==> Counter@25bbe1b6
jshell> c.current()
$3 ==> 0
jshell> c.next()
$4 ==> 1
jshell> c.next()
$5 ==> 2
jshell> c.current()
$6 ==> 2
jshell>

可以使用/types命令在jshell中打印所有聲明類型的列表。 該命令具有以下形式:

/types
/types <type-name>
/types <snippet-id>
/types -start
/types -all

注意,Counter類的源代碼不包含包聲明,因為jshell不允許在包中聲明類(或任何類型)。 在jshell中聲明的所有類型都被視為內部合成類的靜態類型。 但是,可能想要測試自己的包中的類。 可以在jshell中使用一個包中已經編譯的類。 當使用類庫開發應用程序時,通常需要它,並且想通過針對類庫編寫代碼段來實驗應用程序邏輯。 需要使用/env命令設置類路徑,因此可能會找到需要的類。

com.jdojo.jshell包中的Person類聲明如下所示。

// Person.java
package com.jdojo.jshell;
public class Person {
    private String name;
    public Person() {
        this.name = "Unknown";
    }
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

以下jshell命令將Windows上的類路徑設置為C:\中。

jshell> /env -class-path C:\Java9Revealed\com.jdojo.jshell\build\classes
|  Setting new options and restoring state.
jshell> Person guy = new Person("Martin Guy Crawford")
|  Error:
|  cannot find symbol
|    symbol:   class Person
|  Person guy = new Person("Martin Guy Crawford");
|  ^----^
|  Error:
|  cannot find symbol
|    symbol:   class Person
|  Person guy = new Person("Martin Guy Crawford");
|                   ^----^

我們使用類的簡單類名稱Person,而不導入它,而jshell無法找到該類。 我們需要導入Person類或使用其全限定名。 以下是此jshell會話的延續,可以修復此錯誤:

jshell> import com.jdojo.jshell.Person
jshell> Person guy = new Person("Martin Guy Crawford")
guy ==> com.jdojo.jshell.Person@192b07fd
jshell> guy.getName()
$9 ==> "Martin Guy Crawford"
jshell> guy.setName("Forrest Butts")
jshell> guy.getName()
$11 ==> "Forrest Butts"
jshell>

十四. 設置執行環境

在上一節中,學習了如何使用/env命令設置類路徑。 該命令可用於設置執行上下文的許多其他組件,如模塊路徑。 還可以使用來解析模塊,因此可以使用jshell模塊中的類型。 其完整語法如下:

/env [-class-path <path>] [-module-path <path>] [-add-modules <modules>] [-add-exports <m/p=n>]

沒有參數的/env命令打印當前執行上下文的值。 -class-path選項設置類路徑。 -module-path選項設置模塊路徑。 -add-modules選項將模塊添加到默認的根模塊中,因此可以解析。 可以使用 -add-modules選項來使用特殊值ALL-DEFAULTALL-SYSTEMALL-MODULE-PATH來解析模塊。-add-exports選項將未導出的包從模塊導出到一組模塊。 這些選項與使用javac和java命令時具有相同的含義。

Tips
在命令行中,這些選項必須以兩個“--”開頭,例如--module-path。 在jshell中,可以是一個破折號或者兩個破折號開始。 例如,在jshell中允許使用--module-path-module-path。

當設置執行上下文時,當前會話將被重置,並且當前會話中的所有先前執行的代碼片段將以安靜模式回放。 也就是說,未顯示回放的片段。 但是,回放時的錯誤將會顯示出來。

可以使用/env/reset/reload命令設置執行上下文。 每個命令都有不同的效果。 上下文選項(如-class-path-module-path)的含義是相同的。 可以使用命令-/ help上下文列出可用於設置執行上下文的所有選項。

來看一下使用/env命令使用模塊相關設置的例子。 在第3章中創建了一個com.jdojo.intro模塊。該模塊包含com.jdojo.intro的包,但它不導出包。 現在,要調用非導出包中的Welcome類的main(String [] args)方法。 以下是需要在jshell中執行的步驟:

  • 設置模塊路徑,因此可以找到模塊。
  • 通過將模塊添加到默認的根模塊中來解決該模塊。 可以使用/env命令中的-add-modules選項來執行此操作。
  • 使用-add-exports命令導出包。 在jshell中輸入的片段在未命名的模塊中執行,因此需要使用ALL-UNNAMED關鍵字將包導出到所有未命名的模塊。 如果在-add-exports選項中未提供目標模塊,則假定為ALL-UNNAMED,並將軟件包導出到所有未命名的模塊。
    *(可選)如果要在代碼段中使用其簡單名稱,請導入com.jdojo.intro.Welcome類。
  • 現在,可以從jshell調用Welcome.main()方法。

以下jshell會話將顯示如何執行這些步驟。 假設以C:\ Java9Revealed作為當前目錄啟動jshell會話,C:\Java9Revealed\com.jdojo.intro\build \ classes目錄包含com.jdojo.intro模塊的編譯代碼。 如果你的目錄結構和當前目錄不同,請將會話中使用的目錄路徑替換為你的目錄路徑。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /env -module-path com.jdojo.intro\build\classes
|  Setting new options and restoring state.
jshell> /env -add-modules com.jdojo.intro
|  Setting new options and restoring state.
jshell> /env -add-exports com.jdojo.intro/com.jdojo.intro=ALL-UNNAMED
|  Setting new options and restoring state.
jshell> import com.jdojo.intro.Welcome
jshell> Welcome.main(null)
Welcome to the Module System.
Module Name: com.jdojo.intro
jshell> /env
|     --module-path com.jdojo.intro\build\classes
|     --add-modules com.jdojo.intro
|     --add-exports com.jdojo.intro/com.jdojo.intro=ALL-UNNAMED
jshell>

十五. 沒有檢查異常

在Java程序中,如果調用拋出檢查異常的方法,則必須使用try-catch塊或通過添加throws子句來處理這些異常。 JShell工具應該是一種快速簡單的方法來評估片段,因此不需要處理jshell片段中檢查的異常。 如果代碼段在執行時拋出一個被檢查的異常,jshell將打印堆棧跟蹤並繼續。

jshell> FileReader fr = new FileReader("secrets.txt")
|  java.io.FileNotFoundException thrown: secrets.txt (The system cannot find the file specified)
|        at FileInputStream.open0 (Native Method)
|        at FileInputStream.open (FileInputStream.java:196)
|        at FileInputStream.<init> (FileInputStream.java:139)
|        at FileInputStream.<init> (FileInputStream.java:94)
|        at FileReader.<init> (FileReader.java:58)
|        at (#1:1)
jshell>

此片段拋出FileNotFoundException,因為當前目錄中不存在名為secrets.txt的文件。 如果文件存在,可以創建一個FileReader,而無需使用try-catch塊。 請注意,如果嘗試在方法中使用此片段,則適用正常的Java語法規則,並且此方法聲明不會編譯:

jshell> void readSecrets() {
   ...> FileReader fr = new FileReader("secrets.txt");
   ...> // More code goes here
   ...> }
|  Error:
|  unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
|  FileReader fr = new FileReader("secrets.txt");
|                  ^---------------------------^
jshell>

十六. 自動補全

JShell工具具有自動補全功能,可以通過輸入部分文本並按Tab鍵進行調用。 當輸入命令或代碼段時,此功能可用。 該工具將檢測上下文並幫助自動完成命令。 當有多種可能性時,它顯示所有可能性,需要手動輸入其中一個。 當它發現一個獨特的可能性,它將完成文本。

Tips
可以在JShell工具上使用/help shortcuts命令查看當前可用的自動補全的鍵。

以下是查找多種可能性的工具的示例。 需要輸入/e並按Tab鍵:

jshell> /e
/edit    /exit
jshell> /e

該工具檢測到嘗試輸入命令,因為文本以斜杠(/)開頭。 有兩個以/ e開頭的命令(/edit/exit),它們打印出來。 現在,需要通過輸入命令的其余部分來完成命令。 在命令的情況下,如果輸入足夠的文本以使命令名稱唯一,然后按Enter,該工具將執行該命令。 在這種情況下,可以輸入/ed/ex,然后按Enter鍵分別執行/edit/exit命令。 您可以輸入斜杠(/),然后按Tab鍵查看命令列表:

jshell> /
/!          /?          /drop       /edit       /env        /exit       /help       /history

以下代碼段創建一個名為strString變量,初始值為“GoodBye”:

jshell> String str = "GoodBye"
str ==> "GoodBye"

繼續這個jshell會話中,輸入“str.”, 並按Tab鍵:

jshell> str.
charAt(                chars()                codePointAt(
codePointBefore(       codePointCount(        codePoints()
compareTo(             compareToIgnoreCase(   concat(
contains(              contentEquals(         endsWith(
equals(                equalsIgnoreCase(      getBytes(
getChars(              getClass()             hashCode()
indexOf(               intern()               isEmpty()
lastIndexOf(           length()               matches(
notify()               notifyAll()            offsetByCodePoints(
regionMatches(         replace(               replaceAll(
replaceFirst(          split(                 startsWith(
subSequence(           substring(             toCharArray()
toLowerCase(           toString()             toUpperCase(
trim()                 wait(

此片段可以在變量str上調用的String類打印所有方法名稱。 請注意,一些方法名以“()”結尾,而其他結尾只有“(”這不是一個錯誤,如果一個方法沒有參數,它的名稱跟隨一個“()”,如果一個方法接受參數,它的名稱將跟隨一個“(”。

繼續這個例子,輸入str.sub並按Tab鍵:

jshell> str.sub
subSequence(   substring(

這一次,該工具在String類中發現了兩個以sub開頭的方法。 可以輸入整個方法調用,str.substring(0,4),然后按Enter鍵來求值代碼段:

jshell> str.substring(0, 4)
$2 ==> "Good"

或者,可以通過輸入str.subs來讓工具自動補全方法名稱。 當輸入str.subs並按Tab時,該工具將完成方法名稱,插入一個“(”,並等待輸入方法的參數:

jshell> str.substring(
substring(
jshell> str.substring(
Now you can enter the method’s argument and press Enter to evaluate the expression:
jshell> str.substring(0, 4)
$3 ==> "Good"
jshell>

當一個方法接受參數時,很可能你想看到這些參數的類型。 可以在輸入整個方法/構造函數名稱和開始圓括號后按Shift + Tab查看該方法的概要。 在上一個例子中,如果輸入str.substring(並按Shift + Tab,該工具將打印substring()方法的概要:

jshell> str.substring(
String String.substring(int beginIndex)
String String.substring(int beginIndex, int endIndex)
<press shift-tab again to see javadoc>

注意輸出。 它說如果再次按Shift + Tab,它將顯示substring()方法的Javadoc。 在下面的提示中,再次按下Shift + Tab打印Javadoc。 如果需要顯示更多的Javadoc,請按空格鍵或鍵入Q以返回到jshell提示符:

jshell> str.substring(
String String.substring(int beginIndex)
Returns a string that is a substring of this string.The substring begins with
the character at the specified index and extends to the end of this string.
Examples:
     "unhappy".substring(2) returns "happy"
     "Harbison".substring(3) returns "bison"
     "emptiness".substring(9) returns "" (an empty string)
Parameters:
beginIndex - the beginning index, inclusive.
Returns:
the specified substring.
String String.substring(int beginIndex, int endIndex)
Returns a string that is a substring of this string.The substring begins at the
specified beginIndex and extends to the character at index endIndex - 1 . Thus
the length of the substring is endIndex-beginIndex .
Examples:
     "hamburger".substring(4, 8) returns "urge"
     "smiles".substring(1, 5) returns "mile"
Parameters:
beginIndex - the beginning index, inclusive.
endIndex - the ending index, exclusive.
Returns:
the specified substring.
jshell> str.substring(

十七. 片段和命令歷史

JShell維護了在所有會話中輸入的所有命令和片段的歷史記錄。 可以使用向上和向下箭頭鍵瀏覽歷史記錄。 也可以使用/history命令打印當前會話中輸入的所有歷史記錄:

jshell> 2 + 2
$1 ==> 4
jshell> System.out.println("Hello")
Hello
jshell> /history
2 + 2
System.out.println("Hello")
/history
jshell>

此時,按向上箭頭顯示/history命令,按兩次顯示System.out.println("Hello"),然后按三次顯示2 + 2。第四次按向上箭頭將顯示最后一個從以前的jshell會話輸入命令/代碼段。 如果要執行以前輸入的代碼段/命令,請使用向上箭頭,直到顯示所需的命令/代碼段,然后按Enter執行。 按向下箭頭將導航到列表中的下一個命令或代碼段。 假設按向上箭頭五次導航到第五個最后一個片斷或命令。 現在按向下箭頭將導航到第四個最后一個代碼段或命令。 當處於第一個和最后一個片段或命令時,按向上箭頭或向下箭頭不起作用。

十八. 讀取JShell堆棧跟蹤

在jshell上輸入的片段是合成類的一部分。 例如,Java不允許聲明頂級方法。 方法聲明必須是類型的一部分。 當Java程序中拋出異常時,堆棧跟蹤將打印類型名稱和行號。 在jshell中,可能會從代碼段中拋出異常。 在這種情況下打印合成類名稱和行號將會產生誤導,對開發者來說是沒有意義的。 堆棧跟蹤中代碼段中代碼位置的格式將為:

at <snippet-name> (#<snippet-id>:<line-number-in-snippet>)

請注意,某些代碼段可能沒有名稱。 例如,輸入一個代碼段2 + 2不會給它一個名字。 一些片段有一個名字,例如一個代碼段,聲明變量被賦予與變量名稱相同的名稱; 方法和類型聲明也一樣。 有時,可能有兩個名稱相同的片段,例如通過聲明變量和具有相同名稱的方法/類型。 jshell為所有片段分配唯一的片段ID。 可以使用/list -all命令查找代碼段的ID。

以下jshell會話聲明了一個divide()方法,並使用運算符ArithmeticException異常打印異常堆棧跟蹤,該異常在整數除以零時拋出:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> int divide(int x, int y) {
   ...> return x/y;
   ...> }
|  created method divide(int,int)
jshell> divide(10, 2)
$2 ==> 5
jshell> divide(10, 0)
|  java.lang.ArithmeticException thrown: / by zero
|        at divide (#1:2)
|        at (#3:1)
jshell> /list -all
  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;
   1 : int divide(int x, int y) {
       return x/y;
       }
   2 : divide(10, 2)
   3 : divide(10, 0)
jshell>

我們嘗試讀取堆棧跟蹤。 (#3:1)的最后一行表示異常是在代碼段3的第1行引起的。注意在/list -all命令的輸出中,代碼段3是表達式的divide(10, 0)導致異常。 第二行,divide (#1:2),表示堆棧跟蹤中的第二級位於代碼段的第2行,名稱為divide代碼段ID是1。

十九. 重用JShell會話(Session)

可以在jshell會話中輸入許多片段和命令,並可能希望在其他會話中重用它們。 可以使用/save命令將命令和片段保存到文件中,並使用/open命令加載先前保存的命令和片段。 /save命令的語法如下:

/save <option> <file-path>

這里,<option>可以是以下選項之一:-al-history-start<file-path>是將保存片段/命令的文件路徑。

/save命令沒有選項將所有活動的片段保存在當前會話中。 請注意,它不保存任何命令或失敗的代碼段。

帶有-all選項的/save命令將當前會話的所有片段保存到指定的文件,包括失敗的和啟動片段。 請注意,它不保存任何命令。

使用-history選項的/save命令保存自啟動以來在jshell中鍵入的所有內容。

使用-start選項的/save命令將默認啟動定義保存到指定的文件。

可以使用/open命令從文件重新加載片段。 該命令將文件名作為參數。

以下jshell會話聲明一個Counter類,創建其對象,並調用對象上的方法。 最后,它將所有活動的片段保存到名為jshell.jsh的文件中。 請注意,文件擴展名.jsh是jshell文件的習慣。 你可以使用你想要的任何其他擴展。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> class Counter {
   ...>    private int count;
   ...>    public synchronized int next() {
   ...>      return ++count;
   ...>    }
   ...>    public int current() {
   ...>      return count;
   ...>    }
   ...> }
|  created class Counter
jshell> Counter counter = new Counter()
counter ==> Counter@25bbe1b6
jshell> counter.current()
$3 ==> 0
jshell> counter.next()
$4 ==> 1
jshell> counter.next()
$5 ==> 2
jshell> counter.current()
$6 ==> 2
jshell> /save jshell.jsh
jshell> /exit
|  Goodbye

此時,應該在當前目錄中有一個名為jshell.jsh的文件,內容如下所示:

class Counter {
   private int count;
   public synchronized int next() {
     return ++count;
   }
   public int current() {
     return count;
   }
}
Counter counter = new Counter();
counter.current()
counter.next()
counter.next()
counter.current()

以下jshell會話將打開jshell.jsh文件,該文件將回放上一個會話中保存的所有片段。 打開文件后,可以開始調用counter變量的方法。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /open jshell.jsh
jshell> counter.current()
$7 ==> 2
jshell> counter.next()
$8 ==> 3
jshell>

二十. 重置JShell狀態

可以使用/reset`命令重置JShell的執行狀態。 執行此命令具有以下效果:

  • 在當前會話中輸入的所有片段都將丟失,因此在執行此命令之前請小心。
  • 啟動片段被重新執行。
  • 重新啟動工具的執行狀態。
  • 使用/set命令設置的jshell配置被保留。
  • 使用/env命令設置的執行環境被保留。

以下jshell會話聲明一個變量,重置會話,並嘗試打印變量的值。 請注意,在重置會話時,所有聲明的變量都將丟失,因此找不到先前聲明的變量:

jshell> int x = 987
x ==> 987
jshell> /reset
|  Resetting state.
jshell> x
|  Error:
|  cannot find symbol
|    symbol:   variable x
|  x
|  ^
jshell>

二十一. 重新加載JShell狀態

假設在jshell會話中回放了許多片段,並退出會話。 現在想回去並回放這些片段。 一種方法是啟動一個新的jshell會話並重新輸入這些片段。 在jshell中重新輸入幾個片段是一個麻煩。 有一個簡單的方法來實現這一點 —— 通過使用/reload命令。 /reload命令重置jshell狀態,並以與之前輸入的序列相同的順序回放所有有效的片段。 可以使用-restore-quiet選項來自定義其行為。

沒有任何選項的/reload命令會重置jshell狀態,並從以下先前的操作/事件中回放有效的歷史記錄,具體取決於哪一個:

  • 當前會話開始
  • 當執行最后一個/reset命令時
  • 當執行最后一個/reload命令時

可以使用-restore選項與/reload命令一起使用。 它將重置和回放以下兩個操作/事件之間的歷史記錄,以最后兩個為准:

  • 啟動jshell
  • 執行/reset命令
  • 執行/reload命令

使用-restore選項執行/reload命令的效果有點難以理解。 其主要目的是恢復以前的執行狀態。 如果在每個jshell會話開始時執行此命令,從第二個會話開始,你的會話將包含在jshell會話中執行的所有代碼段! 這是一個強大的功能。 也就是說,可以對代碼片段求值,關閉jshell,重新啟動jshell,並執行/reload -restore命令作為第一個命令,並且不會丟失以前輸入的任何代碼段。 有時,將在會話中執行/ reset命令兩次,並希望恢復這兩個復位之間存在的狀態。 可以使用此命令來實現此結果。

以下jshell會話在每個會話中創建一個變量,並通過在每個會話執行/reload -restore命令來恢復上一個會話。 該示例顯示第四個會話使用在第一個會話中聲明的x1的變量。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> int x1 = 10
x1 ==> 10
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
jshell> int x2 = 20
x2 ==> 20
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
-: int x2 = 20;
jshell> int x3 = 30
x3 ==> 30
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /reload -restore
|  Restarting and restoring from previous state.
-: int x1 = 10;
-: int x2 = 20;
-: int x3 = 30;
jshell> System.out.println("x1 is " + x1)
x1 is 10
jshell>

/reload命令顯示其回放的歷史記錄。 可以使用-quiet選項來抑制重放顯示。 -quiet選項不會抑制回放歷史記錄時可能會生成的錯誤消息。 以下示例使用兩個jshell會話。 第一個會話聲明一個x1的變量。 第二個會話使用-quiet選項與/reload命令。 請注意,此時,由於使用了-quiet選項,因此在第二個會話中沒有看到回放顯示變量x1被重新加載。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> int x1 = 10
x1 ==> 10
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /reload -restore -quiet
|  Restarting and restoring from previous state.
jshell> x1
x1 ==> 10
jshell>

二十二. 配置JShell

使用/set命令,可以自定義jshell會話,從啟動片段和命令到設置平台特定的片段編輯器。

1. 設置代碼編輯器

JShell工具附帶一個默認的代碼編輯器。 在jshell中,可以使用/edit命令來編輯所有的片段或特定的片段。 /edit命令在編輯器中打開該片段。 代碼編輯器是一個特定於平台的程序,如Windows上的notepad.exe,將被調用來編輯代碼段。 可以使用/set命令與編輯器作為參數來設置或刪除編輯器設置。 命令的有效形式如下:

/set editor [-retain] [-wait] <command>
/set editor [-retain] -default
/set editor [-retain] -delete

如果使用-retain選項,該設置將在jshell會話中持續生效。

如果指定了一個命令,則該命令必須是平台特定的。 也就是說,需要在Windows上指定Windows命令,UNIX上指定UNIX命令等。 該命令可能包含標志。 JShell工具會將要編輯的片段保存在臨時文件中,並將臨時文件的名稱附加到命令中。 編輯器打開時,無法使用jshell。 如果編輯器立即退出,應該指定-wait選項,這將使jshell等到編輯器關閉。 以下命令將記事本設置為Windows上的編輯器:

jshell> /set editor -retain notepad.exe

-default選項將編輯器設置為默認編輯器。 -delete選項刪除當前編輯器設置。 如果-retain選項與-delete選項一起使用,則保留的編輯器設置將被刪除:

jshell> /set editor -retain -delete
|  Editor set to: -default
jshell>

設置在以下環境變量中的編輯器 ——JSHELLEDITOR,VISUAL或EDITOR,優先於默認編輯器。 這些環境變量按順序查找編輯器。 如果沒有設置這些環境變量,則使用默認編輯器。 所有這些規則背后的意圖是一直有一個編輯器,然后使用默認編輯器作為后備。 沒有任何參數和選項的 /set編輯器命令打印有關當前編輯器設置的信息。

以下jshell會話將記事本設置為Windows上的編輯器。 請注意,此示例將不適用於Windows以外的平台,需要在平台特定的程序中指定編輯器。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor -default
jshell> /set editor -retain notepad.exe
|  Editor set to: notepad.exe
|  Editor setting retained: notepad.exe
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor -retain notepad.exe
jshell> 2 + 2
$1 ==> 4
jshell> /edit
jshell> /set editor -retain -delete
|  Editor set to: -default
jshell> /exit
|  Goodbye
C:\Java9Revealed>SET JSHELLEDITOR=notepad.exe
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set editor
|  /set editor notepad.exe
jshell>

2. 設置反饋模式

執行代碼段或命令時,jshell會打印反饋。 反饋的數量和格式取決於反饋模式。 可以使用四種預定義的反饋模式之一或自定義反饋模式:

  • silent
  • concise
  • normal
  • verbose

silent模式根本不給任何反饋,verbose模式提供最多的反饋。 concise模式給出與normal模式相同的反饋,但是格式緊湊。 設置反饋模式的命令如下:

/set feedback [-retain] <mode>

這里,<mode>是四種反饋模式之一。 如果要在jshell會話中保留反饋模式,請使用-retain選項。

也可以在特定的反饋模式中啟動jshell:

jshell --feedback <mode>

以下命令以verbose反饋模式啟動jshell:

C:\Java9Revealed>jshell --feedback verbose

以下示例說明如何設置不同的反饋模式:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> 2 + 2
$1 ==> 4
jshell> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$2 ==> 4
|  created scratch variable $2 : int
jshell> /set feedback concise
jshell> 2 + 2
$3 ==> 4
jshell> /set feedback silent
-> 2 + 2
-> System.out.println("Hello")
Hello
-> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 + 2
$6 ==> 4
|  created scratch variable $6 : int

jshell中設置的反饋模式是臨時的。 它只對當前會話設置。 要在jshell會話中持續反饋模式,使用以下命令:

jshell> /set feedback -retain

此命令將持續當前的反饋模式。 當再次啟動jshell時,它將配置在執行此命令之前設置的反饋模式。 仍然可以在會話中臨時更改反饋模式。 如果要永久設置新的反饋模式,則需要使用/set feedback <mode>命令,再次執行該命令以保持新的設置。

還可以設置一個新的反饋模式,並且同時通過使用-retain選項來保留以后的會話。 以下命令將反饋模式設置為verbose,並將其保留在以后的會話中:

jshell> /set feedback -retain verbose

要確定當前的反饋模式,只需使用反饋參數執行`/se命令。 它打印用於在第一行設置當前反饋模式的命令,然后是所有可用的反饋模式,如下所示:

jshell> /set feedback
|  /set feedback normal
|
|  Available feedback modes:
|     concise
|     normal
|     silent
|     verbose
jshell>

Tips
當學習jshell時,建議以verbose反饋模式啟動它,因此可以獲得有關命令和代碼段執行狀態的詳細信息。 這將有助於更快地了解該工具。

3. 創建自定義反饋模式

這四個預配置的反饋模式很適合使用jshell。 它們提供不同級別的粒度來自定義您shell。 當然,可以擁有自己的自定義反饋模式。必須編寫幾個定制步驟。 很可能,將需要在預定義的反饋模式中自定義一些項目。 可以從頭開始創建自定義反饋模式,或者通過從現有的反饋模式中復制自定義反饋模式,並有選擇地進行自定義。 創建自定義反饋模式的語法如下:

/set mode <mode> [<old-mode>] [-command|-quiet|-delete]

這里,<mode>是自定義反饋模式的名稱; 例如,kverbose。 <old-mode>是現有的反饋模式的名稱,其設置將被復制到新模式。 使用-command選項顯示有關設置模式的信息,而在設置模式時使用-quiet選項不顯示任何信息。 -delete選項用於刪除模式。

以下命令通過從預定義的verbose反饋模式復制所有設置來創建一個名為kverbose的新反饋模式:

/set mode kverbose verbose -command

以下命令將持續使用名為kverbose的新反饋模式以備將來使用:

/set mode kverbose -retain

需要使用-delete選項刪除自定義反饋模式。 但是不能刪除預定義的反饋模式。 如果保留使用自定義反饋模式,則可以使用-retain選項將其從當前和所有將來的會話中刪除。 以下命令將刪除kverbose反饋模式:

/set mode kverbose -delete -retain

在這一點上,預定義的詳細模式和自定義kverbose模式之間沒有區別。 創建反饋模式后,需要自定義三個設置:

  • 提示
  • 輸出截斷限制
  • 輸出格式

Tips
完成定制反饋模式之后,需要使用/set feedback <new-mode>命令開始使用它。

可以設置兩種類型的提示進行反饋 - 主提示和延續提示。 當jshell准備好讀取新的代碼段/命令時,會顯示主提示。 當輸入多行代碼段時,延續提示將顯示在行的開頭。 設置提示的語法如下:

/set prompt <mode> "<prompt>" "<continuation-prompt>"

在這里,<prompt>是主提示符,<continuation-prompt>是延續提示符。

以下命令設置kverbose模式的提示:

/set prompt kverbose "\njshell-kverbose> " "more... "

可以使用以下命令為反饋模式設置每種類型的動作/事件的最大字符數:

/set truncation <mode> <length> <selectors>

這里,<mode>是設置截斷限制的反饋模式;<length>是指定選擇器顯示的最大字符數。 <selectors>是逗號分隔的選擇器列表,用於確定應用截斷限制的上下文。 選擇器是表示特定上下文的預定義關鍵字,例如,vardecl是一個在沒有初始化的情況下表示變量聲明的選擇器。 有關設置截斷限制和選擇器的更多信息,請使用以下命令:

/help /set truncation

以下命令將截斷限制設置為80個字符,並將變量值或表達式設置為五個字符:

/set truncation kverbose 80
/set truncation kverbose 5 expression,varvalue

請注意,最具體的選擇器確定要使用的實際截斷限制。 以下設置使用兩個選擇器 —— 一個用於所有類型的片段(80個字符),一個用於表達式和變量值(5個字符)。 對於表達式,第二個設置是最具體的設置。 在這種情況下,如果變量的值超過五個字符,則顯示時將被截斷為五個字符。

設置輸出格式是一項復雜的工作。 需要根據操作/事件設置你所期望的所有輸出類型的格式。 有關設置輸出格式的更多信息,請使用以下命令:

/help /set format

設置輸出格式的語法如下:

/set format <mode> <field> "<format>" <selectors>

這里,<mode>是要設置輸出格式的反饋模式的名稱;;<field>是要定義的上下文特定格式;<format>用於顯示輸出。<format>可以包含大括號中的預定義字段的名稱,例如{name},{type},{value}等,將根據上下文替換為實際值。 <selectors>是確定將使用此格式的上下文的選擇器。

當為輸入的代碼片段添加,修改或替換表達式時,以下命令設置顯示格式以供反饋。 整個命令輸入一行。
/set format kverbose display "{result}{pre}創建一個類型為{type}的名為{name}的臨時變量,並使用{value} {post}”初始化“表達式添加,修改,替換原來的信息。

以下jshell會話通過從預定義的詳細反饋模式復制所有設置來創建一個名為kverbose的新反饋模式。 它自定義提示,截斷限制和輸出格式。 它使用verbose和kverbose反饋模式來比較jshell行為。 請注意,以下示例中的所有命令都需要以一行形式輸入。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set feedback
|  /set feedback -retain normal
|
|  Available feedback modes:
|     concise
|     normal
|     silent
|     verbose
jshell> /set mode kverbose verbose -command
|  Created new feedback mode: kverbose
jshell> /set mode kverbose -retain
jshell> /set prompt kverbose "\njshell-kverbose> " "more... "
jshell> /set truncation kverbose 5 expression,varvalue
jshell> /set format kverbose display "{result}{pre}created a temporary variable named {name} of type {type} and initialized it with {value}{post}" expression-added,modified,replaced-primary
jshell> /set feedback kverbose
|  Feedback mode: kverbose
jshell-kverbose> 2 +
more... 2
$2 ==> 4
|  created a temporary variable named $2 of type int and initialized it with 4
jshell-kverbose> 111111 + 222222
$3 ==> 33333
|  created a temporary variable named $3 of type int and initialized it with 33333
jshell-kverbose> /set feedback verbose
|  Feedback mode: verbose
jshell> 2 +
   ...> 2
$4 ==> 4
|  created scratch variable $4 : int
jshell> 111111 + 222222
$5 ==> 333333
|  created scratch variable $5 : int
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set feedback
|  /set feedback -retain normal
|
|  Retained feedback modes:
|     kverbose
|  Available feedback modes:
|     concise
|     kverbose
|     normal
|     silent
|     verbose
jshell>

在這個jshell會話中,可以將表達式和變量值的截斷限制設置為kverbose反饋模式的五個字符。 這就是為什么在kverbose反饋模式中,表達式111111 + 222222的值打印為33333,而不是333333。這不是一個錯誤。 這是由你的設置造成的。

請注意,命令/set feedback顯示用於設置當前反饋模式的命令和可用反饋模式的列表,其中列出了您的反饋模式kverbose。

當創建自定義反饋模式時,了解現有反饋模式的所有設置將會有所幫助。 可以使用以下命令打印所有反饋模式的所有設置列表:

/set mode

還可以通過將模式名稱作為參數傳遞給命令來打印特定反饋模式的所有設置列表。 以下命令打印silent反饋模式的所有設置的列表。 輸出中的第一行是用於創建silent模式的命令。

jshell> /set mode silent
|  /set mode silent -quiet
|  /set prompt silent "-> " ">> "
|  /set format silent display ""
|  /set format silent err "%6$s"
|  /set format silent errorline "    {err}%n"
|  /set format silent errorpost "%n"
|  /set format silent errorpre "|  "
|  /set format silent errors "%5$s"
|  /set format silent name "%1$s"
|  /set format silent post "%n"
|  /set format silent pre "|  "
|  /set format silent type "%2$s"
|  /set format silent unresolved "%4$s"
|  /set format silent value "%3$s"
|  /set truncation silent 80
|  /set truncation silent 1000 expression,varvalue
jshell>

4. 設置啟動代碼片段

可以使用/set命令和start參數來設置啟動代碼片段和命令。 啟動jshell時,啟動代碼段和命令將自動執行。 已經看到從幾個常用軟件包導入類型的默認啟動片段。 通常,使用/env命令設置類路徑和模塊路徑,並將import語句導入到啟動腳本。

可以使用/list -start命令打印默認啟動片段列表。 請注意,此命令將打印默認的啟動片段,而不是當前的啟動片段。 也可以刪除啟動片段。 默認啟動片段包括在啟動jshell時獲得的啟動片段。 當前的啟動片段包括默認啟動片段減去當前jshell會話中刪除的那些片段。

可以使用/set命令的以下形式設置啟動片段/命令:

/set start [-retain] <file>
/set start [-retain] -default
/set start [-retain] -none

使用-retain選項是可選的。 如果使用它,該設置將在jshell會話中保留。

第一個形式用於從文件中設置啟動片段/命令。 當在當前會話中執行/reset/reload命令時,該文件的內容將被用作啟動片段/命令。 從文件中設置啟動代碼后,jshell緩存文件的內容以供將來使用。 在重新設置啟動片段/命令之前,修改文件的內容不會影響啟動代碼。

第二種形式用於將啟動片段/命令設置為內置默認值。

第三個形式用於設置空啟動。 也就是說,啟動時不會執行片段/命令。

沒有任何選項或文件的/set start命令顯示當前啟動設置。 如果啟動是從文件設置的,它會顯示文件名,啟動片段以及啟動片段的設置時間。

請考慮以下情況。 com.jdojo.jshell目錄包含一個com.jdojo.jshell.Person類。 在jshell中測試這個類,並使用java.time包中的類型。 為此,啟動設置將如下所示。

/env -class-path C:\Java9Revealed\com.jdojo.jshell\build\classes
import java.io.*
import java.math.*
import java.net.*
import java.nio.file.*
import java.util.*
import java.util.concurrent.*
import java.util.function.*
import java.util.prefs.*
import java.util.regex.*
import java.util.stream.*
import java.time.*;
import com.jdojo.jshell.*;
void printf(String format, Object... args) { System.out.printf(format, args); }

將設置保存在當前目錄中startup.jsh的文件中。 如果將其保存在任何其他目錄中,則可以在使用此示例時使用該文件的絕對路徑。 請注意,第一個命令是Windows的/env -class-path命令,假定將源代碼存儲在C:\目錄下。 根據你的平台更改類路徑值,並在計算機上更改源代碼的位置。

注意startup.jsh文件中的最后一個片段。 它定義了printf()的頂層函數,它是System.out.printf()方法的包裝。 默認情況下,printf()函數包含在JShell工具的初始構建中。 后來被刪除了。 如果要使用簡短的方法名稱(如printf())而不是System.out.printf(),以便在標准輸出上打印消息,則可以將此代碼段包含在啟動腳本中。 如果希望在jshell中使用println()printf()頂層方法,則需要啟動jshell,如下所示:

C:\Java9Revealed>jshell --start DEFAULT --start PRINTING

DEFAULT參數將包括所有默認的import語句,而PRINTING參數將包括print()println()printf()方法的所有版本。 使用此命令啟動jshell后,執行/list -start命令查看命令中使用的兩個啟動選項添加的所有啟動導入和方法。

以下jshell會話將顯示如何從文件中設置啟動信息及其在子序列會話中的用法:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set start
|  /set start -default
jshell> /set start -retain startup.jsh
jshell> Person p;
|  created variable p, however, it cannot be referenced until class Person is declared
jshell> /reset
|  Resetting state.
jshell> Person p;
p ==> null
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set start
|  /set start -retain startup.jsh
|  ---- startup.jsh @ Feb 20, 2017, 10:06:47 AM ----
|  /env -class-path C:\Java9Revealed\com.jdojo.jshell\build\classes
|  import java.io.*
|  import java.math.*
|  import java.net.*
|  import java.nio.file.*
|  import java.util.*
|  import java.util.concurrent.*
|  import java.util.function.*
|  import java.util.prefs.*
|  import java.util.regex.*
|  import java.util.stream.*
|  import java.time.*;
|  import com.jdojo.jshell.*;
|  void printf(String format, Object... args) { System.out.printf(format, args); }
jshell> Person p
p ==> null
jshell> LocalDate.now()
$2 ==> 2016-11-15
jshell>
jshell> printf("2 + 2 = %d%n", 2 + 2)
2 + 2 = 4
jshell>

Tips
直到重新啟動jshell,執行/reset/reload命令之前,設置啟動片段/命令才會生效。 不要在啟動文件中包含/reset或者/reload命令。 當啟動文件加載時,它將導致無限循環。

有三個預定義的腳本的名稱如下:

  • DEFAULT
  • PRINTING
  • JAVASE

DEFAULT腳本包含常用的導入語句。 PRINTING腳本定義了重定向到PrintStream中的print()println()printf()方法的頂層JShell方法,如本節所示。 JAVASE腳本導入所有的Java SE軟件包,它是很大的,需要幾秒鍾才能完成。 以下命令顯示如何將這些腳本保存為啟動腳本:

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> println("Hello")
|  Error:
|  cannot find symbol
|    symbol:   method println(java.lang.String)
|  println("Hello")
|  ^-----^
jshell> /set start -retain DEFAULT PRINTING
jshell> /exit
|  Goodbye
C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> println("Hello")
Hello
jshell>

首次使用println()方法導致錯誤。 將PRINTING腳本保存為啟動腳本並重新啟動該工具后,該方法將起作用。

二十三. 使用JShell文檔

JShell工具附帶了大量文檔。 因為它是一個命令行工具,在命令行上閱讀文檔會有一點點困難。 可以使用/help/? 命令顯示命令列表及其簡要說明。

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 referenced by name or id
|  /drop <name or id>  -- delete a source entry referenced by name or id
...

可以使用特定命令作為/help命令的參數來獲取有關命令的信息。 以下命令打印有關/help命令本身的信息:

jshell> /help /help
|
|  /help
|
|  Display information about jshell.
|  /help
|       List the jshell commands and help subjects.
|
|  /help <command>
|       Display information about the specified command. The slash must be included.
|       Only the first few letters of the command are needed -- if more than one
|       each will be displayed.  Example:  /help /li
|
|  /help <subject>
|       Display information about the specified help subject. Example: /help intro

以下命令將顯示有關/list/set命令的信息。 輸出未顯示,因為它們很長:

jshell> /help /list
|...
jshell> /help /set
|...

有時,命令用於處理多個主題,例如,/set命令可用於設置反饋模式,代碼段編輯器,啟動腳本等。如果要打印有關命令的特定主題的信息 ,可以使用以下格式的/help命令:

/help /<command> <topic-name>

以下命令打印有關設置反饋模式的信息:

jshell> /help /set feedback

以下命令打印有關創建自定義反饋模式的信息:

jshell> /help /set mode

使用/help命令與主題作為參數打印有關主題的信息。 目前,有三個預定義的主題:intro,shortcuts和context。 以下命令將打印JShell工具的介紹:

jshell> /help intro

以下命令打印可在JShell工具中使用的快捷方式列表及其說明:

jshell> /help shortcuts

以下命令將打印用於設置執行上下文的選項列表。 這些選項與/env/reset/reload命令一起使用。

jshell> /help context

二十四. Shell API

JShell API可讓你對片段求值引擎進行編程訪問。 作為開發人員,不能使用此API。 這意味着要被諸如NetBeans IDE這樣的工具使用,這些工具可能包含一個等效於JShell命令行工具的UI,讓開發人員可以對IDE內部代碼的代碼段求值,而不是打開命令提示符來執行此操作。 在本節中,簡要介紹了JShell API並通過一個簡單的例子來展示它的用法。

JShell API位於jdk.jshell模塊和jdk.jshell包中。 請注意,如果使用JShell API,模塊將需要讀取jdk.jshell模塊。 JShell API很簡單。 它主要由三個抽象類和一個接口組成:

JShell
Snippet
SnippetEvent
SourceCodeAnalysis

JShell類的一個實例代表一個代碼片段求值引擎。 這是JShell API中的主要類。 JShell實例在執行時維護所有代碼片段的狀態。

代碼片段由Snippet類的實例表示。 JShell實例在執行代碼段時生成代碼片段事件。

代碼段事件由SnippetEvent接口的實例表示。 片段事件包含片段的當前和先前狀態,片段的值,導致事件的片段的源代碼,如果在片段執行期間發生異常,則為異常對象等。

SourceCodeAnalysis類的實例為代碼段提供了源代碼分析和建議功能。 它回答了以下問題:

  • 這是一個完整的片段嗎?
  • 這個代碼片段可以通過附加一個分號來完成嗎?

SourceCodeAnalysis實例還提供了一些建議列表,例如Tab補全和訪問文檔。 此類旨在由提供JShell功能的工具使用。

下圖顯示了JShell API的不同組件的用例圖。 在接下來的部分,解釋這些類及其用途。 最后一節中給出了一個完整的例子。

用例圖

1. 創建JShell類

JShell類是抽象的。 它提供了兩種創建實例的方法:

  • 使用靜態create()方法
  • 使用內部構建類JShell.Builder

create()方法返回一個預配置的JShell實例。 以下代碼片段顯示了如何使用create()方法創建JShell:

// Create a JShell instance
JShell shell = JShell.create()

JShell.Builder類允許通過指定代碼段ID生成器,臨時變量名稱生成器,打印輸出的打印流,讀取代碼片段的輸入流以及錯誤輸出流來記錄錯誤來配置JShell實例。 可以使用JShell類的builder()靜態方法獲取JShell.Builder類的實例。 以下代碼片段顯示了如何使用JShell.Builder類創建一個JShell,其中代碼中的myXXXStream是對流對象的引用:

// Create a JShell instance
JShell shell = JShell.builder()
                     .in(myInputStream)
                     .out(myOutputStream)
                     .err(myErrorStream)
                     .build();

一旦擁有JShell實例, 可以使用eval(String snippet)方法對片段求值。 可以使用drop(PersistentSnippet snippet)方法刪除代碼段。 可以使用addToClasspath(String path)方法將路徑附加到類路徑。 這三種方法改變了JShell實例的狀態。

Tips
完成使用JShell后,需要調用close()方法來釋放資源。 JShell類實現了AutoCloseable接口,因此使用try-with-resources塊來處理JShell是確保在不再使用時關閉它的最佳方式。 JShell是可變的,不是線程安全的。

可以使用JShell類的onSnippetEvent(Consumer<SnippetEvent> listener) onShutdown(Consumer<JShell> listener)方法來注冊片段事件處理程序和JShell關閉事件處理程序。 當代碼片段的狀態由於第一次求值或其狀態由於對另一個代碼段求值而被更新時,代碼段事件將被觸發。

JShell類中的sourceCodeAnalysis()方法返回一個SourceCodeAnalysis類的實例,可以用於代碼輔助功能。

JShell類中的其他方法用於查詢狀態。 例如,snippets()types()methods()variables()方法分別返回所有片段的列表,所有帶有有效類型聲明的片段,帶有有效方法聲明的片段和帶有有效變量聲明的片段。

eval()方法是JShell類中最常用的方法。 它求值/執行指定的片段並返回List<SnippetEvent>。 可以查詢列表中的代碼段事件的執行狀態。 以下是使用eval()方法的代碼示例代碼:

String snippet = "int x = 100;";
// Evaluate the snippet
List<SnippetEvent> events = shell.eval(snippet);
// Process the results
events.forEach((SnippetEvent se) -> {
    /* Handle the snippet event here */
});

2. 使用代碼片段

Snippet類的實例代表一個代碼片段。 該類不提供創建對象的方法。 JShell的片段提供為字符串,並且將Snippet類的實例作為片段事件的一部分。 代碼段事件還提供了代碼片段的以前和當前狀態。 如果有一個Snippet對象,可以使用JShell類的status(Snippet s)方法查詢其當前狀態,該方法返回Snippet.Status

Tips
Snippet類是不可變的,線程安全的。

Java中有幾種類型的片段,例如變量聲明,具有初始化的變量聲明,方法聲明,類型聲明等。Snippet類是一個抽象類,並且有一個子類來表示每個特定類型的片段。 以下列表顯示代表不同類型代碼片段的類的繼承層次結構:

  • Snippet
    • ErroneousSnippet
    • ExpressionSnippet
    • StatementSnippet
    • PersistentSnippet
      • ImportSnippet
      • DeclarationSnippet
        • MethodSnippet
        • TypeDeclSnippet
        • VarSnippet

Snippet類的子類的名稱是直觀的。 例如,PersistentSnippet的一個實例表示保存在JShell中的代碼段,可以重用,如類聲明或方法聲明。 Snippet類包含以下方法:

String id()
String source()
Snippet.Kind kind()
Snippet.SubKind subKind()

id()方法返回代碼段的唯一ID,並且source()方法返回其源代碼。 kind()subKind()方法返回一個代碼片段的類型和子類型。

代碼段的類型是Snippet.Kind枚舉的常量,例如IMPORTTYPE_DECLMETHODVAR等。代碼片段的子類型提供了有關其類型的更多具體信息,例如,如果 snippet是一個類型聲明,它的子類型將告訴你是否是類,接口,枚舉或注解聲明。片段的子類型是Snippet.SubKind枚舉的常量,如CLASS_SUBKINDENUM_SUBKIND等。 Snippet.Kind枚舉包含一個isPersistent屬性,如果此類代碼是持久性的,則該值為true,否則為false。。

Snippet類的子類添加更多方法來返回特定類型的片段的特定信息。 例如,VarSnippet類包含一個typeName()方法,它返回變量的數據類型。MethodSnippet類包含parameterTypes()signature()方法,它們返回參數類型和方法的完整簽名的字符串形式。

代碼片段不包含其狀態。 JShell執行並保存代碼片段的狀態。 請注意,執行代碼片段可能會影響其他代碼片段的狀態。 例如,聲明變量的代碼片段可能會將聲明方法的代碼片段的狀態從有效變為無效,反之亦然,如果該方法引用了該變量。 如果需要片段的當前狀態,請使用JShell類的status(Snippet s) 方法,該方法返回Snippet.Status枚舉的以下常量:

  • DROPPED:該代碼片片段由於使用JShell類的drop()方法刪除而處於非有效狀態。
  • NONEXISTENT:該代碼段無效,因為它不存在。
  • OVERWRITTEN:該代碼片段已被替換為新的代碼片段,因此無效。
  • RECOVERABLE_DEFINED:該片段是包含未解析引用的聲明片段。 該聲明具有有效的簽名,並且對其他代碼段可見。 當其他代碼段將其狀態更改為VALID時,可以恢復並使用它。
  • RECOVERABLE_NOT_DEFINED:該片段是包含未解析引用的聲明片段。 該代碼段具有無效的簽名,而其他代碼片段不可見。 當其狀態更改為VALID時,可以稍后使用。
  • REJECTED:代碼片段無效,因為初始求值時編譯失敗,並且無法進一步更改JShell狀態。
  • VALID:該片段在當前JShell狀態的上下文中有效。

3. 處理代碼片段事件

JShell會生成片段事件作為片段求職或執行的一部分。 可以通過使用JShell類的onSnippetEvent()方法注冊事件處理程序或使用JShell類的eval()方法的返回值來執行代碼段事件,返回類型是List <SnippetEvent>。 以下顯示如何處理片段事件:

try (JShell shell = JShell.create()) {
    // Create a snippet
    String snippet = "int x = 100;";
    shell.eval(snippet)
         .forEach((SnippetEvent se) -> {
              Snippet s = se.snippet();
              System.out.printf("Snippet: %s%n", s.source());
              System.out.printf("Kind: %s%n", s.kind());
              System.out.printf("Sub-Kind: %s%n", s.subKind());
              System.out.printf("Previous Status: %s%n", se.previousStatus());
              System.out.printf("Current Status: %s%n", se.status());
              System.out.printf("Value: %s%n", se.value());
        });
}

4. 一個實例

我們來看看JShell API的操作。 下面包含名為com.jdojo.jshell.api的模塊的模塊聲明。

// module-info.java
module com.jdojo.jshell.api {
    requires jdk.jshell;
}

下面包含JShellApiTest類的完整代碼,它是com.jdojo.jshell.api模塊的成員。

// JShellApiTest.java
package com.jdojo.jshell.api;
import jdk.jshell.JShell;
import jdk.jshell.Snippet;
import jdk.jshell.SnippetEvent;
public class JShellApiTest {
    public static void main(String[] args) {
        // Create an array of snippets to evaluate/execute
        // them sequentially
        String[] snippets = { "int x = 100;",
                              "double x = 190.89;",
                              "long multiply(int value) {return value * multiplier;}",
                              "int multiplier = 2;",
                              "multiply(200)",
                              "mul(99)"
                            };
        try (JShell shell = JShell.create()) {
            // Register a snippet event handler
            shell.onSnippetEvent(JShellApiTest::snippetEventHandler);
            // Evaluate all snippets
            for(String snippet : snippets) {
                shell.eval(snippet);
                System.out.println("------------------------");
            }
        }
    }
    public static void snippetEventHandler(SnippetEvent se) {
        // Print the details of this snippet event
        Snippet snippet = se.snippet();
        System.out.printf("Snippet: %s%n", snippet.source());
        // Print the cause of this snippet event
        Snippet causeSnippet = se.causeSnippet();
        if (causeSnippet != null) {
            System.out.printf("Cause Snippet: %s%n", causeSnippet.source());
        }
        System.out.printf("Kind: %s%n", snippet.kind());
        System.out.printf("Sub-Kind: %s%n", snippet.subKind());
        System.out.printf("Previous Status: %s%n", se.previousStatus());
        System.out.printf("Current Status: %s%n", se.status());
        System.out.printf("Value: %s%n", se.value());
        Exception e = se.exception();
        if (e != null) {
            System.out.printf("Exception: %s%n", se.exception().getMessage());
        }
    }
}

輸出結果:

A JShellApiTest Class to Test the JShell API
Snippet: int x = 100;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 100
------------------------
Snippet: double x = 190.89;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: VALID
Current Status: VALID
Value: 190.89
Snippet: int x = 100;
Cause Snippet: double x = 190.89;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: VALID
Current Status: OVERWRITTEN
Value: null
------------------------
Snippet: long multiply(int value) {return value * multiplier;}
Kind: METHOD
Sub-Kind: METHOD_SUBKIND
Previous Status: NONEXISTENT
Current Status: RECOVERABLE_DEFINED
Value: null
------------------------
Snippet: int multiplier = 2;
Kind: VAR
Sub-Kind: VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 2
Snippet: long multiply(int value) {return value * multiplier;}
Cause Snippet: int multiplier = 2;
Kind: METHOD
Sub-Kind: METHOD_SUBKIND
Previous Status: RECOVERABLE_DEFINED
Current Status: VALID
Value: null
------------------------
Snippet: multiply(200)
Kind: VAR
Sub-Kind: TEMP_VAR_EXPRESSION_SUBKIND
Previous Status: NONEXISTENT
Current Status: VALID
Value: 400
------------------------
Snippet: mul(99)
Kind: ERRONEOUS
Sub-Kind: UNKNOWN_SUBKIND
Previous Status: NONEXISTENT
Current Status: REJECTED
Value: null
------------------------
The main() method creates the following six snippets and stores them in a String array:
1.
"int x = 100;"
 
2.
"double x = 190.89;"
 
3.
"long multiply(int value) {return value * multiplier;}"
 
4.
"int multiplier = 2;"
 
5.
"multiply(200)"
 
6.
"mul(99)"
 

try-with-resources塊用於創建JShell實例。 snippetEventHandler()方法被注冊為片段事件處理器。 該方法打印有關代碼段的詳細信息,例如源代碼,導致代碼片段狀態更新的源代碼,代碼片段的先前和當前狀態及其值等。最后,使用for-each循環遍歷所有的片段,並調用eval()方法來執行它們。

當執行這些代碼片段時,讓我們來看看JShell引擎的狀態:

  • 執行代碼段1時,代碼段不存在,因此從NONEXISTENT轉換為VALID 狀態。 它是一個變量聲明片段,它的計算結果為100。
  • 當代碼段2被執行時,它已經存在。 請注意,它使用不同的數據類型聲明名為x的同一個變量。 其以前的狀態為VALID,其當前狀態也為VALID。 執行此代碼段會將狀態從VALID更改為OVERWRITTEN,因為不能使用同一名稱的兩個變量。
  • Snippet 3聲明一個multiply()的方法,它使用一個multiplier 的未聲明變量,其狀態從NONEXISTENT更改為RECOVERABLE_DEFINED。 定義了方法,這意味着它可以被引用,但不能被調用,直到定義了適當類型的multiplier 變量。
  • Snippet 4定義了multiplier變量,使代碼段3有效。
  • Snippet 5調用multiply()方法。 該表達式是有效的,結果為400。
  • Snippet 6調用mul()方法的,但從未定義過。 該片段是錯誤的並被拒絕。

通常,JShell API和JShell工具不會一起使用。 但是,讓我們一起使用它們只是為了樂趣。 JShell API只是Java中的另一個API,也可以在JShell工具中使用。 以下jshell會話實例化一個JShell,注冊一個片段事件處理器,並對兩個片段求值。

C:\Java9Revealed>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> /set feedback silent
-> import jdk.jshell.*
-> JShell shell = JShell.create()
-> shell.onSnippetEvent(se -> {
>>  System.out.printf("Snippet: %s%n", se.snippet().source());
>>  System.out.printf("Previous Status: %s%n", se.previousStatus());
>>  System.out.printf("Current Status: %s%n", se.status());
>>  System.out.printf("Value: %s%n", se.value());
>> });
-> shell.eval("int x = 100;");
Snippet: int x = 100;
Previous Status: NONEXISTENT
Current Status: VALID
Value: 100
-> shell.eval("double x = 100.89;");
Snippet: double x = 100.89;
Previous Status: VALID
Current Status: VALID
Value: 100.89
Snippet: int x = 100;
Previous Status: VALID
Current Status: OVERWRITTEN
Value: null
-> shell.close()
-> /exit
C:\Java9Revealed>

二十五. 總結

Java Shell在JDK 9中稱為JShell,是一種提供交互式訪問Java編程語言的命令行工具。 它允許對Java代碼片段求值,而不是強制編寫整個Java程序。 它是Java的REPL。 JShell也是一個API,可以為其他工具(如IDE)的Java代碼提供對REPL功能的編程訪問。

可以通過運行保存在JDK_HOME\bin目錄下的jshell程序來啟動JShell命令行工具。 該工具支持執行代碼片段和命令。 片段是Java代碼片段。 片段可以用來執行和求值,JShell維護其狀態。 它還跟蹤所有輸入的片段的狀態。 可以使用命令查詢JShell狀態並配置jshell環境。 為了區分命令和片段,所有命令都以斜杠(/)開頭。

JShell包含幾個功能,使開發人員更有效率,並提供更好的用戶體驗,例如自動補全代碼並在工具中顯示Javadoc。 JShell嘗試使用JDK中已有的功能(如編譯器API)來解析,分析和編譯代碼段,以及使用Java Debugger API將現有代碼片段替換為JVM中的新代碼片段。 JShell的設計使得可以在Java語言中使用新的構造,而不會對JShell工具本身進行很少或不用改動。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM