JigsawでSPIを使用する

Java9では,ようやくJigsawが導入され,モジュール化ができるようになります.
一方,SPI(SurviceProviderInterface)と呼ばれる仕組みがあり,広く利用されています.
今回は,JigsawでSPIを扱うための方法を紹介します.

Jigsawの基礎知識はある程度あるものと想定します.
もしも分からない部分があれば,櫻庭さんの記事*1 *2 *3 *4 *5 *6が分かりやすくまとまっているので,そちらをご参照ください.

SPIとは

SPIはSurviceProviderIntefaceの略で,あるプログラムやライブラリに対して,第三者が実装を提供するため仕組みです.
SPIはJDBCやPluggable Annotation Processing APIなど,多くの場面で利用されています.
ライブラリはSPIとなるインターフェースを提供し,java.util.ServiceLoaderを用いて,第三者の実装を読み込みます.
三者はライブラリが提供するインターフェースを実装し,所定の方法でパッケージングし,クラスパスに指定します.
今までは,実装を提供する第三者は.jarファイルのMETA-INF/services内にインターフェースのFQCNが名前のファイルを作成し,その中に一行づつ具象クラスのFQCNを記述します.

例を用いて説明しましょう.
例はIndex of /~shinyafox/jigsaw/example/ModuledSPIExample/においてあるので,そちらも参考にしてください.

SPI提供者

まず,以下がSPIとなるインターフェース,HelloInterfaceです.
挨拶文を返すメソッドhelloが宣言されます.

package com.example.hello.spi;

public interface HelloInterface {
    String hello();
}

このSPIを利用するクラスが以下です.
ServiceLoader#loadを用いてクラスパス内からHelloInterfaceを実装するクラスを探します.
HelloInterfaceを実装する全てのクラスに対し,helloメソッドを呼び出して,その結果を出力しています.

package com.example.hello;

import java.util.Iterator;
import java.util.ServiceLoader;

import com.example.hello.spi.HelloInterface;

public class Main {
    public static void main(String... args) {
        Iterator<HelloInterface> i = ServiceLoader.load(HelloInterface.class).iterator();

        while (i.hasNext()) {
            HelloInterface hi = i.next();
            String hello = hi.hello();
            System.out.println(hello);
        }
    }
}

これらは以下のようなファイル構造でhello.jarにパッケージングされています.

hello.jar
└com
 └example
  └hello
   ├spi
   │└HelloInteface.class
   └Main.class
SPI実装者

さて,SPIを実装するクラスは以下のようになります.

package jp.example.hello;

import com.example.hello.spi.HelloInterface;

public class JapaneseHello implements HelloInterface {
    public String hello() {
        return "こんにちは";
    }
}

以下のファイル構造で,JapaneseHelloクラスをJARファイルとしてhello_jp.jarにパッケージングします.

hello_jp.jar
├jp
│└example
│ └hello
│  └JapaneseHello.class
└META-INF
 └services
  └com.example.hello.spi.HelloInterface

/META-INF/services/com.example.hello.spi.HelloInterface内に,このインターフェースを実装するクラスのFQCNを記述することで,ServiceLoaderで読み込むクラスの対象にできます.
JapaneseHelloを読み込ませるため,/META-INF/services/com.example.hello.spi.HelloInterfaceは以下の内容になります.

jp.example.hello.JapaneseHello

複数のクラスをServiceLoaderで読み込む対象とする場合は,改行して複数のクラスを列挙します.

実行

hello_jp.jarをクラスパスに与えて,hello.jarを実行すると,以下の出力が得られます.

$ java -cp "hello_jp.jar:hello.jar" com.example.hello.Main
こんにちは

ここまでが従来のSPIの取り扱い方です.

JigsawでSPIを利用する

ここからはJigsawでSPIを利用する方法を紹介します.

SPI提供者

SPIをモジュール化して提供するには,module-info.java内でexports節で提供するSPIを含むパッケージを指定し,公開します.
同時に,uses節を用いて提供するSPIを指定します.

先ほどのhello.jarを用いて例示します.
実装は同じで,JARファイルの構造が異なります.

