KotlinのJVMプラットフォームアノテーションのガイド

1. 前書き

Kotlinは、KotlinクラスとJavaの互換性を促進するためにいくつかの注釈を提供します。
このチュートリアルでは、KotlinのJVMアノテーション、それらの使用方法、およびJavaでKotlinクラスを使用した場合の効果について具体的に説明します。

2. KotlinのJVMアノテーション

  • KotlinのJVMアノテーションは、Kotlinコードをバイトコードにコンパイルする方法と、結果のクラスをJavaで使用する方法に影響します。*

    Kotlinのみを使用する場合、ほとんどのJVMアノテーションは影響を与えません。 ただし、_ @ JvmName_と_ @ JvmDefault_は、純粋にKotlinを使用する場合にも効果があります。

3. @JvmName

  • _ @ JvmName_注釈をファイル、関数、プロパティ、ゲッター、セッターに適用できます。*

    *すべての場合で、_ @ JvmName_はバイトコードでターゲットの名前を定義します。これは、Javaからターゲットを参照するときに使用できる名前でもあります。*
    アノテーションは、Kotlin自体から呼び出すときに、クラス、関数、ゲッター、またはセッターの名前を変更しません。
    *考えられる各ターゲットをさらに詳しく見てみましょう。*

3.1. ファイル名

デフォルトでは、Kotlinファイルのすべてのトップレベルの関数とプロパティは_filenameKt.class_にコンパイルされ、すべてのクラスは_className.class_にコンパイルされます。
_message.kt_という名前のファイルがあり、最上位の宣言と_Message:_という名前のクラスがあるとします
package jvmannotation

fun getMyName() : String {
    return "myUserId"
}

class Message {
}
コンパイラは、_MessageKt.class_と_Message.class_の2つのクラスファイルを作成します。 Javaから両方を呼び出すことができます。
Message m = new Message();
String me = MessageKt.getMyName();
_MessageKt.class_に別の名前を付けたい場合は、*ファイルの最初の行に@__JvmName __annotationを追加できます:*
@file:JvmName("MessageHelper")
package jvmannotation
Javaでは、アノテーションで定義されている名前を使用できます。
String me = MessageHelper.getMyName();
*注釈は、クラスファイルの名前を変更しません。 _Message.class _。*のままになります

3.2. 関数名

  • _ @ JvmName_注釈は、バイトコード内の関数の名前を変更します。*次の関数を呼び出すことができます。

@JvmName("getMyUsername")
fun getMyName() : String {
    return "myUserId"
}
そして、Javaから、アノテーションで指定した名前を使用できます。
String username = MessageHelper.getMyUsername();
Kotlinでは、実際の名前を使用します。
val username = getMyName()
_ @ JvmName_が便利な2つの興味深い使用例があります。関数と型消去を使用します。

3.3. 関数名の競合

*最初の使用例は、自動生成されたgetterまたはsetterメソッドと同じ名前の関数です。*
次のコード
val sender = "me"
fun getSender() : String = "from:$sender"
コンパイル時エラーを生成します:
Platform declaration clash: The following declarations have the same JVM signature (getSender()Ljava/lang/String;)
public final fun <get-sender>(): String defined in jvmannotation.Message
public final fun getSender(): String defined in jvmannotation.Message
エラーの理由は、Kotlinが自動的にゲッターメソッドを生成し、同じ名前の追加関数を使用できないためです。
その名前の関数が必要な場合は、_ @ JvmName_を使用して、Kotlinコンパイラーにバイトコードレベルで関数の名前を変更するように指示できます。
@JvmName("getSenderName")
fun getSender() : String = "from:$sender"
これで、実際の名前でKotlinから関数を呼び出し、通常どおりメンバー変数にアクセスできます。
val formattedSender = message.getSender()
val sender = message.sender
Javaから、アノテーションで定義された名前で関数を呼び出し、生成されたgetterメソッドでメンバー変数にアクセスできます。
String formattedSender = m.getSenderName();
String sender = m.getSender();
この時点で、このようなゲッター解決を行うことは、ネーミングの混乱を引き起こす可能性があるため、できる限り避けるべきであることに注意する必要があります。

