Java 9 揭祕(11. Java Shell)

Tips
作一個終身學習的人。java

Java 9

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

  • 什麼是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命令行工具相同的功能。express

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!」消息的最低要求:api

// 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編程語言的方法。 對於有經驗的開發人員,它提供了一種快速的方式來查看代碼段的結果,而無需編譯和運行整個程序。 它還提供了一種使用增量方法快速開發原型的方法。 添加一段代碼,獲取即時反饋,並添加另外一個代碼片斷代碼,直到原型完成。session

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工具自己進行不多或不用改動。

相關文章
相關標籤/搜索