Java 8からJava 6へのStream APIの純粋に実験的な移植手法

1年前、 MavenRetrolambdaを使用して、Java 8言語ツールと関連する「Java 8ではない」ライブラリを使用してアプリケーションをAndroidに移植する方法について説明しました。 残念ながら、新しいJava 8 APIは、古いターゲットプラットフォーム上に存在しないため、使用できません。 しかし、アイデア自体は私を長い間放置していなかったので、たとえば、Stream APIを古いプラットフォームに移植し、ラムダ式のような言語の機能に制限されないようにすることは可能だろうかと思いました。







最終的に、このアイデアは次のことを意味します:前のケースのように、利用可能なツール、特に古き良きRetrolambdaを使用してStream APIバイトコードを書き換え、このAPIを使用するコードがJavaの古いバージョンで動作できるようにする必要があります。 なぜJava 6なのか? 正直なところ、私はこのバージョンのJavaでより長く作業しましたが、Java 5は見つかりませんでした。私にとってJava 7はまるで空を飛ぶようなものでした。









また、この記事で説明するすべての指示は純粋に実験的なものであり、実用的ではないことを繰り返します。 まず第一に、ブートクラスローダーを使用する必要があるという事実のために、これは常に受け入れられるわけでも、まったく可能であるわけでもありません。 そして第二に、このアイデアの実装そのものは率直に言って湿気があり、多くの不便があり、明らかな落とし穴はありません。







ツール



そのため、必要なツールのセットは、次のメインパッケージで表されます。









実験に関係する関連ツール:









古いバージョンのOpenJDKに加えて、移植例はMavenではなくAntを使用して行われます。 私は設定に関する規約の支持者であり、Antを5、6年使用していませんが、Antはこの特定のタスクを解決するためのはるかに便利なツールであると思われます。 まず第一に、シンプルさのため、そして実際には、Maven、速度、およびクロスプラットフォームで達成するのが難しい微調整のため(シェルスクリプトはさらに短くなりますが、Cygwinおよび同様のローション)。







Stream APIの簡単な例を、概念実証として使用します。







package test; import java.util.stream.Stream; import static java.lang.System.out; public final class EntryPoint { private EntryPoint() { } public static void main(final String... args) { runAs("stream", () -> Stream.of(args).map(String::toUpperCase).forEach(EntryPoint::dump)); } private static void runAs(final String name, final Runnable runnable) { out.println("pre: " + name); runnable.run(); out.println("post: " + name); } private static void dump(final Object o) { out.println(">" + o); } }
      
      





実験がどのように進むかについてのいくつかの言葉。 Antのbuild.xml



多くのステップに分割され、各ステップには移植プロセス中に独自のディレクトリが割り当てられます。 これにより、少なくとも私にとっては、ソリューションを見つけてデバッグするプロセスが大幅に簡素化され、ステップごとの変更を追跡できます。







移植プロセス



ステップ0。初期化



いつものように、Antで最初に行うことはほとんど常にターゲットディレクトリを作成することです。







 <target name="init" description="Initializes the workspace"> <mkdir dir="${targetDir}"/> </target>
      
      





ステップ1.グラブ



実験の非常に重要な要素は、テストケースが依存するすべてのクラスの最小の正確なリストです。 残念ながら、これがもっと簡単にできるかどうかわかりません。繰り返し実行の方法を使用して、JRE 8から必要なすべてのクラスを登録するのにかなりの時間を費やしました。







一方、 java.util.stream



パッケージ全体をプルしてから、他の依存関係をプルするためにさらに時間を費やすこと(そして、おそらくProGuardなどのツールで処理すること)を試みるのは理にかなっています。 しかし、私は別の簡単なトリックを選ぶことにしました。 $**



マスクを使用して、ネストされたクラスと内部クラスを単純にコピーします。 これにより、時間とリストが大幅に節約されます。 Java 8で新しい機能が追加されたため、Javaの古いバージョンに存在していたいくつかのクラスも同様にコピーする必要があります。 これは、たとえば、新しいデフォルトMap.putIfAbsent(Object,Object)