3.4. タイプ消去の競合

  • 2番目の使用例は、一般的なtype erasureが原因で名前が競合する場合です。*

    ここで簡単な例を見てみましょう。 次の2つのメソッドは、JVMでメソッドシグネチャが同じであるため、同じクラス内では定義できません。
fun setReceivers(receiverNames : List<String>) {
}

fun setReceivers(receiverNames : List<Int>) {
}
コンパイルエラーが表示されます。
Platform declaration clash: The following declarations have the same JVM signature (setReceivers(Ljava/util/List;)V)
Kotlinの両方の関数に同じ名前を付けたい場合、いずれかの関数に_ @ JvmName_の注釈を付けることができます。
@JvmName("setReceiverIds")
fun setReceivers(receiverNames : List<Int>) {
}
Kotlinは両方の署名を異なると見なすため、宣言された名前_setReceivers()_を使用してKotlinから両方の関数を呼び出すことができます。 Javaから、2つの別々の名前、_setReceivers()_および_setReceiverIds()_で2つの関数を呼び出すことができます。

3.5. ゲッターとセッター

_ @ JvmName_アノテーションを適用して、*デフォルトのゲッターとセッターの名前を変更することもできます。*
Kotlinの次のクラス定義を見てみましょう。
class Message {
    val sender = "me"
    var text = ""
    private val id = 0
    var hasAttachment = true
    var isEncrypted = true
}
Kotlinから、クラスメンバーを直接参照できます。たとえば、_text_に値を割り当てることができます。
val message = Message()
message.text = "my message"
val copy = message.text
ただし、Javaからは、Kotlinコンパイラーによって自動生成されるgetterおよびsetterメソッドを呼び出します。
Message m = new Message();
m.setText("my message");
String copy = m.getText();
*生成されたゲッターメソッドまたはセッターメソッドの名前を変更する場合は、クラスメンバーに_ @ JvmName_注釈を追加できます。*
@get:JvmName("getContent")
@set:JvmName("setContent")
var text = ""
これで、定義されたゲッター名とセッター名によってJavaのテキストにアクセスできます。
Message m = new Message();
m.setContent("my message");
String copy = m.getContent();
ただし、_ @ JvmName_アノテーションは、Kotlinからクラスメンバーにアクセスする方法を変更しません。 それでも変数に直接アクセスできます。
message.text = "my message"
Kotlinでは、次の場合でもコンパイルエラーが発生します。
m.setContent("my message");

3.6. 命名規則

  • _ @ JvmName_アノテーションは、JavaからKotlinクラスを呼び出すときに特定の命名規則に準拠する場合にも役立ちます*。

    これまで見てきたように、コンパイラは生成されたゲッターメソッドにプレフィックス_get_を追加します。 ただし、_is_で始まる名前のフィールドの場合はそうではありません。 Javaでは、次の方法でメッセージクラスの2つの__boolean__sにアクセスできます。
Message message = new Message();
boolean isEncrypted = message.isEncrypted();
boolean hasAttachment = message.getHasAttachment();
ご覧のとおり、コンパイラは_isEncrypted._のゲッターメソッドのプレフィックスを付けません。
ただし、_is._で始まるプロパティにのみ適用されます。_getHasAttachment()_がまだあります。 ここで、_ @ JvmName_アノテーションを追加できます。
@get:JvmName("hasAttachment")
var hasAttachment = true
さらに、Javaのイディオマティックゲッターを取得します。
boolean hasAttachment = message.hasAttachment();

3.7. アクセス修飾子の制限

