Javaスクリプト入門

みんなでJavaスクリプトに入門しましょう!

クラスをちゃんと定義する

それでは、まずはJavaっぽく書いてみましょう。

public class Main {
  public static void main(String[] args) {
    System.out.println("Hello!");
  }
}

これを、任意のファイル名で保存します。たとえばhello。
実行するにはJava 11以降が必要です。みなさんすでにSDKMANを入れてると思うのでOracle OpenJDKの12.0.1を使ってみましょう。

$ sdk use java 12.0.1-open

では実行します

$ java --source 12 hello
Hello!

実行できました!

これではちょっと面白くないので、先ほどのファイルの先頭にちょっとなにか追加しておきます。

#! /home/naoki/.sdkman/candidates/java/current/bin/java --source 12
public class Main {
  public static void main(String[] args) {
    System.out.println("Hello!");
  }
}

いわゆるShebangですね。

そして実行権限を付加

$ chmod +x hello

実行してみます。

$ ./hello
Hello!

やった!

これはJEP 330: Launch Single-File Source-Code Programsに基づくもので、Java 11から導入されています。
Java11ではjavacせずにJavaファイルが実行できるようになる - きしだのHatena

これを「スクリプト」と呼ばないとき、現代のスクリプト言語も実行時にネイティブコードになったりするので、どの時点でコンパイルされればスクリプトなのか(スクリプトではないのか)という話になっておもしろいですね。

もっとスクリプトっぽく書く

さっきのは あまりにもJavaだったので、もっとJavaっぽくなく書いてみます。

System.out.println("Hello2!")
/exit

このファイルを任意のファイル名で保存します。たとえばhello2。行末のセミコロンも不要です。

では実行します。実行するためにはJava 9以降が必要です。すでに先ほどJava 12を導入してますね。

$ jshell hello2
Hello2!

やった!

もうちょっと複雑なコードを書いてみましょう。

import java.time.*
var t = LocalDate.now()
System.out.println(t.minusMonths(1) + " to " + t)
System.out.println("Hello2!")
/exit

実行してみます。

$ jshell hello2
2019-05-29 to 2019-06-29
Hello2!

だいぶスクリプトっぽい!

注意が必要なのは、ブロックの中ではセミコロンが必要というところです。

import java.time.*
var t = LocalDate.now()
System.out.println(t.minusMonths(1) + " to " + t)
for (int i=0; i< 3; ++i) {
  System.out.println("Hello" + i);
}
/exit

実行してみます。

$ jshell hello2
2019-05-29 to 2019-06-29
Hello0
Hello1
Hello2

Shebangもやってみましょう。

#! /home/naoki/.sdkman/candidates/java/current/jshell
import java.time.*
var t = LocalDate.now()
System.out.println(t.minusMonths(1) + " to " + t)
for (int i=0; i< 3; ++i) {
  System.out.println("Hello" + i);
}
/exit

そして実行。

$ ./hello2
エラー:
'#'は不正な文字です
#! /home/naoki/.sdkman/candidates/java/current/bin/jshell
^
エラー:
式の開始が不正です
#! /home/naoki/.sdkman/candidates/java/current/bin/jshell
   ^
エラー:
式の開始が不正です
#! /home/naoki/.sdkman/candidates/java/current/bin/jshell
               ^
2019-05-29 to 2019-06-29
Hello0
Hello1
Hello2

がーん!!
実行されは するけど、#!の行もJavaとして解釈しようとするためにエラーになっちゃってますね。
これどうにかしないのかな?

7/1追記 BTSは立ってますね。 https://bugs.openjdk.java.net/browse/JDK-8167440

ちなみに、シェルスクリプトで1行目を無視するとかもありますが、OpenJDKソースを改変するなら、ここに

if (src.startsWith("#")) {
  return false;
}

を入れると#がコメントになってイケます。
https://github.com/openjdk/jdk/blob/master/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java#L1194

まとめ

Javaスクリプト最高!

GraalVMはどれだけ遅いか

GraalVM流行ってますね。
そして、多くの人はGraalをAOTとして使うnative-imageのことだけをGraalVMと言ってたりします。
ご安心を。このエントリではGraalをJITとして使うHotSpotモードとGraalをAOTとして使うnative-imageの両方が遅いという話です。

GraalVMは速い、と言われてますが、残念ながらHotSpotモードでC2より速い結果を手元では出せていません。
公式ブログでは1.7倍から5倍速くなると書いてますけど、手元では再現できてません。
Under the hood of GraalVM JIT optimizations - graalvm - Medium