に適用されます。これは、テストには関係しませんが、正しい操作に必要です。







 <target name="01-grab" depends="init" description="Step 01: Grab some JRE 8 classes"> <unzip src="${java.home}/lib/rt.jar" dest="${step01TargetDir}"> <patternset> <include name="java/lang/AutoCloseable.class"/> <include name="java/lang/Iterable.class"/> <include name="java/util/Arrays.class"/> <include name="java/util/AbstractMap.class"/> <include name="java/util/EnumMap.class"/> <include name="java/util/EnumMap$**.class"/> <include name="java/util/function/Consumer.class"/> <include name="java/util/function/Function.class"/> <include name="java/util/function/Supplier.class"/> <include name="java/util/Iterator.class"/> <include name="java/util/Map.class"/> <include name="java/util/Objects.class"/> <include name="java/util/Spliterator.class"/> <include name="java/util/Spliterator$**.class"/> <include name="java/util/Spliterators.class"/> <include name="java/util/Spliterators$**.class"/> <include name="java/util/stream/AbstractPipeline.class"/> <include name="java/util/stream/BaseStream.class"/> <include name="java/util/stream/ForEachOps.class"/> <include name="java/util/stream/ForEachOps$**.class"/> <include name="java/util/stream/PipelineHelper.class"/> <include name="java/util/stream/ReferencePipeline.class"/> <include name="java/util/stream/ReferencePipeline$**.class"/> <include name="java/util/stream/Sink.class"/> <include name="java/util/stream/Sink$**.class"/> <include name="java/util/stream/Stream.class"/> <include name="java/util/stream/StreamShape.class"/> <include name="java/util/stream/StreamOpFlag.class"/> <include name="java/util/stream/StreamOpFlag$**.class"/> <include name="java/util/stream/StreamSupport.class"/> <include name="java/util/stream/TerminalSink.class"/> <include name="java/util/stream/TerminalOp.class"/> </patternset> </unzip> </target>
      
      





実際、非常に印象的なクラスのリストは、最初はmap()



およびforEach()



ように単純な場合にのみ必要です。







ステップ2.コンパイル



退屈なテストコードのコンパイル。 どこも簡単です。







 <target name="02-compile" depends="01-grab" description="Step 02: Compiles the source code dependent on the grabbed JRE 8 classes"> <mkdir dir="${step02TargetDir}"/> <javac srcdir="${srcDir}" destdir="${step02TargetDir}" source="1.8" target="1.8"/> </target>
      
      





ステップ3.マージ



この手順は、Java 8 rt.jar



からクラスをコピーした結果とテストケースをマージするだけなので、少し奇妙に見えるかもしれません。 これは、次のいくつかのステップで実際に必要になり、適切な後処理のためにJavaパッケージを移動します。







 <target name="03-merge" depends="02-compile" description="Step 03: Merge into a single JAR in order to relocate Java 8 packages properly"> <zip basedir="${step01TargetDir}" destfile="${step03TargetFile}"/> <zip basedir="${step02TargetDir}" destfile="${step03TargetFile}" update="true"/> </target>
      
      





ステップ4.シェード



Mavenには、クラスファイルのバイトコードを直接変更してパケットを移動できる興味深いプラグインがあります。 私は知らないかもしれませんが、Antカウンターパートが存在するかどうかインターネットで見栄えが悪いかもしれませんが、Ant 小さな拡張機能を書くしかありませんでした。これは、Mavenプラグイン用のシンプルなアダプターです。 maven-shade-plugin



他の機能maven-shade-plugin



ません。







この段階で、Retrolambdaをさらに使用できるようにするには、すべてのjava.*



パッケージを〜.javaのような名前に変更する必要があり~.java.*



(はい、チルダです-なぜですか?)。 実際のところ、Retrolambdaはjava.lang.invoke.MethodHandles



クラスの作業に依存しているため、 java.*



(およびsun.*



パッケージはOracle JDK / JREのように)の使用が禁止されています。 したがって、一時的にパッケージを移動することは、単にjava.lang.invoke.MethodHandles



を「ブラインド」する方法です。







ステップ1のように、インクルードリストを介してクラスの完全なリストを個別に指定する必要がありました。 これを行わずにリストを完全に省略すると、クラスファイル内のshade



は、処理される予定のないクラスも移動します。 この場合、たとえば、 java.lang.String



~.java.lang.String



(少なくとも、これはjavap



逆コンパイルされたクラスから明確に見ることができjavap



)、Retrolambdaを壊します。これは単にコードの変換を静かに停止し、クラスを生成しません。ラムダ/ invokedynamic



。 すべてのクラスを除外リストに書き込むことは、検索が難しく、余分なチルダを検索するためにjavap