*アノテーション__ appropriate__は、適切なアクセス権を持つクラスメンバーにのみ適用できることに注意してください。*
_ @ set:JvmName_を不変のメンバーに追加しようとすると:
@set:JvmName("setSender")
val sender = "me"
コンパイル時エラーが発生します:
Error:(11, 5) Kotlin: '@set:' annotations could be applied only to mutable properties
そして、_ @ get:JvmName_または_ @ set:JvmName_をプライベートメンバーに追加しようとすると:
@get:JvmName("getId")
private id = 0
警告のみが表示されます。
An accessor will not be generated for 'id', so the annotation will not be written to the class file
また、Kotlinコンパイラーは注釈を無視し、ゲッターメソッドまたはセッターメソッドを生成しません。

4. @JvmStatic_および@_JvmField

_https://www.baeldung.com/kotlin-jvm-field-annotation [@JvmField] _および_https://www.baeldung.com/kotlin-jvm-synthetic [@JvmSyntheticについて説明する2つの記事が既にあります。 ] _注釈。したがって、ここではそれらについて詳しく説明しません。
ただし、_ @ JvmField_を簡単に見て、定数と_ @ JvmStatic_アノテーションの違いを指摘します。

4.1. _ @ JvmStatic_

_ @ JvmStatic_注釈は、名前付きオブジェクトまたはコンパニオンオブジェクトの関数またはプロパティに適用できます。
注釈のない_MessageBroker_から始めましょう。
object MessageBroker {
    var totalMessagesSent = 0
    fun clearAllMessages() { }
}
Kotlinでは、これらのプロパティと関数に静的な方法でアクセスできます。
val total = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()
ただし、Javaで同じことを行う場合は、そのオブジェクトのINSTANCEを使用して行う必要があります。
int total = MessageBroker.INSTANCE.getTotalMessagesSent();
MessageBroker.INSTANCE.clearAllMessages();
これは、Javaではあまり一般的ではありません。 したがって、_ @ JvmStatic_アノテーションを使用できます。
object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0
    @JvmStatic
    fun clearAllMessages() { }
}
これで、Javaの静的プロパティとメソッドも表示されます。
int total = MessageBroker.getTotalMessagesSent();
MessageBroker.clearAllMessages();

4.2. _ @ JvmField、@ JvmStatic_および定数

_ @ JvmField _、_ @ JvmStatic_、およびKotlinの_constant_の違いをよりよく理解するために、次の例を見てみましょう。
object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0

    @JvmField
    var maxMessagePerSecond = 0

    const val maxMessageLength = 0
}
名前付きオブジェクトは、シングルトンのKotlin実装です。 プライベートコンストラクターと_public_ _static_ _INSTANCE_フィールドを持つ_final_クラスにコンパイルされます。 上記のクラスに相当するJavaは次のとおりです。
public final class MessageBroker {
    private static int totalMessagesSent = 0;
    public static int maxMessagePerSecond = 0;
    public static final int maxMessageLength = 0;
    public static MessageBroker INSTANCE = new MessageBroker();

    private MessageBroker() {
    }

    public static int getTotalMessagesSent() {
        return totalMessagesSent;
    }

    public static void setTotalMessagesSent(int totalMessagesSent) {
        this.totalMessagesSent = totalMessagesSent;
    }
}
_ @ JvmStatic_アノテーションが付けられたプロパティは、_private static_フィールドと、対応するgetterおよびsetterメソッドに相当することがわかります。 _ @ JvmField_アノテーションが付けられたフィールドは_public static_フィールドに相当し、定数は_public static final_フィールドに相当します。

5. _ @ JvmOverloads_

  • Kotlinでは、関数のパラメーターにデフォルト値を提供できます。 **これは、必要なオーバーロードの数を減らし、関数呼び出しを短くします。

    次の名前付きオブジェクトを見てみましょう。
object MessageBroker {
    @JvmStatic
    fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
        return ArrayList()
    }
}
_findMessages_を呼び出すには、デフォルト値を持つパラメーターを右から左に順番に省略することにより、複数の異なる方法で実行できます。
MessageBroker.findMessages("me", "text", 5);
MessageBroker.findMessages("me", "text");
MessageBroker.findMessages("me");
デフォルト値がないため、最初のパラメーター_sender_の値をスキップできないことに注意してください。
ただし、Javaからは、すべてのパラメーターの値を提供する必要があります。
MessageBroker.findMessages("me", "text", 10);
  • Kotlin関数をJavaで使用する場合、* デフォルトのパラメーター値の利点はありませんが、すべての値を明示的に提供する必要があることがわかります。

    Javaでも複数のメソッドオーバーロードを使用する場合は、_ @ JvmOverloads_アノテーションを追加できます。