native-imageは速い、というのはよくありますが、これはネイティブ化によりJVMの起動時間や最適化の時間、最適化されずに動く時間が省略されるので起動が速い、という話です。長く動くプロセスの場合、そういった起動にかかる時間というのは無視できるようになり、実行時に集めた情報を使って最適化するJITのほうが速いです。

用語について

ところでここで用語の確認を。
GraalVMというのは、Javaで書かれたJITコンパイラGraalを中心とした多言語環境です。
普通にGraalをJavaJITコンパイラとして使うのがGraalVMのHotSpotモードです。ようするにjavaコマンドです。
Graalの最適化機構を一般のスクリプト言語から使えるようにしてJavaScriptRubyJITコンパイラにしてしまうのがTruffleです。
そして、Graalを事前にJavaコードに適用してネイティブ化するというのがnative-imageです。

計測

ということで、どのくらい遅いか比べてみます。
コードはこのレイトレ。
https://github.com/kishida/smallpt4j/blob/original/src/main/java/naoki/smallpt/SmallPT.java
commonsのFastMathを使っているので、通常のjava.lang.Mathに置き換えて実行します。また、ループ中のSystem.out.printlnはコメントアウトしました。
それはそうと、以前はこのコードはImageIOを使っているのでGraalVMでネイティブ化できなかったのですが、いまではできるようになってます。
実行するとこんな感じのPNG画像が出力されます。
f:id:nowokay:20190627042056p:plain

GraalVM 19.0.2 CE

ここではWindowsで動かしてみました。GraalVMは19.0.2CEです。
まずはHotSpotモード。

C:\Users\naoki\Documents\prj>java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-20190603180034.buildslave.jdk8u-src-tar--b03)
OpenJDK 64-Bit GraalVM CE 19.0.2 (build 25.212-b03-jvmci-19-b04, mixed mode)

C:\Users\naoki\Documents\prj>java SmallPT
Samples:40 Type:master Time:PT9.713S

10秒弱。

ネイティブ化してみます。

C:\Users\naoki\Documents\prj>native-image SmallPT
[smallpt:21796]    classlist:   1,793.30 ms
...
[smallpt:21796]        write:     786.36 ms
[smallpt:21796]      [total]:  26,307.39 ms

C:\Users\naoki\Documents\prj>smallpt
Samples:40 Type:master Time:PT45.234S

45秒。だいぶ遅い!

GraalVM 19.0.2 EE

ついでにEEでも試してみます。

C:\Users\naoki\Documents\prj>java -version
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b31)
Java HotSpot(TM) 64-Bit GraalVM EE 19.0.2 (build 25.212-b31-jvmci-19-b04, mixed mode)

C:\Users\naoki\Documents\prj>java SmallPT
Samples:40 Type:master Time:PT10.281S

10秒強。あんま変わらん。

ではネイティブ化。

C:\Users\naoki\Documents\prj>native-image SmallPT
[smallpt:43056]    classlist:   2,165.31 ms
...
[smallpt:43056]        image:     788.17 ms
Warning: Generating and stripping of debug info not supported on Windows[smallpt:43056]        write:     775.12 ms
[smallpt:43056]      [total]:  29,450.40 ms

C:\Users\naoki\Documents\prj>smallpt
Samples:40 Type:master Time:PT13.039S

13秒!CEに比べるとめっちゃ速い!

OpenJDK

それではC2版。つまりふつうのOpenJDK

$ java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_212-b03)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.212-b03, mixed mode)

$ java SmallPT
Samples:40 Type:master Time:PT6.795S

6.8秒。GraalVMに比べるとだいぶ速いです。

OpenJDKの11でも試してみます。

$ java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

$ java SmallPT
Samples:40 Type:master Time:PT5.8484946S

5.8秒。お、ちょっと速くなってる!

まとめ

ということで、まとめるとこう。
ついでにMacでも計測しています。Windowsが8コアでMacが4コアなので、ちょうど倍くらいの時間がかかってますね。CEのnative-imageがそれほど遅くないのだけど、コア数によるものかWindowsMacの違いか気になるところ。

GraalVM CE HotSpot GraalVM CE Native GraalVM EE HotSpot GraalVM EE Native OpenJDK 8 OpenJDK 11
Windows(i7-8Cores) 9.713 45.234 10.281 13.039 6.795 5.848
Mac(i7-4Cores) 20.966 47.069 18.797 19.135 16.783 12.594

グラフにするとこう。
f:id:nowokay:20190627045257p:plain

ということで、GraalVMのネイティブイメージは起動以外は速くない むしろ遅い、GraalVM EEのネイティブイメージはCEに比べるとかなり速いけどHotSpotモードほどではない、JDKは8から11でも速くなってる、という結果になりました。
LinuxでやればGraalは速いという噂もあります。