hello.jar
├com
│└example
│ └hello
│  ├spi
│  │└HelloInteface.class
│  └Main.class
└module-info.class

module-info.classが加わってることに注意してください.
module-info.classはmodule-info.javaコンパイルして生成されたファイルです.
このファイルがあることで,hello.jarは名前付きモジュールとして扱われます.

module-info.javaを以下に示します.

module com.example.hello {
    exports com.example.hello;
    exports com.example.hello.spi; // SPIであるHelloInterfaceを可視にする

    uses com.example.hello.spi.HelloInterface; // HelloInterfaceがSPIであることを指示
}

ポイントはexports節でHelloInterfaceを含むcom.example.hello.spiが指定されることと,uses節でHelloInterfaceが指定されることです.

SPI実装者

SPIを実装するクラスを含むJARパッケージをモジュール化する時は,module-info.java内のprovides節でSPIを実装するクラスを指定します.
provides節の構文は「provides SPIとなるインターフェースのFQCN with 実装クラスのFQCN」です.

これも先ほどの例を用いて説明します.

実装は先程の例と同じで,hello_jp.jarのファイル構造が異なります.

hello_jp.jar
├jp
│└example
│ └hello
│  └JapaneseHello.class
└module-info.class

/META-INF/services以下が無くなり,module-info.classに変わったことに注意してください.
module-info.classの元となるmodule-info.javaを以下に示します.

module jp.example.hello {
    requires com.example.hello;
    exports jp.example.hello;

    provides com.example.hello.spi.HelloInterface with jp.example.hello.JapaneseHello;
}

provides節で,HelloInterfaceの実装クラスとしてJapaneseHelloが指定されています.

今回の例では違いを分かりやすくするために,/META-INF/servicesを削除しましたが,互換性のために,それらを残すことも可能です.
特に,URLClassLoaderを用いて実装クラスを読み込む場合に,読み込む側の実装が適切でないと,/META-INF/services以下が無い場合は実装クラスが読み込まれません.
これについては別の記事で紹介しますが,互換性のために/META-INF/services以下を残しておくことをオススメします.

実行

それでは実行しましょう.
実行はhello.jarとhello_jp.jarをモジュールとして読み込むように設定し,com.example.helloモジュールのcom.example.hello.Mainを実行するクラスとして指定します.

$ java -mp "hello_jp.jar:hello.jar" -m com.example.hello/com.example.hello.Main
こんにちは

ご覧のようにして,JigsawでSPIを扱うことができました.

jshellでファイルを読み込んでスクリプトみたいに実行する

jshellはファイルを読み込んで実行できます.
そのため,Javaコードをファイルに書いておいて,jshellで読み込ませてスクリプトみたいに実行できます.
これがホントのJavaScriptですね.

以下の様なファイルを用意してみましょう

System.out.println("This is the true Java Script!!")
/ex

ポイントはファイルの最後に/exを付けることです!
/exは/exitの省略形で,jshellを終了させます.
これが無いと,Welcomeメッセージが出力され,jshellセッションが始まってしまいます.

実行は単純に「jshell file.jsh」とするだけです.

$ ~/bin/jdk9b122/bin/jshell file.jsh 
This is the true Java Script!!

注意点としては,プログラムが実行されるまでに結構時間がかかります.
jshellを実行するとJVMインスタンスが2つ立ち上がるためです.
jshellのプロンプトとコンパイルを実行するJVMインスタンスと,コードを実行するJVMインスタンスです.
また,jshellは起動時に,補完のために現在読み込まれているクラスパスのインデックス化を行うためでもあります.

更に,一行ずつコンパイルを行うため,その部分でもオーバヘッドがあり,javaコマンドでクラスファイルを実行するのと同じ程の実行時間にはならない点にも注意してください.

まぁ,気軽にJavaコードを実行できるようになったのは良いですね.

JJUG CCC 2016 Springでjshellの発表してきた #jjug_ccc

JJUG CCC 2016 Springでjshellの発表をしてきました!

何人来てくださるか不安でしたが,始まってみれば盛況で本当に良かったです.
ぜひ皆さん,jshellを試して頂いて,皆にjshellの良さをシェアしてください!

スライドは以下から.

質問について