を使用してクラスファイル内を突く必要があるため、より実用的でないと考えています。







 <target name="04-shade" depends="03-merge" description="Step 04: Rename java.* to ~.java.* in order to let RetroLambda work since MethodHandles require non-java packages"> <shade jar="${step03TargetFile}" uberJar="${step04TargetFile}"> <relocation pattern="java" shadedPattern="~.java"> <include value="java.lang.AutoCloseable"/> <include value="java.lang.Iterable"/> <include value="java.util.Arrays"/> <include value="java.util.AbstractMap"/> <include value="java.util.EnumMap"/> <include value="java.util.EnumMap$**"/> <include value="java.util.function.Consumer"/> <include value="java.util.function.Function"/> <include value="java.util.function.Supplier"/> <include value="java.util.Iterator"/> <include value="java.util.Map"/> <include value="java.util.Objects"/> <include value="java.util.Spliterator"/> <include value="java.util.Spliterator$**"/> <include value="java.util.Spliterators"/> <include value="java.util.Spliterators$**"/> <include value="java.util.stream.AbstractPipeline"/> <include value="java.util.stream.BaseStream"/> <include value="java.util.stream.ForEachOps"/> <include value="java.util.stream.ForEachOps$**"/> <include value="java.util.stream.PipelineHelper"/> <include value="java.util.stream.ReferencePipeline"/> <include value="java.util.stream.ReferencePipeline$**"/> <include value="java.util.stream.Sink"/> <include value="java.util.stream.Sink$**"/> <include value="java.util.stream.Stream"/> <include value="java.util.stream.StreamShape"/> <include value="java.util.stream.StreamOpFlag"/> <include value="java.util.stream.StreamOpFlag$**"/> <include value="java.util.stream.StreamSupport"/> <include value="java.util.stream.TerminalSink"/> <include value="java.util.stream.TerminalOp"/> </relocation> </shade> </target>
      
      





小さな余談。 理論的には、Antでのリストの複製はrefid



をサポートする要素を使用して解決できますが、これはいくつかの理由で機能しません。









したがって、これらの欠点はすべて解決されますが、明らかにこの記事の枠組み内ではありません。







ステップ5.解凍



次のステップは、移動されたパッケージでJARファイルを解凍することです。Retrolambdaはディレクトリでのみ機能するためです。







 <target name="05-unzip" depends="04-shade" description="Step 05: Unpacking shaded JAR in order to let Retrolamda work"> <unzip src="${step04TargetFile}" dest="${step05TargetDir}"/> </target>
      
      





ステップ6. Retrolambda



