Java 8からJava 6ぞのStream APIの玔粋に実隓的な移怍手法

1幎前、 MavenずRetrolambdaを䜿甚しお、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から゜ヌスの䞀郚を収集するだけでよいず思いたす。



Source: https://habr.com/ru/post/J313888/


All Articles