質問はSli.doというウェブサービスを用いて,投稿してもらいました.
合計で10以上の質問が寄せられ,非常に活発に質問していただけました.
それらについての回答はスライドの最後に入れましたので,ご確認ください.

気づいた時に打ち込んで質問できるので良いですね.
マイクリレーとか,そういった事が無くなるので,効率的で非常に良かったです.
回答も画面で見せながら質問を皆とシェアしながらできるので良かったです.

願わくばTwitterにもハッシュタグ付きで呟かれると,Twitterが盛り上がっていいんですけどね.

糖質制限の経過報告

糖質制限を今年の9月からやっています.
自分向けのまとめも兼ねた,糖質制限 Advent Calendar 2015の16日目の記事です.
前の人はKosuke Kidaさんで,「ランナーから見た糖質」でした.
次の人はyancyaさんです.

糖質制限

糖質制限をする前は,77kg〜78kgでした.

今年の4月までは72kg前後で推移していました.
今年の3月,4月に結構暴飲暴食してしまい,75kgまで増えてしまいました.
更に酷いことに,4月,5月ぐらいに冷凍うどんの旨さに目覚めてしまい,主食を冷凍うどん2玉,3玉にしてしまいました.
あれよあれよと体重が増えて,数週間で77kg〜78kgに.

そもそもとして,ちっちゃい時から炭水化物が大好きでした.
中学生で82kgとかあったのですが,思い返せばあの時のご飯(米)の食べ方は異常でした.
普段から米を丼(「おちゃわん」なんて可愛いものではない)で食べていました.
しゃぶしゃぶとかすき焼きをすると,米を丼で3杯ぐらい食べていた気がします.

何故太るのか疑問でしたが,糖質制限を知り,糖質と脂質がどう働くかを知ったら,断言できます.
太る原因は(もちろん運動不足もありますが)糖質と脂質でした.

糖質制限のきっかけ

糖質制限のきっかけは留学でした.
留学はきっかけにもなりましたし,お米を食べなくても辛くなくなる要因にもなりました.

↓No.1 Crazy American Dinner(左から炭水化物,炭水化物,肉,炭水化物)

8月〜9月の第一週までアメリカのシアトルに留学していました.
噂通りアメリカ人はめっちゃ食います.
というか,めっちゃ食わせてきます.
親戚のおばちゃんもビックリなぐらい食わせてきます.
断っても食わせてきます.むしろちょっとキレて食わせてきます.

日に日に,ズボンきつなったなぁと思うようになりました.
留学も終盤に差し掛かった8月下旬,ホストファミリーの親戚の家で体重計に乗った時に,82kg*1という数字を目にします.
これはヤバいと思うようになりました.
何としても痩せねばと.

そんな時にグーグル先生が糖質制限の存在を教えてくれました.
最初に見たのは「【マンガ】で分かる糖質制限ダイエット 誰でも細マッチョin池袋皮膚科 | ゆうスキンクリニック(皮膚科)」でした.
漫画形式で読みやすく,理論についても触れられていました.
これを見た時に糖質制限をやろうと決めました.

余談ですが,我が家は糖尿の家系だったので,糖質制限にはより一層惹かれました.

実はアメリカでも少し糖質制限をしていました.
お昼ごはんはObertoのベーコンジャーキー(1袋800円〜1000円するけどめっちゃうまい)にするとか.

これ,めっちゃうまいので,アメリカに行った時は食べてみて欲しい.何故日本に無いのかと思うぐらい.アメリカで食べて美味しかった数少ない食品の1つ.

アメリカの炭水化物は本当に美味しくなくて,パンも,スパゲッティも,ご飯も圧倒的に日本の方が美味しかったです.
特に僕が大好きなお米は酷くて,日本の様な良質なお米がないのも問題なのだけど,炊飯器が貧弱過ぎて全然美味しく炊けてませんでした.
そのため日に日にあまり炭水化物を食べたいと思わなくなりました.
お陰で,日本に帰った後もご飯を食べなくても辛いとは思わなくなりました.
これが,上で述べたお米を食べなくても辛くなくなる要因という点です.

糖質制限

9月から糖質制限を始めました.
ようやく本題です.


