Java8 Method Reference
Java8からLambdaと一緒にMethod Referenceも導入されました。
これにより、今までアノニマスクラスで書いていたものが、LambdaやMethod Referenceで書くことができるようになりました。
例えば、Comparatorの実装は、
Comparator<String> comparator = String::compareTo;
と書くことができるようになりました。
ここで、気になるのは、Method Referenceでは、どんなものがあって、どういう風に書けるのかということ。
Method Referenceには大きく分けて、4種類のものがあるようです。
- スタティックメソッドのMethod Reference
- 任意の型指定によるインスタンスメソッドのMethod Reference
- オブジェクト指定のインスタンスメソッドのMethod Reference
- コンストラクタのMethod Reference
書き方については、4種類すべて同じで、
ClassName::methodName
という書き方をしますが、4種類それぞれで、引数とメソッドが呼び出される形式が違います。
具体的にはそれぞれ、以下のような使い方をします。
スタティックメソッドのMethod Reference
引数がそのまま、スタティックメソッドの引数に割り当てられます。
これに対応するLambdaは以下のようになります。
(arg) -> ClassName.staticMethod(arg);
任意の型指定によるインスタンスメソッドのMethod Reference
第一引数に対して、Method Referenceで与えたメソッドが呼び出される形となります。
そのため、第一引数とMethod Referenceの型が一致しない場合はコンパイルエラーになります。
残りの引数は、インスタンスメソッドの引数として渡されます。
対応するLambdaは以下のようになります。
(arg1, arg2, arg3) -> arg1.instanceMethod(arg2, arg3);
ちなみに、親クラスを指定した場合は、
super::methodName
と書くことができます。
また、ジェネリクスなどで型を明示的に指定したい場合は
ClassName::<Type>methodName
と書くこともできます。
オブジェクト指定のインスタンスメソッドのMethod Reference
こちらは、スタティックメソッドのMethod Referenceと同様に、
すべての引数がそのままインスタンスメソッドの引数に割り当てられます。
これに対応するLambdaは以下のようになります。
CustomClass instance = new CustomClass(); (arg1, arg2) -> instance.instanceMethod(arg1, arg2);
ここで、Method ReferenceとLambdaの違いは、Lambdaで実行する場合、
instanceはfinalもしくはeffectively finalである必要がありますが、Method Referenceではそのような制約はないということです。
その他、以下のような操作も書くことができます。
Supplier supplier = (list.size() > 10 ? list.subList(0,10) : list)::iterator;
ちょっと読み難いですが、ちゃんと動きます。
コンストラクタのMethod Reference
これも基本的には、スタティクメソッドの場合と同じです。
すべての引数がそのままコンストラクタの引数に割り当てられます。
ただし、注意が必要なのは、メソッド名としてnewを使う点です。
そのため、コンストラクタのMethod Referenceだけは以下のような形になります。
ClassName::new;
ちょっと触ってみた感想としては、Lambdaに比べて、引数の関係が直感的でないせいか、
読むのはいいですが、書くのに最初は苦労する感じがしました。(慣れれば問題なさそう)
ただ、便利だとは思うので、可読性を下げない程度に使っていきたいと思います。
Java8 Lambdaの型推論
Java8からLambdaが書けるようになって、いろいろと簡単に書くことができるようになったので、
その型推論について勉強してみた。
JavaのLambdaではFunctional Interfaceの実装をLambdaで与えることができる。
例えば、Comparatorの実装に
Comparator<String> comparator = (String a, String b) -> a.compareTo(b)
などと書けるようになった。
そのため、Listのソートなどでは、
list.sort((String a, String b) -> a.compareTo(b));
と書くことで、ソートされる。
ここで、記述したLambdaがどのFunctional Interfaceの実装かの型推論がコンパイル時に行なわれている。
実際には、以下のような手順で型推論が行なわれているようだ。
- 呼び出したメソッド定義を確認(例では、List.sortのメソッド定義)
- メソッド定義からLambdaはComparator
の実装であることがわかる - Comparator
はString型の引数を二つ受け取り、int型の値を返すメソッドの実装をする必要がある - ここで、引数に渡したLambdaに戻って、引数の型/個数・返り値の確認が行われる
- Lambdaでは、引数の型は省略できるので、省略している場合は個数の確認が行われる
最後の項目で、引数の型/個数・返り値に問題がなければコンパイルに成功し、
何処かで型が一意に決まらなければ、コンパイルエラーになる。
例えば、以下のような三種類のメソッド定義をしたクラスを準備して、
Lambdaをsortの引数に指定した場合、01, 02は同じ型/個数の引数を取り、
同じ型の返り値を返すため、コンパイルエラーになる。
これは、型推論をした結果、よび出すメソッドが一意に特定できないためである。
※03は返り値の型が他のものと異なるので、型推論可能
01, Comparatorを引数に渡す場合
public void sort(List<String> list, Comparator<String> c1) { list.sort(c1); }
02, ToIntBiFunctionを引数に渡す場合
public void sort(List<String> list, ToIntBiFunction<String, String> biFunction) { Comparator<String> comparator = (String a, String b) -> biFunction.applyAsInt(a, b); list.sort(comparator); }
03, BiFunctionを引数に渡す場合
public void sort(List<String> list, BiFunction<String, String, String> biFunction) { Comparator<String> comparator = (String a, String b) -> Integer.valueOf(biFunction.apply(a, b)); list.sort(comparator); }
また、型推論の祭に、auto boxingなどは考慮されないので、
Comparator<Integer> c1 = (int a1, int a2) -> { if (a1 < a2) return 1; else if (a1 == a2) return 0; return -1; };
などと書くとコンパイルエラーになる。
ちなみに、この場合、引数の型を省略していれば、メソッドの処理部分ではauto boxingされるので、
コンパイルエラーにも実行時エラーにもならずに正しく動いてくれる。
# 省略してもいいところを省略することは変なミスを防ぐ結果にもなるということ
次は、メソッドリファレンスの話を書こうと思う。
MavenでCheckerFrameworkを使う
前の記事では、IntelliJでCheckerFrameworkを使う方法を書きましたが、本記事では、MavenでCheckerFrameworkを使って書かれたコードをコンパイルする方法を書きます。
すでに、Maven Centralに必要なjarは上がっていますので、特に難しいことはなく、pom.xmlをきちんと書いてあげればイイだけです。
前回の記事と同様、The Checker Framework Manual: Custom pluggable types for Javaにmavenでの使い方も書いてありますが、英語しかないので、ざっくり備忘録も兼ねて書いておきます。
JDK8からアノテーションの利用できる場所が変更になってこともあり、CheckFramework上でのアノテーションの書き方がJDK8とJDK7以前とでは違います。
そのため、JDK7以前のソースコードに対して、CheckerFrameworkを利用する場合は、タイプアノテーションコンパイラを利用してください。
具体的な手順は以下のとおりです。
1, 必要な依存関係を追加します
<!-- annotations from the Checker Framework: nullness, interning, locking, ... --> <dependency> <groupId>org.checkerframework</groupId> <artifactId>checker-qual</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.checkerframework</groupId> <artifactId>checker</artifactId> <version>2.0.0</version> </dependency> <!-- タイプアノテーションコパイラを利用する場合は以下も使う --> <!-- <dependency> <groupId>org.checkerframework</groupId> <artifactId>compiler</artifactId> <version>2.0.0</version> </dependency> --> <dependency> <groupId>org.checkerframework</groupId> <artifactId>jdk8</artifactId> <version>2.0.0</version> </dependency>
2, Mavenで使うプロパティを設定します
<properties> <!-- These properties will be set by the Maven Dependency plugin --> <annotatedJdk>${org.checkerframework:jdk8:jar}</annotatedJdk> <!-- タイプアノテーションコンパイラを使いたい場合はこちらを使う --> <!-- <typeAnnotationsJavac>${org.checkerframework:compiler:jar}</typeAnnotationsJavac> --> </properties>
ここで、${org.checkerframework:jdk8:jar}はMavenのプロパティ参照なので、${CHECKERFRAMEWORK}/checker/dist/jdk8.jarが取得できるようにプロパティを設定する必要があります。もちろん、
また、maven-dependency-pluginを利用できるように以下のようにpluginの設定を追加します。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.3</version> <executions> <execution> <goals> <goal>properties</goal> </goals> </execution> </executions> </plugin>
3, maven-compiler-pluginの設定を記述します
<plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> <fork>true</fork> <annotationProcessors> <!-- 使いたいチェッカーが他にもある場合は、FQクラス名を追加します--> <annotationProcessor>org.checkerframework.checker.nullness.NullnessChecker</annotationProcessor> </annotationProcessors> <compilerArgs> <arg>-Xbootclasspath/p:${annotatedJdk}</arg> <!-- もしTypeAnnotationsJavacの値を使いたい場合はこちらを記述--> <!-- <arg>-J-Xbootclasspath/p:${typeAnnotationsJavac}</arg> --> </compilerArgs> </configuration> </plugin>
4, mavenを実行する
上記をまとめたpom.xmlとその実行結果は以下のようになります。
[pom.xml]
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.iboy</groupId> <artifactId>checkerSample</artifactId> <version>1.0-SNAPSHOT</version> <properties> <org.checkerframework>${env.CHECKERFRAMEWORK}</org.checkerframework> <annotatedJdk>${org.checkerframework}</annotatedJdk> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.3</version> <executions> <execution> <goals> <goal>properties</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf-8</encoding> <fork>true</fork> <annotationProcessors> <annotationProcessor>org.checkerframework.checker.nullness.NullnessChecker</annotationProcessor> </annotationProcessors> <compilerArgs> <arg>-Xbootclasspath/p:${annotatedJdk}</arg> </compilerArgs> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.checkerframework</groupId> <artifactId>checker-qual</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.checkerframework</groupId> <artifactId>checker</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.checkerframework</groupId> <artifactId>jdk8</artifactId> <version>2.0.0</version> </dependency> </dependencies> </project>
[実行結果]
エラーがある場合は、以下のように出力されます。
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile) on project checkerSample: Compilation failure
[ERROR] ../checkerSample/src/main/java/NullnessExample.java:[7,35] エラー: [assignment.type.incompatible] incompatible types in assignment.
[ERROR] -> [Help 1]
※Javaソースコードは前の記事と同じものを使用
IntellijでChekcerFrameworkを使ったコードのビルド
JJUG CCC 2016 SpringでThe Checker Frameworkというものがあることを知ったので、早速使ってみた。
ドキュメントが全部英語で、日本語のものがなくて辛かったので今回は、Intellijを使ってビルドできるようになるまでを書く。
(英語版の設定はThe Checker Framework Manual: Custom pluggable types for Javaを参照)
(注)以下の内容で${CHECKERFRAMEWORK}はCheckFrameworkをインストール(展開)したパスです
- IntelliJの設定画面上で利用するJDKのバージョンを8にする
- プロジェクトの設定画面を開いてライブラリに以下のjarを追加する ${CHECKERFRAMEWORK}/checker/dist/checker.jar ${CHECKERFRAMEWORK}/checker/dist/javac.jar
- 再度、IntelliJの設定画面を開き、「Javaコンパイラ」の設定メニューを開く(場所がわからない場合は設定画面で検索すると良い) ここで「追加コマンドラインパラメータ(Additional command line parameters)に-Xbootclasspath/p:${CHECKERFRAMEWORK}/checker/dist/jdk8.jar と記述する
- IntelliJ設定メニュー中の「アノテーションプロセッサ」で以下の内容をセットする 「Enable annotation processing」にチェックをつける 「Processor FQ Name」に利用するチェッカーのFQクラス名を指定する(例えば、org.checkerframework.checker.nullness.NullnessChecker)
上記を実行した上で、ビルドを行うと、CheckerFrameworkで定義されているアノテーションを利用することができます。
例えば、以下のコードを実行を書いてビルドすると、
import org.checkerframework.checker.nullness.qual.NonNull; public class NullnessExample { public static void main(String[] args) { @NonNull Object myObject = null; myObject = new Object(); System.out.println(myObject.toString()); } }
Error:(5, 36) java: [assignment.type.incompatible] incompatible types in assignment.
found : null
required: @UnknownInitialization @NonNull Object
というような形でコンパイルエラーを出してくれます。
Java SE8ではforEachよりもStramが早い!!
既に一週間以上経ち、かなり今更感がありますが、
JJUG ナイト・セミナー 「Java エンジニアのためのJava(再)入門」 6/20(木)開催 | 日本Javaユーザーグループ
に参加してきました。
その際に、@yoshioteradaさんに勧められたのをきっかけに、私もJavaやJavaScriptなど、自分の学んだことをブログにまとめようと思い、このページを作りました。
週に一回くらいは、何かしらの情報をアップしていこうと思います。
#このブログの使い方についても、少しずつマスターしていこう・・・
早速ですが、今日の話題は、JJUG ナイト・セミナー 「Java エンジニアのためのJava(再)入門」 6/20(木)開催 | 日本Javaユーザーグループで@cero_tさんからお話いただいた、from old Java to modern Javaの内容についてです。
内容的な部分は、最近Java7を使っている私としては、Java6の部分までは、特に知らないことろもなく、「ふんふん。そうだよねぇ。。。」と言う感じで聞いていました。
#いや、年齢的な意味でも、Java5でのジェネリクスの導入による衝撃なんかは知らないんだけどさ...^^;
知らないところが出てきたのは、Java7から。。。
以下、知らなかった点
・Java SE7からObjects.HashというAPIができた
・Java SE7では、Files.readAllLinesというAPIがある
の二点。(あれ?APIだけじゃね?話は文法的なところがメインじゃなかったっけ?)
そして、最も気になった部分があったのは、Java SE8の部分が終わって、質問タイムの時!
「新しい文法を古いものになれた人にどう進めればいいんでしょう?」
と言う質問に対する@skrbさんの答えの中に、
「Streamを使うだけでforEachよりも格段に早いんです!」
って話がありました。
そんなことは全く知らない私は「え?そうなの?(ぽかーん」状態。。。
ということで、前置きが長くなりましたが、本日はここを検証します。
Streamをてっとり早く検証しようと思って、以下(ページ下部に準備)のようなソースコードを準備しました。
#ほぼ@cero_tさんがスライドにあげているもののパクリ^^;
実行した結果がこちら
for Loop Time is :5612
stream Loop Time is :3335
Parallel stream Loop Time is :4510
#あ、結果の単位はすべて[msec]です!
う。。。うん。確かに、Streamを使った方がforEachより早い!
並列化した方が遅いのは、処理内容が簡単すぎて、並列化のオーバヘッドの方が大きくなっただけだろうか?
ここは少し疑問に思うところ。。。時間があるときに、見てみようと思います。
というか、そもそも、なんでStreamを使うと早いかという部分も検証すべきなんですが、今週は忙しかったので、ごめんなさい。。。
ちゃんと調べておきます。
ということで、今回は、StreamがforEachよりも早いよ!と言うことを確認してみました。
実際、確かに早かったので、forEachではなく、Streamをじゃんじゃん使っていきましょう!(Lambda普及的な意味でも^^)
次回は、Java SE8で新しくなったJava VM(Nashorn)JavaScriptエンジン(Nashorn)に関して、書いてみようと思います。
(裏話)
実は、このソースコード、実際に動かしてみると、最初はStreamの方がかなり遅いという結果がでていました。
おかしいなぁ。。。と思いつつ、一日放置して、その後、ソースコードを書き直してみた結果がこれ。
うーん。。。たぶんどっかで書き間違えしたんだろうな。。。
//実行したコード package com.iboy; import com.iboy.entity.Record; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; public class Main { public static int LIST_SIZE = 10000000; public static int SEED = 100000; /** * テストデータの作成 * @return */ private List<Record> createTestData() { Random random = new Random(SEED); List<Record> list = new ArrayList<>(); for (int i=0; i < LIST_SIZE; i++) { int tmp = random.nextInt(); Record tmpRecode = new Record(tmp); list.add(tmpRecode); } return list; } /** * 通常のforEachを用いた処理 * @param data filter&sort対象のデータ */ private void doForEachProcess(List<Record> data) { List<Record> list3 = new ArrayList<>(data); long currentTime = System.currentTimeMillis(); List<Record> tmpList = new ArrayList<>(); for (Record record : list3) { if (record.getScore() % 2 == 0) { tmpList.add(record); } } Comparator<Record> comparator = new Comparator<Record>() { @Override public int compare(Record o1, Record o2) { if (o1.getScore() < o2.getScore()) { return -1; } else if (o1.getScore() == o2.getScore()) { return 0; } return 1; } }; Collections.sort(tmpList, comparator); long endTime = System.currentTimeMillis(); System.out.println("for Loop Time is :" + (endTime - currentTime)); } /** * Streamを用いた処理 * @param data */ private void doStreamProcess(List<Record> data) { List<Record> list = new ArrayList<>(data); long currentTime = System.currentTimeMillis(); list.stream().filter(record -> record.getScore() % 2 == 0) .sorted((o1, o2) -> { if (o1.getScore() < o2.getScore()) { return -1; } else if (o1.getScore() == o2.getScore()) { return 0; } return 1; }) .collect(Collectors.toList()); long endStreamTime = System.currentTimeMillis(); System.out.println("stream Loop Time is :" + (endStreamTime - currentTime)); } /** * ParallelStreamを用いた処理 * @param data */ private void doParallelStreamProcess(List<Record> data) { List<Record> list = new ArrayList<>(data); long currentTime = System.currentTimeMillis(); List<Record> sorted = list.parallelStream().filter(record -> record.getScore() % 2 == 0) .sorted((o1, o2) -> { if (o1.getScore() < o2.getScore()) { return -1; } else if (o1.getScore() == o2.getScore()) { return 0; } return 1; }).collect(Collectors.toList()); long endParallelStreamTime = System.currentTimeMillis(); System.out.println("Parallel stream Loop Time is :" + (endParallelStreamTime - currentTime)); } public static void main(String[] args) { Main main = new Main(); List<Record> list = main.createTestData(); //Do forEach process main.doForEachProcess(list); //Do Stream process main.doStreamProcess(list); //Do parallel Stream process main.doParallelStreamProcess(list); } }