7/4追記 19.1.0が出てたので試してみたけど、CEのnative-imageがちょっと速くなった?

CE HotSpot CE Native EE HotSpot EE Native
19.1.0 Win 10.676 43.419 10.787 12.972

NetBeansからHelidonを使う

Helidonです、Helidon。

プロジェクトの作成

HelidonはMavenプロジェクト的には素直なので、Mavenはバンドルの3.3.9で大丈夫です。 プロジェクト作成もNetBeansからできます。

新規作成でjava with MavenからProject from Archetypeを選びます。
f:id:nowokay:20190620004502p:plain

Archetypeの検索でHelidonといれるとSEとMPが出るので、好きなほうを。
ここではSEにします。
f:id:nowokay:20190620004813p:plain

あとは適当にプロジェクト名などを。
f:id:nowokay:20190620005453p:plain

プロジェクトが作成されました。
f:id:nowokay:20190620005633p:plain

ビルドや実行なども通常のプロジェクトどおりメニューから行えます。

Native Imageの作成

Helidon SEはGraalVMのNative Imageに対応していますが、単にjarファイルをnative-imageコマンドでコンパイルはできなかったので、Mavenからネイティブコンパイルを行う必要があります。
ネイティブコンパイルするときは、native-imageプロファイルをActivateする必要があります。
f:id:nowokay:20190620010247p:plain

また、GraalVMのnative-imageコマンドにPATHが通ってる必要があります。バージョンはRC16ではダメでした。19.0.0以降が必要です。
ビルドするとネイティブコンパイルが始まります。
f:id:nowokay:20190620014744p:plain

ただ、native-imageにパスを通したりプロファイルを切り替えたりは面倒なので、Quarkusの場合と同じようにGoalを設定するほうがよさそう。 f:id:nowokay:20190620020356p:plain

この場合、Goalsにはpackage、Profilesにnative-image、PropertiesにEnv.PATHを設定します。
Env.PATHは${Env.PATH}:で始めて既存のパスを有効にする必要があるのと、パスなのでbinまでいれる必要があることに注意してください。Remember Asにnativeなどを設定しておくとメニューから選べるようになります。 f:id:nowokay:20190620020154p:plain

これでプロファイル切り替えをせずにネイティブコンパイルできるようになります。 f:id:nowokay:20190620020824p:plain

ということで、やはり1分20秒くらいかけてネイティブコンパイルできました。 f:id:nowokay:20190620021507p:plain

NetBeansからQuarkusを使う

Quarkusです、Quarkus。

Mavenのアップデート

QuarkusのビルドにはMaven 3.5.3が必要ですが、NetBeans11にバンドルされているMavenは3.3.9なので、そのままではビルドなどができません。
ということで、Mavenの最新版をダウンロードしてどこかに解凍します。
Maven – Download Apache Maven

そしたら、PreferenceのJavaタブでMavenを選んでMavenを解凍したパスを設定します。
f:id:nowokay:20190615014520p:plain

プロジェクトの作成

残念ながら、GUIからのプロジェクト作成は できなさそう。なので、ドキュメント通りにコマンドラインでプロジェクト生成します。

$ mvn io.quarkus:quarkus-maven-plugin::create

そうするとArtifact IDなどを聞かれてくるので、適当に入力していきます。 (もちろん空のMavenやGradleプロジェクトを作成してビルドファイルを編集すれば、プロジェクト作れます。)

※ 6/29追記
0.18.0でネイティブコンパイルがGraalVM 19に対応しました。プロジェクト作成時にバージョンを明示しておく必要があります。

$ mvn io.quarkus:quarkus-maven-plugin:0.18.0:create

実行用Goalの設定

開発時のプロジェクト実行にはquarkus:dev:というGoalを使う必要があります。そのままではNetBeansのメニューからのプロジェクト実行ができません。
なので、プロジェクト実行時に実行されるGoalを指定します。

プロジェクトのプロパティからActionsを選んでRun ProjectのExecute Goalsにquarkus:dev:を指定します。 またSet Propertiesのところにexec.argsでclasspathが指定してあるので、この行は削除しておきます。 f:id:nowokay:20190615015617p:plain

そうすると[F6]などでプロジェクトが実行できるようになります。 f:id:nowokay:20190615020021p:plain

Native Imageの作成

ついでにNative Imageを作れるようにしましょう。
現時点ではGraalVMの最新であるバージョン19ではなく、ひとつ前のRC16が必要です。
Release GraalVM Community Edition 1.0 RC16 · oracle/graal
※ 6/29追記 0.18.0からGraalVM 19に対応しています。