9月上旬〜中旬


糖質制限を始めた1週間は徹底的に糖質を抜きました.アトキンスダイエットで言うインダクションです.
お肉や卵,お魚をメインに食べるようにし,お肉だけではお腹が空くので野菜やキノコも今まで以上に食べるようにしました.
この時は本当に辛かった記憶があります.
それは炭水化物を食べられない辛さではなく,全然お腹が一杯にならない,満腹感が出ない辛さです.

満腹感を感じるのは血糖値が上昇した時です.
この時の糖質漬けの脳は糖質制限食では血糖値が上がらなく,全然満腹感を感じないでしょうね.
気を紛らわすためにアタリメとかスルメとかを食べました.これは結構効果的でした.

身体の変化としては,この時期は結構頭痛が起きた記憶があります.
また,あまり力が入らなくなりました.
とは言いつつ,9月は夏休みで,家でダラダラするぐらいだったので特に辛くはありませんでした.
今までは快便でしたが,糖質制限食に移行しだすとビックリするぐらい便が出なくなりました.

この辺りはインダクションに起きるとよく言われる現象ですね.

別の辛さとしては,最初の1週間は全然体重が減りませんでした.
なので,これが効果があるのかが視覚的に分からず,精神的に辛かったです.
でも一週間を過ぎてからは面白い様に毎日300g〜400gづつ減りだしたので面白くなりました.

9月下旬〜10月下旬


この時期でも糖質は一日40gに収まる様にしました.
多分20gを超えない日の方が多かったと思います.

この頃から料理に凝りだしました.
さすがに飽きてきたので.

一番料理に力を入れていた時期だと思います.
この時の晩御飯例.お肉しか写っていないのは,お肉に加えて,サラダ(サニーレタス+シーザドレッシング)も食べていました.
お肉か魚,野菜,キノコの組み合わせになるようにしました.

この時期になると空腹感は全然感じ無くなりました.
ただ,チョコとかの甘いものをメチャクチャ食べたくなりました.
無性にチョコが食べたくなったので,色々低糖質なチョコを買いあさりました.

左2つはとにかく苦い.食べた時に糖質制限してるな〜という実感と謎の達成感を得られるので良い.
右のは普通のチョコとあまり変わらない.左2つを食べたことがあるとメチャクチャ美味しく感じられる.チョコに砂糖を入れた人は天才だと思える.

この時期も徐々には減っていた.
でも,週末に横浜に行ったりして外食を結構していたので,減っては増え減っては増えという感じ.

10月下旬には72kgまで戻せた.

10月24日〜11月1日

JavaOneのためにサンフランシスコに行っていました.
炭水化物は控えめでしたが,散々美味しい物をたらふく食べました.最高でした.
無限ビールも,無限ワインも飲んだ.
食後のアイスも食べた.飛行機でハーゲンダッツ出るので最高.

2kg太りました.

74kg.

11月上旬〜今まで


寒くなってきたので,この頃から家鍋を始めました.
11月の中旬までは前述のような料理をしていました.

ただ,キッチンに立つのも辛いし,鶏を捌くのも指が凍える様になってきたので,11月中旬以降は鍋ばっかりになりました.
鍋は基本的に豚肉,白菜,厚揚げでしています.
豚肉は100g100円前後で買えるので結構コスパは良い.
お肉は300g〜400gを食べるようにしています.
たまに椎茸やネギも入れます.

基本的に鍋キューブの「濃厚白湯」,「寄せ鍋しょうゆ」,「豚骨味噌」をローテしている.
180cc水/1キューブなのだけど,それじゃ全然煮えないし,すぐに水が蒸発してしまうので一回に3キューブ使っている.
1キューブ当たり,炭水化物量が2g〜4gぐらいなのでそこまで糖質多くない.
出汁を全部飲む訳じゃないしね.

そんなこんなで,11月16日には一時的に最初の目標である70kgを切りました.
減少幅は以前よりも小さくなりましたが.一週間に500gづつぐらい減っていっています.

現在は67kg後半で推移しています.

糖質制限の結果と今後

体重:77kg -> 67.6kg
体脂肪率:20.6%*2
服のサイズ:L〜LL -> M〜L