@JvmStatic
@JvmOverloads
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
    return ArrayList()
}
*注釈はKotlinコンパイラーに、デフォルト値を使用して* * _n_パラメーターの_(n 1)_オーバーロードメソッドを生成するように指示します。*
  1. すべてのパラメーターを持つ1つのオーバーロードメソッド。

  2. 連続して除外することにより、デフォルトパラメータごとに1つのメソッド
    右から左へのデフォルト値を持つパラメーター。

    これらの関数に相当するJavaは次のとおりです。
public static List<Message> findMessages(String sender, String type, int maxResults)
public static List<Message> findMessages(String sender, String type)
public static List<Message> findMessages(String sender)
関数にはデフォルト値を持つ2つのパラメーターがあるため、同じ方法でJavaから呼び出すことができます。
MessageBroker.findMessages("me", "text", 10);
MessageBroker.findMessages("me", "text");
MessageBroker.findMessages("me");

6. _ @ JvmDefault_

Java 8のように、Kotlinでは、インターフェイスのデフォルトメソッドを定義できます。
interface Document {
    fun getType() = "document"
}

class TextDocument : Document

fun main() {
    val myDocument = TextDocument()
    println("${myDocument.getType()}")
}
これは、Java 7 JVMで実行する場合でも機能します。 Kotlinは、デフォルトのメソッドを実装する静的内部クラスを実装することでこれを実現します。
このチュートリアルでは、生成されたバイトコードを詳しく調べません。 代わりに、これらのインターフェイスをJavaで使用する方法に焦点を当てます。*さらに、インターフェイス委任に対する_ @ JvmDefault_の影響を確認します。*

6.1. KotlinのデフォルトインターフェイスメソッドとJava

インターフェースを実装するJavaクラスを見てみましょう。
public class HtmlDocument implements Document {
}
次のようなコンパイルエラーが発生します。
Class 'HtmlDocument' must either be declared abstract or implement abstract method 'getType()' in 'Document'
Java 7以前でこれを行う場合、デフォルトのインターフェイスメソッドはJava 8の新機能であったため、これを期待しています。 ただし、Java 8では、デフォルトの実装を使用できるようにする予定です。 メソッドに注釈を付けることでこれを実現できます。
interface Document {
    @JvmDefault
    fun getType() = "document"
}
_ @ JvmDefault_アノテーションを使用できるようにするには、次の2つの引数のいずれかをKotlinコンパイラーに追加する必要があります。
  • * Xjvm-default = enable – *インターフェースのデフォルトのメソッドのみが
    生成された

  • * Xjvm-default = compatibility – *デフォルトのメソッドと静的メソッドの両方
    内部クラスが生成されます

6.2. _ @ JvmDefault_およびインターフェイスの委任

  • _ @ JvmDefault_アノテーションが付けられたメソッドは、インターフェイスの委任から除外されます。 これは、注釈がKotlin自体でそのようなメソッドを使用できる方法も変更することを意味します。*

    それが実際に何を意味するのか見てみましょう。
    クラス_TextDocument_は、インターフェース_Document_を実装し、_getType()_をオーバーライドします。
interface Document {
    @JvmDefault
    fun getTypeDefault() = "document"

    fun getType() = "document"
}