実験の中心:バイトコードバージョン52(Java 8)をバージョン50(Java 6)に変換します。 さらに、上記で使用したトリックにより、Retrolambda(または、JDK 8)はクラスに静かに、そして追加の質問なしですでに指示します。 また、メソッドのデフォルトサポートを有効にする必要があります。これは、Java 8の多くの新機能がメソッド上に構築されているためです。 JRE 7以下ではそのようなメソッドの操作方法がわからないため、Retrolambdaは再定義されていない各クラスのメソッドの実装を単純にコピーします(これにより、「最終的なアプリケーションとそのライブラリ」に対してのみRetrolambdaを使用する必要があります、それ以外の場合、デフォルトメソッドの実装が単に存在しない場合に問題が発生する可能性が高いです。







 <target name="06-retrolambda" depends="05-unzip" description="Step 06: Perform downgrade from Java 8 to Java 6 bytecode"> <java jar="${retrolambdaJar}" fork="true" failonerror="true"> <sysProperty key="retrolambda.bytecodeVersion" value="50"/> <sysProperty key="retrolambda.classpath" value="${step05TargetDir}"/> <sysProperty key="retrolambda.defaultMethods" value="true"/> <sysProperty key="retrolambda.inputDir" value="${step05TargetDir}"/> <sysProperty key="retrolambda.outputDir" value="${step06TargetDir}"/> </java> </target>
      
      





ステップ7. Zip



指示されたバージョンを1つのファイルに戻し、シェードプラグインを反対方向に起動します。







 <target name="07-zip" depends="06-retrolambda" description="Step 07: Pack the downgraded classes back before unshading"> <zip basedir="${step06TargetDir}" destfile="${step07TargetFile}"/> </target>
      
      





ステップ8.シェーディング解除



幸いなことに、シェードプラグインが反対方向への移動で機能するには、2つのパラメーターだけで十分です。 このステップの最後で、アプリケーション内のパッケージは元に戻され、 ~.java.*



java.*



再び。







 <target name="08-unshade" depends="07-zip" description="Step 08: Relocate the ~.java package back to the java package"> <shade jar="${step07TargetFile}" uberJar="${step08TargetFile}"> <relocation pattern="~.java" shadedPattern="java"/> </shade> </target>
      
      





ステップ9.開梱



このステップでは、後で2つの別々のJARファイルを組み立てるために、クラスを単純に解凍します。 再び興味深いものはありません。







 <target name="09-unpack" depends="08-unshade" description="Step 09: Unpack the unshaded JAR in order to create two separate JAR files"> <unzip src="${step08TargetFile}" dest="${step09TargetDir}"/> </target>
      
      





ステップ10および11。パック



すべてのクラスをまとめますが、個別に「新しいランタイム」とテストアプリケーション自体を作成します。 そして再び-非常に些細で面白くないステップ。







 <target name="10-pack" depends="09-unpack" description="Step 10: Pack the downgraded Java 8 runtime classes"> <zip basedir="${step09TargetDir}" destfile="${step10TargetFile}"> <include name="java/**"/> </zip> </target> <target name="11-pack" depends="09-unpack" description="Step 11: Pack the downgraded application classes"> <zip basedir="${step09TargetDir}" destfile="${step11TargetFile}"> <include name="test/**"/> </zip> </target>
      
      





試験結果



以上です。 ターゲットディレクトリには、実際のStream APIの小さな側面の小さなポートが含まれており、Java 6で実行できます。 これを行うには、Antの別のルールを作成します。







 <target name="run-as-java-6" description="Runs the target artifact in Java 6"> <fail unless="env.JDK_6_HOME" message="JDK_6_HOME not set"/> <java jvm="${env.JDK_6_HOME}/bin/java" classpath="${step11TargetFile}" classname="${mainClass}" fork="true" failonerror="true"> <jvmarg value="-Xbootclasspath/p:${step10TargetFile}"/> <arg value="foo"/> <arg value="bar"/> <arg value="baz"/> </java> </target>
      
      





そしてここでは、あまり標準的ではない-Xbootclasspath/p



使用に特別な注意を払う必要があります。 要するに、その本質は次のとおりです。最初の場所で基本クラスをロードする場所をJVMが指定できるようにします。 この場合、元のrt.jarの残りのクラスは、必要に応じて$JAVA_HOME/jre/lib/rt.jar



から遅延ロードされ$JAVA_HOME/jre/lib/rt.jar



。 これを-verbose:class



は、JVMの起動時に-verbose:class



スイッチを使用し-verbose:class









サンプル自体を実行するには、JDK 6またはJRE 6を指すJDK_6_HOME



環境変数も必要ですJDK_6_HOME



run-as-java-6



呼び出されると、移植が成功した結果が標準出力に出力されます。







 PRE: stream >FOO >BAR >BAZ POST: stream
      
      





動作しますか? はい!







おわりに



Java 8でコードを記述することに慣れていて、このコードが古いバージョンのJavaで動作するようにしたいと思います。 特に、かなり古くて重いコードベースが利用できる場合。 また、インターネット上で、古いバージョンのJavaでStream APIを使用する機会があるかどうかという質問をよく見ることができる場合、彼らは常にノーと言います。 まあ、ほとんどない。 そして彼らは正しいでしょう。 もちろん、古いJREで機能する同様の機能を持つ代替ライブラリが提供されます。 個人的にはGoogle Guavaに最も感銘を受けており、Java 8では不十分な場合によく使用します。







実験的ハックは実験的ハックであり、デモンストレーションを超えてさらに前進することは理にかなっているとは思えません。 しかし、研究と実験の精神のために、なぜですか? GitHubの実験を詳しく見ることができます







未解決および未解決の問題



Antのrefid



に加えて、個人的にはいくつかの質問が残っています。







この例は他のJVM実装で機能しますか?

Oracle JVMで実行されますが、Oracleライセンスでは、 -Xbootclasspath



を使用してrt.jar



部分を置き換えるアプリケーションデプロイが禁止されてい-Xbootclasspath









手動の反復に頼ることなく、依存関係クラスのリストを自動的に作成することは可能ですか?

私は個人的にそのような分析の自動方法を知りません。 java.util.stream.*



全体を実行しようとすることができjava.util.stream.*



完全にパッケージ化できますが、さらに問題があると思います。







Dalvik VMでこの例を実行することはできますか?

これはAndroidを指します。 結果をdxに渡して、実際のデバイスで-Xbootclasspath



を使用してDalvik VMを直接実行しようとしましたが、Dalvikはそのような要求を永続的に無視します。 この理由は、Dalvik VMのアプリケーションがZygoteから来ているためだと思われますが、 Zygoteは明らかにそのような意図を認識していません。 これがなぜできないのか、何が問題なのかについては、 StackOverflowで読むことができます。 そして、 -Xbootclasspath



dalvikvm



を起動できれば、アプリケーション自体のランチャーが必要になり、このブートクラスパスが置き換えられると思われます。 そのようなシナリオは可能ではないようです。







GWTはどうですか?

そして、これはまったく異なる話であり、異なるアプローチです。 先日、待望のGWT 2.8.0 (2年前のバージョン2.7.0)のリリースが行われ、Java 8で記述されたソースコードのラムダおよびその他の機能が完全に実装されましたが、これはすべてSNAPSHOTのリリース前でしたバージョン。 GWTはソースコードでのみ機能するため、GWTでバイトコードを混乱させることはできません。 Stream APIをクライアント側に移植するには、ソースをGWTに適した形式に変換するプリプロセッサ(RxJavaの移植例)に渡した後、JDK 8からソースの一部を収集するだけでよいと思います。








All Articles