Mサイズの服が入るようになったのが凄く嬉しい

痩せたけど,体形としてはまだまだという感じ.
食事制限しかしていなくて,あまり運動をしていなかったので,今後は運動も合わせてしていきたい.

とりあえず,次の目標として体重65kg,体脂肪率18%を目指している.
その後は60kg,15%を目指したい.

今は甘いものを食べたいと思うことも少なくなったし,炭水化物も食べたいとは思わない.
なので,糖質制限をするにしてもしないにしても,炭水化物控えめな食事になると思う.

糖質制限の副作用

自炊するようになった

自炊するようになりました.
家事スキルがかなり上がった

食費は増えなかった,むしろ減ったかも

糖質制限をすると食費が増えるという風に言う人もいるけど,僕の場合はむしろ減った気がします.
上の自炊するようになったにも関わるのだけれど,今まではコンビニ弁当,外食が主だったので結構食費が掛かっていました.
今は1日1000円も掛かっていないので結果的には食費は減ったと言えるかも.

*1:アメリカはポンドだったので180ポンドとかぐらいだった気がする

*2:最初に体脂肪率を測っていなかったので結果しかない.多分25%は超えてたと思う

DeprecatedがJDK9で変わるかもしれない件(JEP 277: Enhanced Deprecation)

この記事はJava Advent Calendar 2015の11日目の記事です.
昨日はkokuzawaさんの「JavaFXのUIをJUnit形式でテストする | KATSUMI KOKUZAWA'S BLOG」でした.
明日は@cero_tさんです.

JDK9でDeprecatedが変わるかもしれません.
この変更はJEP 277: Enhanced Deprecationで行われます.
手前味噌で,雑な翻訳ですが,翻訳版もあります.

まだ確定したわけではないので,常に元のJEPを確認するようにしてください.
今後変更があると思います.

概略

JDK9では以下の様にDeprecatedまわりが変更されます.

  • Deprecatedを用いてより一層詳細なAPIの状態などを提供できるようにする
  • アプリケーションが使用している非推奨APIを実行時にログとして出力する
  • アプリケーションで使用している非推奨APIの情報を静的に生成するツールを提供する
  • 機能のライフサイクルに関するJava SEとJDKのポリシーを明確化する

動機

なぜ@Deprecatedを変更しないといけないのでしょう?
現在,@Deprecatedはかなり矛盾した状態になっています.

@Deprecatedは1.5で導入されました.
@DeprecatedはJavadocでは以下のように説明されています.

注釈@Deprecatedの付いたプログラム要素は、一般に危険であったり,より適切なほかのプログラム要素で代用できることもあり,プログラマには使用を薦められないプログラム要素です.
推奨されていないプログラム要素が使用されている場合,あるいは推奨されているコードにおいてオーバーライドされている場合,コンパイラが警告を発します.

Deprecatedは,別のAPIに移行することを推進し,非推奨APIに新たに使うことが無いように,そのAPIがライフサイクルの最後を迎えるという事を開発者に伝えることを意図していました.
上記のJavadocでは述べられていないのに,様々な文章で,いつか@DeperecatedなAPIは削除されるだろうと述べられています.

@Deprecatedは結果的に幾つかの異なった目的で使われてしまっています.
何も削除されるべきではないと考える人たちによって,非推奨APIは実際にはほとんど削除されていません.
一方で,非推奨になったら,いつか削除されるだろうと思っている人たちもおり,混乱が生じています.
どちらも意図したものではありませんでした.
このことは開発者に伝わった@Deprecatedの意味と,そしてなにより,開発者が非推奨APIの使用に出会った時に何をするべきかが不明瞭だったために起きました.
現在,非推奨が実際のところ何を意味するのかということで皆が混乱しており,更には誰も非推奨を深刻に捉えていないと,Stuart Marksは指摘しています.
更には,このことはJava SEから何かを削除することを一層困難にしているとも指摘しています.

