Java8がリリースされて結構な期間が過ぎました。実行環境が全てJava8ならいいのですが、世の中色々な制約がありますのでJava7やJava6が実行環境になっている場合も多々あります。
その実行環境で動作するJavaアプリケーションを開発する側は当然のようにJava6やJava7の開発環境でコードを書く必要が出てきます。
そうなると、Java8から導入されたラムダ式(や Stream API)の恩恵に預かることはできません。
そんな悲しい思いをしている時にはRetrolambda
の出番です。
ラムダ式で書かれたJava8用のソースコードからJava7やJava6で動作するモジュールを生成してくれる神様のようなツールです。
素敵なRetrolambda
、これはもう使うしかありませんね。
Javaのラムダ式はStream APIと組み合わせることにより大きな力を発揮する部分が多いです。
が、今回紹介しているRetrolambda
はラムダ式などの解釈はうまくやってくれますがStream APIを提供してくれるものではありません。
そのためStream APIを利用したコードを書いてしまうと、Java6やJava7環境では(Stream APIのクラスやメソッドがないので)動作しません。
この問題を解決するため、今回はLightweight-Stream-API
を組み合わせます。
Lightweight-Stream-API
は、Java標準のStream APIと同等(むしろそれ以上)の機能を利用することができるようになります。
これでラムダ式+Stream APIを用いてJava6やJava7の開発を行うことができるようになります。
開発環境の構築
本エントリでは、Eclipse + Maven で開発環境を構築してサンプルコードを記述しています
プログラムをビルドする環境のイメージ
Retrolambda
を使った開発を行う場合、
Java8用のコードがコンパイル可能な状況でソースコードを記述する
↓
Retrolambda
を介在させて、Java7(またはJava6)用のjarを生成する
というイメージの開発の流れになります。
Java8用のコードを記述するための設定
Java8用の開発環境を構築するためにpom.xmlに以下の記述をします
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins>
Retrolambdaに関する設定
開発するためのプロジェクトにおいてpom.xmlにRetrolambda
に関する以下の記述が必要となります。
<plugins> <plugin> <groupId>net.orfjackal.retrolambda</groupId> <artifactId>retrolambda-maven-plugin</artifactId> <version>2.3.0</version> <executions> <execution> <goals> <goal>process-main</goal> <goal>process-test</goal> </goals> </execution> </executions> <configuration> <target>1.6</target> </configuration> </plugin> </plugins>
configuration要素配下のtargetに最終的に実行するJava環境に合わせた値を設定する必要があります。
- Java6の場合 ⇒ 1.6
- Java7の場合 ⇒ 1.7
Lightweight-Stream-APIに関する設定
今回の開発ではStream APIも使用したいのでpom.xmlにLightweight-Stream-API
に関する以下内容を追記します
<dependency> <groupId>com.annimon</groupId> <artifactId>stream</artifactId> <version>1.1.3</version> </dependency>
pom.xmlの全体
私がEclipse上で試した際のpom.xmlは以下のようになりました(必要な部分を抜粋しています)
<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"> <dependencies> <dependency> <groupId>com.annimon</groupId> <artifactId>stream</artifactId> <version>1.1.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>net.orfjackal.retrolambda</groupId> <artifactId>retrolambda-maven-plugin</artifactId> <version>2.3.0</version> <executions> <execution> <goals> <goal>process-main</goal> <goal>process-test</goal> </goals> </execution> </executions> <configuration> <target>1.6</target> </configuration> </plugin> </plugins> <pluginManagement> <plugins> <plugin> <groupId>org.eclipse.m2e</groupId> <artifactId>lifecycle-mapping</artifactId> <version>1.0.0</version> <configuration> <lifecycleMappingMetadata> <pluginExecutions> <pluginExecution> <pluginExecutionFilter> <groupId>net.orfjackal.retrolambda</groupId> <artifactId>retrolambda-maven-plugin</artifactId> <versionRange>[2.3.0,)</versionRange> <goals> <goal>process-test</goal> <goal>process-main</goal> </goals> </pluginExecutionFilter> <action> <ignore></ignore> </action> </pluginExecution> </pluginExecutions> </lifecycleMappingMetadata> </configuration> </plugin> </plugins> </pluginManagement> </build> </project>
ビルド方法
通常のMavenプロジェクトと同様にmvn clean ⇒ mvn install の実行などでビルドしてください。
これで自動的にラムダ式などで書かれたコードであっても、Java6やJava7で実行できるモジュールが生成されます。
ちなみに、Mavenでのビルドを行うと以下のようなログがコンソールに出力されます
[INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building sample.lambda 0.0.1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ sample.lambda --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory C:\work\sample.lambda\src\main\resources [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ sample.lambda --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- retrolambda-maven-plugin:2.3.0:process-main (default) @ sample.lambda --- [INFO] Processing classes with Retrolambda Bytecode version: 50 (Java 6) Default methods: false Input directory: C:\work\sample.lambda\target\classes Output directory: C:\work\sample.lambda\target\classes Classpath: [C:\work\sample.lambda\target\classes, C:\Users\yukiv\.m2\repository\com\annimon\stream\1.1.3\stream-1.1.3.jar] Included files: all Agent enabled: false WARNING: The interface org/sample/lambda/ng/SampleInteface has a default method "defaultmedhod" but backporting default methods is not enabled [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ sample.lambda --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory C:\work\sample.lambda\src\test\resources [INFO] [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ sample.lambda --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- retrolambda-maven-plugin:2.3.0:process-test (default) @ sample.lambda --- [INFO] Processing classes with Retrolambda Bytecode version: 50 (Java 6) Default methods: false Input directory: C:\work\sample.lambda\target\test-classes Output directory: C:\work\sample.lambda\target\test-classes Classpath: [C:\work\sample.lambda\target\test-classes, C:\work\sample.lambda\target\classes, C:\Users\yukiv\.m2\repository\junit\junit\3.8.1\junit-3.8.1.jar, C:\Users\yukiv\.m2\repository\com\annimon\stream\1.1.3\stream-1.1.3.jar] Included files: all Agent enabled: false [INFO] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ sample.lambda --- [INFO] [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ sample.lambda --- [INFO] Building jar: C:\work\sample.lambda\target\sample.lambda-0.0.1.jar [INFO] [INFO] --- maven-install-plugin:2.4:install (default-install) @ sample.lambda --- [INFO] Installing C:\work\sample.lambda\target\sample.lambda-0.0.1.jar to C:\Users\yukiv\.m2\repository\org\sample\sample.lambda\0.0.1\sample.lambda-0.0.1.jar [INFO] Installing C:\work\sample.lambda\pom.xml to C:\Users\yukiv\.m2\repository\org\sample\sample.lambda\0.0.1\sample.lambda-0.0.1.pom [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.331 s [INFO] Finished at: 2016-11-04T19:51:38+09:00 [INFO] Final Memory: 14M/114M [INFO] ------------------------------------------------------------------------
出力内容に Bytecode version: 50 (Java 6)
という部分があり、Java6用のモジュールを生成していることが分かります。
Retrolambdaを用いた開発における制約
Retrolambda
を利用することでラムダ式などを利用することがOKとなりますが、最終的に実行する環境がJava8ではなくJava6やJava7であるためいくつかの制約が存在しています。
制約事項の前に、OKなこととしては以下が挙げられます
- ラムダ式を用いたコードを書く
- メソッド参照を使用する
制約事項としては以下があります
- Java標準のStream API(
java.util.stream
パッケージ)を使用できない - CollectionのstreamメソッドやforEachメソッドは使用できない
- Java8から増えたクラスやメソッドは使用できない(Optional, LongAdder, LocalDateTime...etc)
- interfaceのデフォルト実装(defaultメソッド)は使用できない
- interfaceに定義したstaticメソッドは使用できない
- Java標準のStream API(
制約事項に書かれている内容を守らなくても、開発はJava8の環境で実施するためコンパイル等はできてしまいます。が、実行環境でエラーとなります。
Stream APIに関する制約については、Lightweight-Stream-API
を使用することで解決(代替)します。
Stream API以外でJava8から増えたクラスを使用したい場合は、対応するBackportのライブラリで代替できる場合もあります。
(例: ThreeTen Backport)
ラムダ式を用いたコードを書いてJava6環境で実行してみる
ラムダ式、メソッド参照、Lightweight-Stream-API
が提供するStream APIを利用したコードを記述して(ビルドをして)、Java6環境で実行してみます
ラムダ式+メソッド参照+Stream APIのコード
■サンプルコード
ラムダ式、メソッド参照、(Lightweight-Stream-API
で代替した)Stream APIを用いた各種メソッドを記述しています。
(制約事項に引っかからないようにjava.util.stream
パッケージに依存していません。代わりに、com.annimon.stream
パッケージに依存するようにしています)
import java.util.List; import java.util.Comparator; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; public class SampleAPI1 { private static final Comparator<String> LENGTH_COMPARATOR = (x, y) -> x.length() - y.length(); public static void filterLength3(List<String> srcList) { Stream.of(srcList) .filter(s -> s.length() > 3) .forEach(System.out::println); } public static List<String> filterLength2toList(List<String> srcList) { List<String> list = Stream.of(srcList) .filter(s -> s.length() > 2) .collect(Collectors.toList()); return list; } public static List<String> filterArrayLength2toList(String[] srcArray) { List<String> list = Stream.of(srcArray) .filter(s -> s.length() > 2) .collect(Collectors.toList()); return list; } public static List<Integer> toLengthList(List<String> srcList) { List<Integer> list = Stream.of(srcList) .map(s -> s.length()) .sorted() .collect(Collectors.toList()); return list; } public static int sumWordLength(List<String> srcList) { int sumLength = Stream.of(srcList) .map(s -> s.length()) .reduce(0, (sum, i) -> sum + i); return sumLength; } public static List<String> sort1(List<String> srcList) { List<String> tmpList = new ArrayList<>(srcList); Collections.sort(tmpList, LENGTH_COMPARATOR); return tmpList; } public static List<String> sort2(List<String> srcList) { List<String> tmpList = new ArrayList<>(srcList); // 比較関数をラムダ式で記述して直接sortに渡してソート実行 Collections.sort(tmpList, (x, y) -> x.length() - y.length()); return tmpList; } }
■実行側のコード
Java6環境で実行するコードを書いています
import java.util.Arrays; import java.util.List; import org.sample.lambda.SampleAPI1; public class ExampleLauncher { public static void main(String[] args) { System.out.println("################################"); System.out.println("Runtime Java Version: " + System.getProperty("java.version")); System.out.println("################################"); List<String> srcList = Arrays.asList("zzzzzzzz", "abc", "abcde", "defg", "x", "xyz", "opqrs"); System.out.println("\n>> invoke SampleAPI1#filterLength3"); SampleAPI1.filterLength3(srcList); System.out.println("\n>> invoke SampleAPI1#filterLength2toList"); List<String> destList = SampleAPI1.filterLength2toList(srcList); System.out.println("length > 2 :" + destList); System.out.println("\n>> invoke SampleAPI1#toLengthList"); List<Integer> lengthList = SampleAPI1.toLengthList(srcList); System.out.println("lengthList: " + lengthList); System.out.println("\n>> invoke SampleAPI1#sumWordLength"); int sumWordLength = SampleAPI1.sumWordLength(srcList); System.out.println("sumWordLength: " + sumWordLength); String[] srcArray = new String[] {"zzzzzzzz", "abc", "abcde", "defg", "x", "xyz", "opqrs"}; System.out.println("\n>> invoke SampleAPI1#filterArrayLength2toList"); List<String> destList2 = SampleAPI1.filterArrayLength2toList(srcArray); System.out.println("destList2: " + destList2); System.out.println("\n>> invoke SampleAPI1#sort1"); List<String> sortedList1 = SampleAPI1.sort1(srcList); System.out.println("src List: " + srcList); System.out.println("sroted List: " + sortedList1); System.out.println("\n>> invoke SampleAPI1#sort2"); List<String> sortedList2 = SampleAPI1.sort2(srcList); System.out.println("src List: " + srcList); System.out.println("sroted List: " + sortedList2); } }
■実行結果
Java6環境で実行した結果です。(実行環境のクラスパスにはLightweight-Stream-API
のjarを追加しています)
################################ Runtime Java Version: 1.6.0_45 ################################ >> invoke SampleAPI1#filterLength3 zzzzzzzz abcde defg opqrs >> invoke SampleAPI1#filterLength2toList length > 2 :[zzzzzzzz, abc, abcde, defg, xyz, opqrs] >> invoke SampleAPI1#toLengthList lengthList: [1, 3, 3, 4, 5, 5, 8] >> invoke SampleAPI1#sumWordLength sumWordLength: 29 >> invoke SampleAPI1#filterArrayLength2toList destList2: [zzzzzzzz, abc, abcde, defg, xyz, opqrs] >> invoke SampleAPI1#sort1 src List: [zzzzzzzz, abc, abcde, defg, x, xyz, opqrs] sroted List: [x, abc, xyz, defg, abcde, opqrs, zzzzzzzz] >> invoke SampleAPI1#sort2 src List: [zzzzzzzz, abc, abcde, defg, x, xyz, opqrs] sroted List: [x, abc, xyz, defg, abcde, opqrs, zzzzzzzz]
もともとラムダ式やメソッド参照を用いたコードだったにも関わらず、Java6環境で正常に実行できていることが分かります。
制約事項にひっかかるコードは実行環境でエラーになる
Retrolambda
の開発における制約事項を無視したコードを記述した場合、実行環境ではエラーとなります。
以下は、それを実際に試したサンプルです
defaultメソッドの利用した場合
defaultメソッド(interfaceのデフォルト実装)が絡んだコードを書くと実行環境においてエラーとなります
■サンプルコード
public interface SampleInteface { void notDefaultmedhod(); default void defaultmedhod() { System.out.println("execute SampleInteface#defaultmedhod"); } } // デフォルトメソッドを実装クラスでオーバーライドしない public class SampleImpl1 implements SampleInteface { @Override public void notDefaultmedhod() { System.out.println("[override] execute SampleImpl1#notDefaultmedhod"); } } // デフォルトメソッドを実装クラスでオーバーライドする public class SampleImpl2 implements SampleInteface { @Override public void notDefaultmedhod() { System.out.println("[override] execute SampleImpl2#notDefaultmedhod"); } @Override public void defaultmedhod() { System.out.println("[override] execute SampleImpl2#defaultmedhod"); } }
■利用側のコード
(1) defaultメソッドを実装クラスでオーバーライドしていない場合
import org.sample.lambda.ng.SampleImpl1; import org.sample.lambda.ng.SampleInteface; public class NGInterfaceLauncher3 { public static void main(String[] args) { SampleInteface api = new SampleImpl1(); System.out.println("\n>> invoke SampleInteface#notDefaultmedhod"); api.notDefaultmedhod(); System.out.println("\n>> invoke SampleInteface#defaultmedhod"); api.defaultmedhod(); } }
(2) defaultメソッドを実装クラスでオーバーライドしている場合
import org.sample.lambda.ng.SampleImpl2; import org.sample.lambda.ng.SampleInteface; public class NGInterfaceLauncher4 { public static void main(String[] args) { SampleInteface api = new SampleImpl2(); System.out.println("\n>> invoke SampleInteface#notDefaultmedhod"); api.notDefaultmedhod(); System.out.println("\n>> invoke SampleInteface#defaultmedhod"); api.defaultmedhod(); } }
■実行結果
結果は以下の通りですが、クラスロードの時点でClassFormatErrorが発生してしまいます。
(1) の結果
java.lang.ClassFormatError: Method defaultmedhod in class org/sample/lambda/ng/SampleInteface has illegal modifiers: 0x1 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631) at java.lang.ClassLoader.defineClass(ClassLoader.java:615) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141) at java.net.URLClassLoader.defineClass(URLClassLoader.java:283) at java.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) Exception in thread "main"
(2) の結果
java.lang.ClassFormatError: Method defaultmedhod in class org/sample/lambda/ng/SampleInteface has illegal modifiers: 0x1 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631) at java.lang.ClassLoader.defineClass(ClassLoader.java:615) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141) at java.net.URLClassLoader.defineClass(URLClassLoader.java:283) at java.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) Exception in thread "main"
interfaceクラスのstaticメソッド
interfaceクラスにstaticメソッドを定義した場合、Java7やJava6の環境ではそのstaticメソッドを実行するコードを書くことができません(コンパイルが通りません)
■サンプルコード インタフェースクラスにstaticメソッドを定義しています。
public interface StaticInteface { public static void staticMedhod1() { System.out.println("SampleInteface#staticMedhod1"); } void method1(); }
■利用側のコード
Java6環境で対象のstaticメソッドを使用するコードを書くことはできません(コンパイルが通りません)
import org.sample.lambda.ng.StaticInteface; public class NGStaticInterfaceLauncher1 { public static void main(String[] args) { // StaticInteface.staticMedhod1(); <- コンパイルが通らない } }
Stream APIを使用した場合
Java標準のStream API(java.util.stream
パッケージ)を利用したコードを書くと実行環境においてエラーとなります
■サンプルコード
以下では、java.util.stream.Streamやjava.util.stream.Collectors, Iterable#forEach 等に依存したコードを記述しています
import java.util.List; public class NGSampleAPI1 { public static void ngPattern1(List<String> srcList) { srcList.stream() .filter(s -> s.length() > 3) .forEach(System.out::println); } public static void ngPattern2(List<String> srcList) { srcList.forEach(s -> System.out.println(s + ": " + s.length())); } public static List<String> ngPattern3(String[] srcArray) { List<String> list = java.util.stream.Stream.of(srcArray) .filter(s -> s.length() > 2) .collect(java.util.stream.Collectors.toList()); return list; } }
■実行結果
上記の各メソッドをJava6環境で実行すると、メソッド実行時に以下のような例外がスローされてエラーとなります。
################################ Runtime Java Version: 1.6.0_45 ################################ >> invoke NGSampleAPI1#ngPattern1 Exception in thread "main" java.lang.NoSuchMethodError: java.util.List.stream()Ljava/util/stream/Stream; at org.sample.lambda.ng.NGSampleAPI1.ngPattern1(NGSampleAPI1.java:9) at org.sample.NGExampleLauncher1.main(NGExampleLauncher1.java:20) >> invoke NGSampleAPI1#ngPattern2 Exception in thread "main" java.lang.NoClassDefFoundError: java/util/function/Consumer at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631) at java.lang.ClassLoader.defineClass(ClassLoader.java:615) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141) at java.net.URLClassLoader.defineClass(URLClassLoader.java:283) at java.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) at org.sample.lambda.ng.NGSampleAPI1.ngPattern2(NGSampleAPI1.java:15) at org.sample.NGExampleLauncher2.main(NGExampleLauncher2.java:20) Caused by: java.lang.ClassNotFoundException: java.util.function.Consumer at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ... 14 more >> invoke NGSampleAPI1#ngPattern3 Exception in thread "main" java.lang.NoClassDefFoundError: java/util/stream/Stream at org.sample.lambda.ng.NGSampleAPI1.ngPattern3(NGSampleAPI1.java:20) at org.sample.NGExampleLauncher3.main(NGExampleLauncher3.java:17) Caused by: java.lang.ClassNotFoundException: java.util.stream.Stream at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ... 2 more
まとめ
Retrolambda
を利用することでラムダ式やメソッド参照を使用したコードであっても、Java6やJava7で動作するモジュールを作成することが可能です。
ただし、全てのJava8の機能を使えるわけではなく一部制約もあり、それを理解した上で利用する必要があります。
また、Retrolambda
自体が提供しないStream APIも使用したい場合は、Lightweight-Stream-API
等を組み合わせて使用する必要があります。