class TextDocument : Document {
    override fun getType() = "text"
}
実装を_TextDocument:_に委任する別のクラスを定義できます
class XmlDocument(d : Document) : Document by d
両方のクラスは、_TextDocument_クラスで実装されているメソッドを使用します。
@Test
fun testDefaultMethod() {
    val myDocument = TextDocument()
    val myTextDocument = XmlDocument(myDocument)

    assertEquals("text", myDocument.getType())
    assertEquals("text", myTextDocument.getType())
    assertEquals("document", myTextDocument.getTypeDefault())
}
両方のクラスのメソッド_getType()_は同じ値を返しますが、_ @ JvmDefault_アノテーションが付けられたメソッド_getTypeDefault()_は異なる値を返します。 これは、* _ getType()_が委任されておらず*、_ XmlDocument_がメソッドをオーバーライドしないため、デフォルトの実装が呼び出されるためです。

7. _ @ Throws_

7.1. Kotlinの例外

Kotlinにはチェック例外がありません。つまり、周囲の_try-catch_は常にオプションです。
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}
両方のクラスのgetType()メソッドは同じ値を返し、@ JvmDefaultの注釈であるgetTypeDefaultメソッドは異なる値を返します。
_try-catch_の有無にかかわらず、関数を呼び出すことができます。
MessageBroker.findMessages("me")

try {
    MessageBroker.findMessages("me")
} catch(e : IllegalArgumentException) {
}
JavaからKotlin関数を呼び出す場合、_try-catch_もオプションです。
MessageBroker.findMessages("");

try {
    MessageBroker.findMessages("");
} catch (Exception e) {
    e.printStackTrace();
}

7.2. Javaで使用するチェック済み例外の作成

  • Javaで関数を使用するときにチェック例外を取得する場合は、_ @ Throws_アノテーションを追加できます。*

@Throws(Exception::class)
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}
この注釈は、Kotlinコンパイラーに次のものと同等のものを作成するよう指示します。
public static List<Message> findMessage(String sender, String type, int maxResult) throws Exception {
    if(sender.length() == 0) {
        throw new Exception();
    }
    return  new ArrayList<>();
}
Javaで_try-catch_を省略すると、コンパイルエラーが発生します。
Unhandled exception: java.lang.Exception
*ただし、Kotlinで関数を使用する場合は、_try-catch_を省略できます。これは、アノテーションがJavaから呼び出される方法を変更するだけだからです。*

8. _ @ JvmWildcard_および_ @ JvmSuppressWildcards_

8.1. 汎用ワイルドカード

Javaでは、継承と組み合わせてジェネリックを処理するワイルドカードが必要です。 _Integer_は_Number_を拡張しますが、次の割り当てによりコンパイルエラーが発生します。
List<Number> numberList = new ArrayList<Integer>();
ワイルドカードを使用して問題を解決できます。
List<? extends Number> numberList = new ArrayList<Integer>();
Kotlinにはワイルドカードはなく、次のように簡単に記述できます。
val numberList : List<Number> = ArrayList<Int>()
これは、*このようなリストを含むKotlinクラスを使用するとどうなるかという質問につながります。*
例として、パラメーターとしてリストを受け取る関数を見てみましょう。
fun transformList(list : List<Number>) : List<Number>
Kotlinでは、_Number_を拡張するパラメーターを持つ任意のリストでこの関数を呼び出すことができます。
val list = transformList(ArrayList<Long>())
もちろん、Javaからこの関数を呼び出したい場合は、これも可能になると期待しています。 Javaの観点から見ると、関数は次のように見えるため、これは実際に機能します。
public List<Number> transformList(List<? extends Number> list)
Kotlinコンパイラーは、ワイルドカードを使用して関数を暗黙的に作成しました。
これがいつ起こるのか、そうでないのかを見てみましょう。

8.2. コトリンのワイルドカード規則

ここで、基本的なルールは、デフォルトでは、Kotlinは必要な場合にのみワイルドカードを生成することです。
*型パラメーターが最終クラスの場合、ワイルドカードはありません:*
fun transformList(list : List<String>) // Kotlin
public void transformList(List<String> list) // Java
ここでは、â_œ_は必要ありませんか? クラスは_String_を拡張できないため、Number_âを拡張します。 ただし、クラスを拡張できる場合は、ワイルドカードがあります。 _Number_は_final_クラスではないため、次のようになります。
fun transformList(list : List<Number>) // Kotlin
public void transformList(List<? extends Number> list) // Java
*さらに、戻り値の型にはワイルドカードがありません:*
fun transformList() : List<Number> // Kotlin
public List<Number> transformList() // Java