別の問題として,非推奨APIについての警告がコンパイル時にしか無いということがあります.
APIが将来のJDKで非推奨になっても,既存のバイナリは警告無しにその非推奨APIを使用し続けてしまいます.
もしもその非推奨APIが削除されたら,古いアプリケーションバイナリは警告が出ることもなく,突然リンクエラーで起動できなくなってしまうでしょう.
更に酷いことに,既存のバイナリが非推奨なAPIに依存しているかどうかを開発者が確認する方法がありません.

Deprecatedを用いてより一層詳細なAPIの状態などを提供できるようにする

APIの非推奨の状態についてのきめ細かい情報をツールとランタイムに提供するために@Deprecatedアノテーションを強化します.
以下メソッドと列挙型Reasonがjava.lang.Deprecatedアノテーションに追加するように提案されています.

デフォルト値 説明
value() Reason[] UNSPECIFIED この値は列挙型の一つ以上の定数の組み合わせになります.すべての値の組み合わせは意味をなしません. 空の配列を指定する事は構文的には可能で, この場合はUNSPECIFIEDが与えられたものとして扱うべきです.
replacement() String[]   このメソッドで返される文字列は非推奨APIを置き換えるAPIへのリンク. それぞれの文字列はJavadocタグの@linkと同じフォーマットにするべきです.
since() String   この文字列はAPIが非推奨になった時のリリースナンバーです.構文は自由ですが,リリースナンバリングはJavadocタグの@sinceのスキームと同じにするべきです. この値がJavadocの@sinceタグと冗長ではないことに注意してください. なぜなら,@sinceタグはそのAPIが導入されたリリースが記録されているのに対し,@Deprecatedアノテーションのsince()メソッドが返す値はそのAPIが非推奨になったリリースを記録しているからです.

列挙型Reasonは以下の要素を持ちます.

要素名 説明
UNSPECIFIED このAPIは他の理由で非推奨になりました. これはデフォルト値です. 今日,暗に非推奨とされているすべてのAPIの非推奨理由はUNSPECIFIEDです.
CONDEMNED このAPIは将来のJDKリリースで削除される予定です.
DANGEROUS このAPIを使用すると,データロスやデッドロック,セキュリティ上の脆弱性,不正確な結果,JVMの正確性の欠如といった結果を招く可能性があります.
OBSOLETE このAPIはもはや必要ではなく,このAPIの使用は削除されるべきです. 置き換え可能なAPIは存在しません. OBSOLETEなAPIはCONDEMNEDとしてマークされるかもしれませんし,されないかもしれません.
SUPERSEDED このAPIは新しいAPIで置き換えられており,このAPIの使用はこのAPIから新しいAPIに変更されるべきです. OBSOLETEなAPIはCONDEMNEDとしてマークされるかもしれません.
UNIMPLEMENTED このAPIの呼び出しは意味がないか常に例外が投げられます.
EXPERIMENTAL このAPIは仕様で策定されておらず,いつか互換性の無い形で変更されるか,削除されるかもしれません.

JDK内にある既存の@Deprecatedの使用もアップデートされます.
例えば,Thread#destroyとThread#stop(Throwable)の@Deprecatedについて,reasonが{UNIMPLEMENTED, CONDEMNED}となるように修正することが提案されています.
また,HashTableなどに新たに@Deprecated注釈が行われる事も提案されています.

アプリケーションが使用している非推奨APIを実行時にログとして出力する

非推奨APIの使用についての警告を発する方法が新たに提供されます.
例えば,非推奨クラスが読み込まれた際に警告を発するための"-verbose:deprecation"といったコマンドラインオプションです.
実行時にライブラリのコードから,この設定を行えるようにすることも提案されています.

警告文はgrepなどのツールで処理しやすい様な文章になる予定ですが,実際にどのような文章になるかはまだ決まっていません.

理由(列挙型Reason)ごとに,警告が発せられるかどうかを設定できる様になるらしいです.

アプリケーションで使用している非推奨APIの情報を静的に生成するツールを提供する

開発者がJARファイル(やクラスファイルの他のコンテナ)内で非推奨なクラスやメンバーの使用があるかを調査できる静的解析ツールが提供されるらしいです.
ツールとしてはjdepsに機能が似ているため,それを拡張したものになるようです.

機能のライフサイクルに関するJava SEとJDKのポリシーを明確化する

