https://trends.google.com/trends/explore?q=%2Fm%2F0_lcrx4
上記は「kotlin」という単語を検索したときのGoogleトレンドのスクリーンショットです。 突然の急増は、 GoogleがKotlinがAndroidの主要言語になっていることを発表したときです 。 これは数週間前のGoogle I / Oカンファレンスで起こりました。 これまで、あなたはあなたの周りの誰もが突然それについて話し始めたので、あなたは以前にこの言語を使用したか、またはそれに興味を持ちました。
Kotlinの主な機能の1つは、Javaとの相互運用性です。JavaからKotlinコードを呼び出し、KotlinからJavaコードを呼び出すことができます。 これはおそらく、言語が広く配布されているため、最も重要な機能です。 すべてをすぐに移行する必要はありません。既存のコードベースの一部を取得して、Kotlinコードの追加を開始するだけで機能します。 Kotlinを試してみて、気に入らない場合は、いつでも拒否できます(ただし、試してみることをお勧めします)。
Javaで5年間働いた後、初めてKotlinを使用したとき、いくつかのことが魔法のように思えました。
「ちょっと待って、何? 定型コードを回避するためにdata class
を記述することはできますか?」
「やめて、 apply
を書いたら、それに関してメソッドを呼び出すたびにオブジェクトを定義する必要はありませんか?」
時代遅れで面倒ではない言語がようやく登場したという事実からの最初の安ighのため、私は不快感を感じ始めました。 Javaとの相互互換性が必要な場合、これらすべての優れた機能はKotlinでどの程度正確に実装されますか? キャッチは何ですか?
この記事は専用です。 Kotlinコンパイラーが特定のコンストラクトをどのように変換してJavaとの互換性を持たせるかを学ぶことに非常に興味がありました。 私の研究では、Kotlin標準ライブラリから最も一般的な4つの方法を選択しました。
-
apply
-
with
-
let
-
run
この記事を読むと、恐れる必要がなくなります。 今、私はすべてがどのように機能するかを理解し、言語とコンパイラを信頼できることを知っているので、はるかに自信を感じています。
適用する
/** * [block] `this` `this`. */ @kotlin.internal.InlineOnly public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
apply
は単純です。これは、拡張型インスタンス(「レシーバー」と呼ばれる)に関してブロックパラメーターを実行し、レシーバー自体を返す拡張関数です。
この機能を適用するには多くの方法があります。 オブジェクトの作成を初期構成にバインドできます。
val layout = LayoutStyle().apply { orientation = VERTICAL }
ご覧のように、作成時に新しいLayoutStyle
構成を提供します。これにより、 コードがクリーンになり、 実装のエラーがはるかに少なくなります。 同じ名前であるため、間違ったインスタンスでメソッドを呼び出しましたか? さらに悪いことに、リファクタリングに完全に欠陥があったときは? 上記のアプローチでは、このような問題に直面するのがはるかに困難になります。 また、 this
パラメーターを定義する必要がないことに注意してください。 クラス自体と同じスコープ内にいます 。 クラス自体を拡張しているかのようであるため、 this
は暗黙的に指定されます。
しかし、それはどのように機能しますか? 短い例を見てみましょう。
enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } fun main(vararg args: Array<String>) { val layout = LayoutStyle().apply { orientation = VERTICAL } }
IntelliJ IDEA Show Kotlinバイトコードツール([ Tools > Kotlin > Show Kotlin Bytecode
Kotlinバイトコードの表示])のおかげで、コンパイラがコードをJVMバイトコードに変換する方法を確認できます。
NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 2 ALOAD 2 ASTORE 3 ALOAD 3 GETSTATIC kotlindeepdive/Orientation.VERTICAL : Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 2 ASTORE 1
バイトコードの指向があまりよくない場合は、 これらの 素晴らしい 記事を読むことをお勧めします。その後、よりよく理解することができます(各メソッドはスタックで呼び出されるため、コンパイラは毎回オブジェクトをロードする必要があることに注意することが重要です)。
ポイントを分析しましょう:
-
LayoutStyle
新しいインスタンスLayoutStyle
スタックに複製LayoutStyle
ます。 - パラメーターがゼロのコンストラクターが呼び出されます。
- ストア/ロード操作が実行されます(以下を参照)。
- 値
Orientation.VERTICAL
スタックに渡されます。 -
setOrientation
がsetOrientation
れ、スタックからオブジェクトと値が発生します。
ここで注意すべき点がいくつかあります。 まず、魔法は一切関係なく、すべてが期待どおりに行わsetOrientation
ます。作成したLayoutStyle
インスタンスに関連して、 LayoutStyle
メソッドがsetOrientation
ます。 さらに、コンパイラーがインライン化するため、 apply
関数はどこにも表示されません。
さらに、バイトコードは、Javaのみを使用した場合に生成されるものとほぼ同じです! 自分で判断する:
// Java enum Orientation { VERTICAL, HORIZONTAL; } public class LayoutStyle { private Orientation orientation = HORIZONTAL; public Orientation getOrientation() { return orientation; } public void setOrientation(Orientation orientation) { this.orientation = orientation; } public static void main(String[] args) { LayoutStyle layout = new LayoutStyle(); layout.setOrientation(VERTICAL); } } // Bytecode NEW kotlindeepdive/LayoutStyle DUP ASTORE 1 ALOAD 1 GETSTATIC kotlindeepdive/Orientation.VERTICAL : kotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (kotlindeepdive/Orientation;)V
ヒント:多数のASTORE/ALOAD
気づいたかもしれません。 これらはKotlinコンパイラーによって挿入されるため、デバッガーはラムダでも機能します! これについては、記事の最後のセクションで説明します。
と
/** * [block] [receiver] . */ @kotlin.internal.InlineOnly public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
apply
に似ていapply
、いくつかの重要な違いがあります。 まず、 with
型の拡張関数ではありません。レシーバーはパラメーターとして明示的に渡される必要があります。 さらに、 with
はブロック関数の結果を返し、 apply
はレシーバー自体の結果を返します。
何でも返すことができるので、この例は非常に信じられそうです。
val layout = with(contextWrapper) { // `this` is the contextWrapper LayoutStyle(context, attrs).apply { orientation = VERTICAL } }
ここでは、 contextWrapper
プレフィックスを省略できます。 contextWrapper
はwith
関数のレシーバーであるため、 context
およびattrs
場合。 しかし、この場合でも、適用方法はapply
と比較するとそれほど明白ではありません。この関数は特定の条件下で役立つ場合があります。
これが与えられたので、例に戻り、以下で使用with
場合に何が起こるかを見てみましょう:
enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = with(SharedState) { LayoutStyle().apply { orientation = previousOrientation } } }
をwith
レシーバーはSharedState
シングルトンであり、レイアウトに設定する方向パラメーターが含まれています。 ブロック関数内で、 LayoutStyle
インスタンスを作成します。 apply
おかげで、 SharedState
から読み取ることで簡単に方向を設定できapply
。
生成されたバイトコードをもう一度見てみましょう。
GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; ASTORE 1 ALOAD 1 ASTORE 2 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 3 ALOAD 3 ASTORE 4 ALOAD 4 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 3 ASTORE 0 RETURN
特別なことは何もありません。 SharedState
クラスの静的フィールドとして実装された取得されたシングルトン。 以前と同様にLayoutStyle
のインスタンスが作成され、コンストラクターが呼び出されますSharedState
内でpreviousOrientation
値を取得する別の呼び出しSharedState
およびLayoutStyle
インスタンスに値を割り当てる最後の呼び出し。
ヒント:「Show Kotlin Bytecode」を使用する場合、「Decompile」をクリックすると、Kotlinコンパイラー用に作成されたバイトコードのJava表現を確認できます。 ネタバレ:それはまさにあなたが期待するものです!
させる
/** * [block] `this` . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
let
、nullになる可能性があるオブジェクトを操作するときに非常に便利です。 if-else式の無限のチェーンを作成する代わりに、単純にステートメントを結合できます?
(「安全な呼び出しステートメント」と呼ばれる) let
を使用すると、結果として、引数が元のオブジェクトのnull不可バージョンであるラムダを取得します。
val layout = LayoutStyle() SharedState.previousOrientation?.let { layout.orientation = it }
例全体を考えてみましょう。
enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation: Orientation? = VERTICAL } fun main() { val layout = LayoutStyle() // layout.orientation = SharedState.previousOrientation -- this would NOT work! SharedState.previousOrientation?.let { layout.orientation = it } }
これで、 previousOrientation
はnullになる場合があります。 レイアウトに直接割り当てようとすると、null許容型をnon-null許容型に割り当てることができないため、コンパイラが怒ります。 もちろん、if式を記述できますが、これによりSharedState.previousOrientation
式への二重参照がSharedState.previousOrientation
ます。 let
を使用すると、レイアウトに安全に割り当てることができる同じパラメーターへのnull不可の参照を取得します。
バイトコードの観点から見ると、すべてが非常に単純です。
NEW kotlindeepdive/let/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/let/LayoutStyle.<init> ()V GETSTATIC kotlindeepdive/let/SharedState.INSTANCE : Lkotlindeepdive/let/SharedState; INVOKEVIRTUAL kotlindeepdive/let/SharedState.getPreviousOrientation ()Lkotlindeepdive/let/Orientation; DUP IFNULL L2 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 0 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/let/LayoutStyle.setOrientation (Lkotlindeepdive/let/Orientation;)V GOTO L9 L2 POP L9 RETURN
単純なIFNULL
条件付き遷移を使用します。これは、本質的には手動で行う必要があります。ただし、コンパイラが効率的に実行する場合を除き、言語はこのようなコードを作成する優れた方法を提供します。 これは素晴らしいと思います!
走る
runには2つのバージョンがあります。1つ目は単純な関数、2つ目はジェネリック型の拡張関数です。 最初の関数はパラメーターとして渡されるブロック関数のみを呼び出すため、2番目の関数を分析します。
/** * [block] `this` . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.run(block: T.() -> R): R = block()
おそらく、 run
は、考えられる最も単純な関数です。 これは、インスタンスがレシーバーとして渡されるタイプの拡張関数として定義され、 block
関数の実行結果を返します。 run
はlet
とapply
ハイブリッドのように思えるかもしれませんが、実際はそうです。 唯一の違いは戻り値です。 apply
の場合は受信者自身を返し、 run
の場合はblock
関数の結果を返します( let
の場合と同様)。
この例では、 run
がblock
関数の結果を返すという事実を強調してrun
ます。この場合、それは割り当て( Unit
)です。
enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = LayoutStyle() layout.run { orientation = SharedState.previousOrientation } // returns Unit }
同等のバイトコード:
NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 0 ALOAD 0 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 2 GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V RETURN
run
は他の関数と同様にインラインであり、単純なメソッド呼び出しになりました。 ここでも奇妙なことはありません!
標準ライブラリの機能には多くの類似点があることに注意しました。これは、できるだけ多くのアプリケーションをカバーするために意図的に行われました。 一方、特定のタスクにどの関数が最適であるかを理解することは、それらの違いがわずかであることを考えると、それほど簡単ではありません。
標準ライブラリを扱うのを助けるために、考慮された主な機能間のすべての違いを要約した表を描きました(例外もあります ):
アプリケーション:追加のstore/load
操作
「Javaバイトコード」と「Kotlinバイトコード」を比較すると、まだ完全に理解できませんでした。 私が言ったように、Kotlinでは、Javaとは異なり、追加の操作astore/aload
。 これはラムダと関係があることは知っていましたが、なぜラムダが必要なのか理解できました。
デバッガーがラムダをスタックフレームとして処理するには、これらの追加の操作が必要であるように思われます 。これにより、作業に介入(ステップイン)できます。 ローカル変数が何であるか、誰がラムダを呼び出しているか、誰がラムダから呼び出されるかなどを確認できます。
しかし、APKを実稼働環境に渡すとき、デバッガの機能は気にしませんか? そのため、これらの関数は、サイズが小さく重要でないにもかかわらず、冗長であり、削除される可能性があると考えることができます。
このためには、すべての人に知られ、誰もが愛するツールであるProGuardが適しています。 バイトコードレベルで動作し、難読化とトリミングに加えて、最適化パスを実行してバイトコードをよりコンパクトにします。 JavaとKotlinで同じコードを作成し、1つのルールセットを使用してProGuardの両方のバージョンに適用し、結果を比較しました。 これが何が起こったのかです。
ProGuardの構成
-dontobfuscate -dontshrink -verbose -keep,allowoptimization class kotlindeepdive.apply.LayoutStyle -optimizationpasses 2 -keep,allowoptimization class kotlindeepdive.LayoutStyleJ
ソースコード
Java:
package kotlindeepdive enum OrientationJ { VERTICAL, HORIZONTAL; } class LayoutStyleJ { private OrientationJ orientation = HORIZONTAL; public OrientationJ getOrientation() { return orientation; } public LayoutStyleJ() { if (System.currentTimeMillis() < 1) { main(); } } public void setOrientation(OrientationJ orientation) { this.orientation = orientation; } public OrientationJ main() { LayoutStyleJ layout = new LayoutStyleJ(); layout.setOrientation(VERTICAL); return layout.orientation; } }
コトリン:
package kotlindeepdive.apply enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = Orientation.HORIZONTAL init { if (System.currentTimeMillis() < 1) { main() } } fun main() { val layout = LayoutStyle().apply { orientation = Orientation.VERTICAL } layout.orientation } }
バイトコード
Java:
sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c LayoutStyleJ.class Compiled from "SimpleJ.java" final class kotlindeepdive.LayoutStyleJ { public kotlindeepdive.LayoutStyleJ(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #6 // Field kotlindeepdive/OrientationJ.HORIZONTAL$5c1d747f:I 8: putfield #5 // Field orientation$5c1d747f:I 11: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 34 19: new #3 // class kotlindeepdive/LayoutStyleJ 22: dup 23: invokespecial #10 // Method "<init>":()V 26: getstatic #7 // Field kotlindeepdive/OrientationJ.VERTICAL$5c1d747f:I 29: pop 30: iconst_1 31: putfield #5 // Field orientation$5c1d747f:I 34: return }
コトリン:
sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c apply/LayoutStyle.class Compiled from "Apply.kt" public final class kotlindeepdive.apply.LayoutStyle { public kotlindeepdive.apply.LayoutStyle(); Code: 0: aload_0 1: invokespecial #13 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #11 // Field kotlindeepdive/apply/Orientation.HORIZONTAL:Lkotlindeepdive/apply/Orientation; 8: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 11: invokestatic #14 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 32 19: new #8 // class kotlindeepdive/apply/LayoutStyle 22: dup 23: invokespecial #16 // Method "<init>":()V 26: getstatic #12 // Field kotlindeepdive/apply/Orientation.VERTICAL:Lkotlindeepdive/apply/Orientation; 29: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 32: return }
2つのバイトコードリストを比較した後の結論:
- 「Kotlinバイトコード」の追加の
astore/aload
は消えましたastore/aload
はそれらを冗長であると判断し、すぐに削除したためです(このため、1つが削除されなかった後、2つの最適化パスを作成する必要がありました)。 - JavaバイトコードとKotlinバイトコードはほぼ同じです。 最初のものは列挙値を操作するときに面白い/奇妙な瞬間がありますが、Kotlinにはそのようなものはありません。
おわりに
開発者に非常に多くの機能を提供する新しい言語を取得することは素晴らしいことです。 しかし、使用するツールに頼ることができることを理解し、それらを使用するときに自信を持っていることも重要です。 コンパイラが余分なことや危険なことを何もしないことを知っているという意味で、「私はKotlinを信頼しています」と言うことができてうれしいです。 Javaで手動で行う必要があることだけを行い、時間とリソースを節約します(そして、JVMのコーディングの長年の喜びを返します)。 これは、エンドユーザーにとってもある程度のメリットがあります。より厳密な型の安全性により、アプリケーションのバグが少なくなるからです。
さらに、Kotlinコンパイラーは絶えず改善されているため、生成されたコードはより効率的になっています。 そのため、コンパイラを使用してKotlinコードを最適化する必要はありません。より効率的で慣用的なコードの作成に専念し、残りはコンパイラに任せることをお勧めします。