8.3. ワイルドカード設定

*ただし、デフォルトの動作を変更したい場合があります。*そのためには、JVMアノテーションを使用できます。 _JvmWildcard_は、注釈付きの型パラメーターが常にワイルドカードを取得するようにします。 And _JvmSuppressWildcards_は、ワイルドカードを取得しないようにします。
上記の関数に注釈を付けましょう:
fun transformList(list : List<@JvmSuppressWildcards Number>) : List<@JvmWildcard Number>
そして、アノテーションの効果を示すJavaから見たメソッドシグネチャを見てください。
public List<? extends Number> transformListInverseWildcards(List<Number> list)
*最後に、https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html [Javaでは戻り型のワイルドカードは一般的に悪い習慣です]に注意する必要がありますが、 *それから、Kotlin JVMアノテーションが役立ちます。

9. _ @ JvmMultifileClass_

すべてのトップレベルの宣言がコンパイルされるクラスの名前を定義するために、_ @ JvmName_アノテーションをファイルに適用する方法をすでに見ました。 もちろん、*提供する名前は一意でなければなりません。*
同じパッケージに2つのKotlinファイルがあり、両方とも_ @ JvmName_アノテーションと同じターゲットクラス名を持つとします。 次のコードを含む最初のファイル_MessageConverter.kt_:
@file:JvmName("MessageHelper")
package jvmannotation
convert(message: Message) = // conversion code
次のコードを含む2番目のファイル_Message.kt_:
@file:JvmName("MessageHelper")
package jvmannotation
fun archiveMessage() =  // archiving code
これを行うと、エラーが発生します。
// Error:(1, 1) Kotlin: Duplicate JVM class name 'jvmannotation/MessageHelper'
//  generated from: package-fragment jvmannotation, package-fragment jvmannotation
これは、Kotlinコンパイラーが同じ名前の2つのクラスを作成しようとするためです。
  • MessageHelper.class_という名前の1つのクラスに両方のファイルのすべてのトップレベル宣言を結合する場合、 @ JvmMultifileClass_を両方のファイルに追加できます。*

    _MessageConverter.kt_に_ @ JvmMultifileClass_を追加しましょう。
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotationfun
convert(message: Message) = // conversion code
次に、_Message.kt_にも追加します。
@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotation
fun archiveMessage() =  // archiving code
Javaでは、両方のKotlinファイルからのすべてのトップレベル宣言が_MessageHelper_に統合されていることがわかります。
MessageHelper.archiveMessage();
MessageHelper.convert(new Message());
*注釈は、Kotlinから関数を呼び出す方法には影響しません。*

10. _ @ JvmPackageName_

すべてのJVMプラットフォームアノテーションは、パッケージ_kotlin.jvm_で定義されています。 このパッケージを見ると、別の注釈があります:_ @ JvmPackageName_。
*このアノテーションは、_ @ file:JvmName_が生成されたクラスファイルの名前を変更するように、パッケージ名を変更できます。*
ただし、注釈は内部としてマークされているため、Kotlinライブラリクラスの外部では使用できません。 したがって、この記事ではこれ以上詳しくは説明しません。

11. 注釈ターゲットのチートシート

  • Kotlinで利用可能なJVMアノテーションに関するすべての情報を見つけるには、https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/index.html [公式ドキュメント]を参照してください。 すべての詳細を見つけるのに適した別の場所は、コード自体です。*定義(JavaDocを含む)は、_kotlin-stdlib.jar_の_kotlin.jvm_パッケージにあります。

    次の表は、どのアノテーションをどのターゲットで使用できるかをまとめたものです。
    link:/uploads/kotlin-jmv-anns-100x19.png%20100w []

12. 結論

この記事では、KotlinのJVMアノテーションを確認しました。 サンプルの完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-kotlin-2[GitHubで]で入手できます。