これについてはまだあまり決まっていないようです.
結構この辺りで揉めそう・・・

その他

Javadoc内で非推奨なAPIがわかりやすく表示される様にするという変更も考えられています.

JavaFXを直接実行できるjshellを作った

この記事はJavaFX Advent Calendar 2015の6日目の記事です.
昨日はy_q1mさんの「JavaFX と ScriptEngine を組み合わせた学習用アプリケーションを作る - Qiita」でした.
明日はaoetkさんです.

まえがき

現在JDK9に向けてOpenJDKではREPLツールであるjshellが作られています.
jshellでは簡単にJavaのプログラム要素を実行させることが出来ます.

そこで・・・

まぁ,そうなりますよね.
JavaFXをjshellから叩きたくなる.

ただ,残念ながら今のjshellからJavaFXのウィンドウを出そうとすると・・・

JavaFXが初期化,スタートアップされていない旨の例外が出てしまいます.
JavaFXの初期化,スタートアップにはいくつかの方法があります.
そのうちの一つに,com.sun.javafx.application.PlatformImpl#startupを使用する方法があります*1
Java9からはこのメソッドが公開メソッドになり,javafx.application.Platform#startupというメソッドからアクセスできるようになるらしいです*2
まだ,公開メソッドになっていなかったので,ここではPlatformImpl#startupを使用します.

JavaFXを初期化,スタートアップすれば実行できるのかというと・・・

ご覧の通り,jshellでコードを実行するスレッドがmainのため,JavaFXのスレッドじゃないぞと怒られてしまいます.

Platform#runLaterを用いればJavaFXのスレッドで実行させることが出来るため,jshellからJavaFXのコードを実行させることが出来ます.

変数宣言とウィンドウやコントロールのnewを同時に出来ないし,かなりうざいです.
それに毎回毎回ボイラープレートなPlatform.runLater(()->...)を書きたくないです

これに対して,mike_neckさんは外部にホックするプログラムを書くことによってもう少し綺麗に書けるようにしています.
KullaでJavaFXの操作を対話的にやってみる - mike-neckのブログ
ですが,これはこれで余計なボイラープレートが発生してしまっています.

本題

そこで,普通に書いてJavaFXのスレッドで実行させる様にjshell自体を改造しました.

その名もjfxshellです.
リポジトリは以下にあります.
Bitbucket
コミットは以下です.
*3 *4
bitter_fox / jfxshell / commit / fa3505467601 — Bitbucket


こちらからjarをダウンロードできます.
JDK9を用いて,「java -jar jfxshell.jar」として実行してください.

以下のように直感的にJavaFXのコードを実行させることが出来ます.
ちなみに,JavaFXのパッケージを一通り最初にimportするようにしているため,JavaFXのクラスはimportせずに使用することが出来ます.

素敵ですね.

実装

jshellは入力に応じて,Javaのコードとして実行できるように,ラップを行います.
例えば,「System.out.println("HelloWorld")」という入力はクラスやメソッドでラップされて以下のようになります*5

public class $REPL17 {
    public static Object do_it$() throws Throwable {
        System.out.println("HelloWorld");
        return null;
    }
}

jfxshellではこれをPlatform.runLaterでラップする様にしています.
上記のコードはjfxshellでは以下のようになります.

class $REPL14 {
    public static Object do_it$() throws Throwable {
        java.util.concurrent.CountDownLatch $$$cdl = new java.util.concurrent.CountDownLatch(1);
        com.sun.javafx.application.PlatformImpl.startup(() -> {});
        javafx.application.Platform.setImplicitExit(false);
        Throwable[] $$$thrown = {null};
        javafx.application.Platform.runLater(() -> {
            try {
                System.out.println();

            } catch (Throwable e) {
                $$$thrown[0] = e;
            } finally {
                $$$cdl.countDown();
            }
        });
        $$$cdl.await();
        if ($$$thrown[0] != null) {
            throw $$$thrown[0];
        }
        return null;
    }
}

