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.classMessage.classの2つのクラスファイルを作成します。 これで、Javaから両方を呼び出すことができます。

Message m = new Message();
String me = MessageKt.getMyName();

MessageKt.class に別の名前を付けたい場合は、ファイルの最初の行に@JvmNameアノテーションを追加できます:

@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. 関数名の競合

最初のユースケースは、自動生成されたゲッターまたはセッターメソッドと同じ名前の関数です。

次のコード:

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が自動的にgetterメソッドを生成し、同じ名前の追加関数を使用できないためです。

その名前の関数が必要な場合は、 @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番目の使用例は、一般的な型消去が原因で名前が衝突する場合です。

ここでは、簡単な例を見ていきます。 次の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の両方の関数に同じ名前を付けたい場合は、関数の1つに@JvmNameで注釈を付けることができます。

@JvmName("setReceiverIds")
fun setReceivers(receiverNames : List<Int>) {
}

これで、Kotlinは両方のシグネチャを異なるものと見なすため、宣言された名前 setReceivers()を使用してKotlinから両方の関数を呼び出すことができます。 Javaからは、 setReceivers() setReceiverIds()という2つの別々の名前で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();

生成されたgetterまたはsetterメソッドの名前を変更する場合は、@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にアクセスできます。

Message message = new Message();
boolean isEncrypted = message.isEncrypted();
boolean hasAttachment = message.getHasAttachment();

ご覧のとおり、コンパイラはisEncryptedのgetterメソッドのプレフィックスを付けません。getIsEncrypted()を使用してゲッターを使用するのは不自然に聞こえるので、これは予想されることのようです。

ただし、これはis。で始まるプロパティにのみ適用されます。getHasAttachment()はまだあります。 ここで、@JvmNameアノテーションを追加できます。

@get:JvmName("hasAttachment")
var hasAttachment = true

そして、よりJavaの慣用的なゲッターを取得します。

boolean hasAttachment = message.hasAttachment();

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

アノテーションは、適切なアクセス権を持つクラスメンバーにのみ適用できることに注意してください。

@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コンパイラはアノテーションを無視し、getterメソッドまたはsetterメソッドを生成しません。

4. @ JvmStaticおよび@JvmField

@JvmField@JvmSyntheticアノテーションについて説明している記事がすでに2つあるため、ここではそれらについて詳しく説明しません。

ただし、 @JvmField をざっと見て、定数と@JvmStaticアノテーションの違いを指摘します。

4.1. @JvmStatic

@JvmStatic アノテーションは、名前付きオブジェクトまたはコンパニオンオブジェクトの関数またはプロパティに適用できます。

注釈のないMessageBrokerから始めましょう。

object MessageBroker {
    var totalMessagesSent = 0
    fun clearAllMessages() { }
}

Kotlinでは、静的な方法でこれらのプロパティと関数にアクセスできます。

val total = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()

ただし、Javaで同じことを行う場合は、そのオブジェクトのインスタンスを介して行う必要があります。

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および定数

Kotlinの@JvmField @JvmStatic 、および 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 で注釈が付けられたプロパティは、 privatestaticフィールドおよび対応するgetterメソッドとsetterメソッドと同等であることがわかります。 @JvmField で注釈が付けられたフィールドは、 public static フィールドと同等であり、定数は public staticfinalフィールドと同等です。

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);

JavaでKotlin関数を使用する場合、 デフォルトのパラメータ値ですが、すべての値を明示的に指定する必要があります。

Javaでも複数のメソッドのオーバーロードが必要な場合は、@JvmOverloadsアノテーションを追加できます。

@JvmStatic
@JvmOverloads
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
    return ArrayList()
}

アノテーションは、Kotlinコンパイラに(n + 1)のオーバーロードされたメソッドを生成するように指示します。  n パラメーターとデフォルト値:

  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

Kotlinでは、Java 8と同様に、インターフェイスのデフォルトのメソッドを定義できます。

interface Document {
    fun getType() = "document"
}

class TextDocument : Document

fun main() {
    val myDocument = TextDocument()
    println("${myDocument.getType()}")
}

これは、Java7JVMで実行している場合でも機能します。 Kotlinは、デフォルトのメソッドを実装する静的内部クラスを実装することでこれを実現します。

このチュートリアルでは、生成されたバイトコードについて詳しくは説明しません。 代わりに、Javaでこれらのインターフェースを使用する方法に焦点を当てます。さらに、インターフェースの委任に対する@JvmDefaultの影響を確認します。

先に進む前に、 Kotlin 1.5以降、@ JvmDefaultアノテーションは廃止され、すべてまたはすべての互換性である新しい-Xjvm-defaultモードを使用するようになりました。 これらのモードは、非抽象Kotlinインターフェースメンバーに対してJVMデフォルトメソッドを生成する必要があることを指定します。

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

インターフェイスを実装するJavaクラスを見てみましょう。

public class HtmlDocument implements Document {
}

次のようなコンパイルエラーが発生します。

Class 'HtmlDocument' must either be declared abstract or implement abstract method 'getType()' in 'Document'

これをJava7以下で行う場合、デフォルトのインターフェースメソッドが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で関数を使用する場合でも、アノテーションはJavaからの呼び出し方法を変更するだけなので、try-catchを省略できます。

8. @JvmWildcardおよび@JvmSuppressWildcards

8.1. 一般的なワイルドカード

Javaでは、継承と組み合わせてジェネリックを処理するためにワイルドカードが必要です。 IntegerNumberを拡張しますが、次の割り当てによりコンパイルエラーが発生します。

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のワイルドカードルール

ここでの基本的なルールは、デフォルトでは、Kotlinは必要な場合にのみワイルドカードを生成するというものです。

タイプパラメータが最終クラスの場合、ワイルドカードはありません。

fun transformList(list : List<String>) // Kotlin
public void transformList(List<String> list) // Java

ここでは、「 ? 番号を拡張します 「、クラスを拡張できないため 。 ただし、クラスを拡張できる場合は、ワイルドカードを使用します。 Numberfinalクラスではないため、次のようになります。

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 は、注釈付きタイプパラメーターが常にワイルドカードを取得することを保証します。 また、 JvmSuppressWildcards は、ワイルドカードを取得しないことを保証します。

上記の関数に注釈を付けましょう。

fun transformList(list : List<@JvmSuppressWildcards Number>) : List<@JvmWildcard Number>

そして、Javaから見たメソッドのシグネチャを見てください。これは、アノテーションの効果を示しています。

public List<? extends Number> transformListInverseWildcards(List<Number> list)

最後に、戻り型のワイルドカードはJavaでは一般的に悪い習慣ですが、必要な場合があることに注意してください。次に、KotlinJVMアノテーションが役立ちます。

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という名前の単一のクラスに結合する場合は、@JvmMultifileClassを両方のファイルに追加できます。

@JvmMultifileClassMessageConverter.ktに追加しましょう。

@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アノテーションに関するすべての情報を見つけるための良い情報源は、公式ドキュメントです。 すべての詳細を見つけるもう1つの良い場所は、コード自体です。 定義(JavaDocを含む)はパッケージに含まれています kotlin.jvm kotlin-stdlib.jar

次の表は、どのアノテーションをどのターゲットで使用できるかをまとめたものです。

12. 結論

この記事では、KotlinのJVMアノテーションについて説明しました。 例の完全なソースコードは、GitHubから入手できます。