必須ではないですがJavaプラットフォームとして登録しておきましょう。 f:id:nowokay:20190615020438p:plain

Build->Compileでプロジェクトのプラットフォームとして指定しておくのが無難です。 f:id:nowokay:20190615020551p:plain

ActionsでAdd Customを選んでNativeを追加します。(別の名前でもいいです) f:id:nowokay:20190615020852p:plain

Execute Goalsにpackage -Pnativeを指定します。また、GRAALVM_HOMEを指定しておく必要があります。
※ 7/15追記 0.19ではGRAALVM_HOMEが不要になっています。
f:id:nowokay:20190615020739p:plain

Use JDK for Maven Buildを使うとJAVA_HOMEが埋め込まれるので、それをGRAALVM_HOMEに書き換えるのが楽です。
f:id:nowokay:20190615021549p:plain

そうするとプロジェクトメニューのRun MavenにNativeが追加されるので、ここからNative Imageが作成できるようになります。
f:id:nowokay:20190615021720p:plain

1分ちょっとかかってNative Imageのビルドができました!
f:id:nowokay:20190615021805p:plain

Windows(Cygwin)でSDKMANがうまく動かないのはcurlのせい

WindowsCygwin使ってSDKMANを動かしてみたのですが、うまく動きませんでした。
なんかファイルをダウンロードしてるのに、ファイルがみつからないと言ってます。というか、curlがなんか文句いってますね。

$ sdk install micronaut

Downloading: micronaut 1.1.3

In progress...

######################################################################## 100.0%Warning: Failed to create the file 
Warning: /home/naoki/.sdkman/tmp/4ulZL0XaBO5l3DQVqssmcLsZUEZJ4cgQ.bin: No such 
Warning: file or directory
                                                                           0.1%
curl: (23) Failed writing body (0 != 16360)
mv: '/home/naoki/.sdkman/tmp/4ulZL0XaBO5l3DQVqssmcLsZUEZJ4cgQ.bin' を stat できません: No such file or directory

これ、結局はWindowscurlコマンドが入って、cygwin上からWindowscurlコマンドを実行してるためにエラーになってる模様。 WindowsパスのC:/home/naoki/.sdkman...に保存しようとして怒られてるんでしょうね。 ということでCygwinのsetup.exeでcurlをインストールして実行するとうまく動きました。

$ sdk install micronaut

Downloading: micronaut 1.1.3

In progress...

###################################################################################### 100.0%

Installing: micronaut 1.1.3
Done installing!


Setting micronaut 1.1.3 as default.

switch式の値付きbreakはyieldになる?

前のエントリではswitch式で値を返すのはbreak-withになると書いてましたが、yieldにしようぜーという議論が勃発しています。
switch式の値付きbreakはbreak-withになることがほぼ確定(追記あり) - きしだのHatena

というか、JEPはすでにyieldになっています。
JEP 354: Switch Expressions (Preview)

https://mail.openjdk.java.net/pipermail/amber-spec-experts/2019-May/001301.html

これは10日ほど前のBrian Goetz氏からの「自転車置き場の議論やろうぜー」という投稿から議論がはじまっていて、ハイフン付きキーワードだけじゃなくコンテキスト付きキーワードも考慮しようという話です。
Call for bikeshed -- break replacement in expression switch

反論としては yield (1);がyieldメソッドに引数1を渡すのか、(1)をswitch式の結果として返すのかわかりにくいんでは、特にIDEの実装が大変なんでは、というのが出てますね。
Yield as contextual keyword (was: Call for bikeshed -- break replacement in expression switch)

どうなるか興味津々

switch式の値付きbreakはbreak-withになることがほぼ確定(追記ありyieldになりました)

Java12でプレビューとしてswitch式が入って、Java13で正式化できるよう作業が進んでいます。 そんな中、switch式を正式化するJEPのドラフトが出ていました。 JEP draft: Switch Expressions 追記 あっというまにドラフトではなくなっています JEP 354: Switch Expressions

プレビューとの違いはbreakがハイフン付キーワードのbreak-withになるという記述があります。

int result = switch (s) {
    case "Foo": 
        break-with 1;
    case "Bar":
        break-with 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break-with 0;
};

かなり違和感が・・・
そしてこれは、package-privateとかいろんなキーワードが現れる準備でもありそうです。
引き算と区別ができないんでは?という点について、ハイフン付キーワードは必ずキーワードとの組み合わせになるはずなので、もともと引き算とはみなせなかったので大丈夫です。

5/22追記: break-withじゃなくてyieldになるかも。JEPが変更されています。しかし議論が大盛り上がり中でどこに落ち着くかわからない状況。 switch式の値付きbreakはyieldになる? - きしだのHatena