statupは最初の一回だけ呼びだせば良いのですが,面倒くさいので毎回呼ぶように合わせてラップしています.
また,Stageが閉じられた際にJavaFXのシステムが終了しないように,毎回,setImplicitExit(false)を呼び出しています.
最後に同期を取るために,CountDownLatchを使っています.
これは,com.sun.javafx.application.PlatformImpl.runAndWaitとして同様の機能が実装されており,このメソッドがPlatform.runAndWaitとして公開されれば,そのメソッドで置き換える予定です.
その他,例外処理とか色々しています.

Let's enjoy JavaFX with jfxshell!!

*1:他にも,JFXPanelのインスタンス化などの方法もありますが,内部でこのメソッドを呼んでいます

*2:JavaOneで言っていた

*3:javaが先にJDKが持っているjdk.jshell,jdk.internal.jshellパッケージを読み込んでしまうので,それらに既にあるクラスを書き換えても反映されなかったために,色々面倒臭い事をしている><.もしも自分の実装を先に読ませる,あるいは上書きさせる方法があったら教えてください・・・クラスローダを自前で書くしか無い?

*4:/Xbootclasspathを指定するとシステム標準で読み込まれるクラスを指定できるけど,JDK9からは/Xbootclasspathはサポートされなくなるし,唯一残る/Xbootclasspath/aで指定してもNoClassDefFoundErrorが出てしまう

*5:importは省略

Collectors#filteringが欲しくなる件

以前,@cero_tさんが書いたブログで「部署をキーにしたMapを作ったうえで、Mapの値のほうには給与が1000を超える社員の人数を入れています。フィルタ処理を入れたいがために、ただの集計処理が使えず、Streamを利用しています。」ということがあって,それなりに綺麗に書ける方法を提案しました.

大本がこのコード.

Map<Dept, Long> groupByDeptAndFilter2(List<Emp> list) {
    return list.stream()
            .collect(Collectors.groupingBy(emp -> emp.dept))
            .entrySet()
            .stream()
            .collect(Collectors.toMap(entry -> entry.getKey(),
                    entry -> entry.getValue()
                            .stream()
                            .filter(emp -> emp.sal > 1000)
                            .count()));
}

で,提案したコードが以下.

Map<Dept, Long> groupByDeptAndFilter1(List<Emp> list) {
    return list.stream()
            .collect(Collectors.groupingBy(emp -> emp.dept, Collectors.collectingAndThen(Collectors.toList(),
                    emps -> emps.stream()
                            .filter(e -> e.sal > 1000)
                            .count())));
}

提案したコードのほうが短くなっていて,比較的シンプルなのだけど,どちらのコードも一回Listを作ってる辺りで,凄くダサいし,メモリとか性能的にもよくない.

そもそもフィルタリングしたいので,Collectors#mappingのようにfilteringしたCollectorを作ってくれる奴がいればもっとクールに書けるのではという話.

そこでfilteringを以下のように定義する.

    public static <T, A, R> Collector<T, ?, R> filtering(Predicate<? super T> predicate, Collector<? super T, A, R> collector) {
        BiConsumer<A, ? super T> downstream = collector.accumulator();
        return Collector.<T, A, R>of(collector.supplier(),
                (a, t) -> {if (predicate.test(t)) downstream.accept(a, t);},
                collector.combiner(), collector.finisher(),
                collector.characteristics().stream().toArray(Collector.Characteristics[]::new));
    }

条件にあった時にしかaccumulateしないようにしたCollectorを生成している.

今回の場合はこのfilteringをcountingを用いると以下のように書ける.

Map<Dept, Long> groupByDeptAndFilter(List<Emp> list) {
    return list.stream()
            .collect(groupingBy(emp -> emp.dept, filtering(emp -> emp.sal > 1000, counting())));
}

何気にこれが正解な気がする.
http://ideone.com/83Lzb7

問題はfilteringがCollectorsにないっていうこと.
誰かOpenJDKに提案して←
提案してみた
雰囲気良さそう



行けちゃう.
現状ではこれが一番よさ気

Map<Dept, Long> groupByDeptAndFilter(List<Emp> list) {
    return list.stream()
            .collect(toMap(e -> e.dept, e -> e.sal > 1000 ? 1L : 0L, Long::sum));
}

http://ideone.com/lY0Pkz

filteringを提案してしまったけど,どうしようw


filtering,マージされた