Java から Kotlin コードを呼び出す

この Codelab では、Java コードからシームレスに呼び出せるように Kotlin コードを記述、変更する方法を学習します。

学習内容

  • @JvmField@JvmStatic などのアノテーションを活用する方法。
  • Java コードから特定の Kotlin 言語機能にアクセスする際の制限。

前提となる知識

この Codelab はプログラマーを対象としており、Java と Kotlin の基本的な知識があることを前提としています。

この Codelab では、Java プログラミング言語で記述された大規模なプロジェクトの一部を移行して、新しい Kotlin コードを組み込むことをシミュレートします。

わかりやすくするために、既存のコードベースを表す UseCase.java という名前の 1 つの .java ファイルを使用します。

元々 Java で記述されていた機能を Kotlin で記述された新しいバージョンに置き換えたばかりで、統合を完了する必要があるという状況を想定します。

プロジェクトをインポートする

プロジェクトのコードは、GitHub プロジェクト(GitHub)からクローンできます。

または、次のリンク先にある zip アーカイブからプロジェクトをダウンロードして抽出することもできます。

ZIP をダウンロード

IntelliJ IDEA を使用している場合は、[Import Project] を選択します。

Android Studio を使用している場合は、[Import project (Gradle, Eclipse ADT, etc.)] を選択します。

UseCase.java を開いて、表示されたエラーの解決に取り組みましょう。

問題のある最初の関数は registerGuest です。

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

Repository.getNextGuestId()Repository.addUser(...) の両方のエラーは「Non-static cannot be accessed from a static context.」です。

次に、Kotlin ファイルの 1 つを見てみましょう。Repository.kt ファイルを開きます。

Repository は、object キーワードを使用して宣言されたシングルトンであることがわかります。問題は、Kotlin が静的プロパティとメソッドとして公開するのではなく、クラス内に静的インスタンスを生成していることです。

たとえば、Repository.getNextGuestId()Repository.INSTANCE.getNextGuestId() を使用して参照できますが、より良い方法があります。

Repository の public プロパティとメソッドに @JvmStatic アノテーションを付けることで、Kotlin に静的メソッドとプロパティを生成させることができます。

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

IDE を使用して、コードに @JvmStatic アノテーションを追加します。

UseCase.java に戻すと、Repository.BACKUP_PATH を除き、Repository のプロパティとメソッドはエラーを引き起こさなくなります。これについては後ほど説明します。

次に、registerGuest() メソッドの次のエラーを修正しましょう。

文字列操作用の静的関数がいくつかある StringUtils クラスがあったとします。Kotlin に変換したときに、メソッドを拡張関数に変換しました。Java には拡張関数がないため、Kotlin はこれらのメソッドを静的関数としてコンパイルします。

残念ながら、UseCase.java 内の registerGuest() メソッドを見ると、正しくない部分があります。

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

これは、Kotlin では、これらの「トップレベル」関数またはパッケージレベル関数が、ファイル名に基づく名前のクラス内に配置されるためです。この場合、ファイル名は StringUtils.kt なので、対応するクラスは StringUtilsKt という名前になります。

StringUtils のすべての参照を StringUtilsKt に変更してこのエラーを修正することはできますが、次の理由から理想的ではありません。

  • コードの多くの場所で更新が必要になる可能性があります。
  • 名前自体が不自然です。

そこで、Java コードをリファクタリングするのではなく、これらのメソッドに別の名前を使用するように Kotlin コードを更新しましょう。

StringUtils.Kt を開き、次のパッケージ宣言を見つけます。

package com.google.example.javafriendlykotlin

@file:JvmName アノテーションを使用すると、パッケージ レベルのメソッドに別の名前を使用するように Kotlin に指示できます。このアノテーションを使用して、クラスに StringUtils という名前を付けます。

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

UseCase.java に戻ると、StringUtils.nameToLogin() のエラーが解決されています。

残念ながら、このエラーは User のコンストラクタに渡されるパラメータに関する新しいエラーに置き換えられました。次のステップに進み、UseCase.registerGuest() の最後のエラーを修正しましょう。

Kotlin は、パラメータのデフォルト値をサポートしています。Repository.ktinit ブロックの中を見ると、それらがどのように使用されているかを確認できます。

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

ユーザー「warlow」の場合、User.kt でデフォルト値が指定されているため、displayName の値を入力する必要がないことがわかります。

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

残念ながら、Java からメソッドを呼び出す場合は同じように動作しません。

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Java プログラミング言語ではデフォルト値はサポートされていません。この問題を解決するには、@JvmOverloads アノテーションを使用して、コンストラクタのオーバーロードを生成するように Kotlin に指示します。

まず、User.kt を少し更新する必要があります。

User クラスには プライマリ コンストラクタが 1 つしかなく、コンストラクタにアノテーションが含まれていないため、constructor キーワードは省略されていました。アノテーションを付ける場合は、constructor キーワードを含める必要があります。

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

constructor キーワードが存在する場合、@JvmOverloads アノテーションを追加できます。

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

UseCase.java に戻ると、registerGuest 関数にエラーがなくなっていることがわかります。

次のステップでは、UseCase.getSystemUsers()user.hasSystemAccess() への壊れた呼び出しを修正します。次のステップに進むか、読み進めて @JvmOverloads がエラーを修正するために行った処理について詳しく確認してください。

@JvmOverloads

@JvmOverloads の動作をより深く理解するために、UseCase.java にテストメソッドを作成しましょう。

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

idusername の 2 つのパラメータのみで User を構築できます。

User syrinx = new User(1001, "syrinx");

groups のデフォルト値を引き続き使用しながら、displayName の 3 番目のパラメータを含めることで、User を構築することもできます。

User ione = new User(1002, "ione", "Ione Saldana");

ただし、displayName をスキップして、追加のコードを記述せずに groups の値だけを指定することはできません。

そのため、その行を削除するか、行の先頭に「//」を付けてコメントアウトします。

Kotlin でデフォルト パラメータとデフォルト以外のパラメータを組み合わせる場合は、名前付きパラメータを使用する必要があります。

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

これは、Kotlin がコンストラクタを含む関数のオーバーロードを生成するものの、デフォルト値を持つパラメータごとに 1 つのオーバーロードしか作成しないためです。

UseCase.java を振り返り、次の問題に対処しましょう。メソッド UseCase.getSystemUsers() での user.hasSystemAccess() の呼び出しです。

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

これは興味深いエラーです。クラス User で IDE のオートコンプリート機能を使用すると、hasSystemAccess()getHasSystemAccess() に名前変更されていることがわかります。

この問題を解決するため、Kotlin で val プロパティ hasSystemAccess に別の名前を生成します。これを行うには、@JvmName アノテーションを使用します。User.kt に戻って、どこに適用すべきかを確認しましょう。

アノテーションを適用する方法は 2 つあります。1 つ目は、次のように get() メソッドに直接適用する方法です。

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

これにより、明示的に定義されたゲッターのシグネチャが指定された名前に変更されるよう Kotlin に通知されます。

また、次のように get: 接頭辞を使用してプロパティに適用することもできます。

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

この代替方法は、デフォルトの暗黙的に定義されたゲッターを使用しているプロパティに特に便利です。次に例を示します。

@get:JvmName("isActive")
val active: Boolean

これにより、ゲッターを明示的に定義することなく、ゲッターの名前を変更できます。

この違いはありますが、どちらを使用してもかまいません。どちらの場合も、Kotlin は hasSystemAccess() という名前のゲッターを作成します。

UseCase.java に戻ると、getSystemUsers() にエラーがないことを確認できます。

次のエラーは formatUser() にありますが、Kotlin ゲッターの命名規則について詳しく知りたい場合は、次のステップに進む前にこちらをお読みください。

ゲッターとセッターの命名

Kotlin を記述していると、次のようなコードを記述することを忘れがちです。

val myString = "Logged in as ${user.displayName}")

実際には、関数を呼び出して displayName の値を取得しています。メニューの [Tools] > [Kotlin] > [Show Kotlin Bytecode] に移動し、[Decompile] ボタンをクリックすると、これを確認できます。

String myString = "Logged in as " + user.getDisplayName();

Java からこれらにアクセスする場合は、ゲッターの名前を明示的に記述する必要があります。

ほとんどの場合、Kotlin プロパティのゲッターの Java 名は、User.getHasSystemAccess()User.getDisplayName() で見たように、単に get + プロパティ名になります。ただし、名前が「is」で始まるプロパティは例外です。この場合、ゲッターの Java 名は Kotlin プロパティの名前になります。

たとえば、User のプロパティは次のようになります。

val isAdmin get() = //...

Java からは次のようにアクセスします。

boolean userIsAnAdmin = user.isAdmin();

@JvmName アノテーションを使用すると、Kotlin は、アノテーションが付けられたアイテムに対して、デフォルトではなく指定された名前を持つバイトコードを生成します。

これはセッターでも同様に機能します。セッターの生成名は常に set + プロパティ名になります。たとえば、次のクラスがあるとします。

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

ゲッターはそのままにして、セッターの名前を setRed() から updateRed() に変更するとします。これを行うには、@set:JvmName バージョンを使用します。

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

Java からは、次のように記述できます。

color.updateRed(0.8f);

UseCase.formatUser() は、直接フィールド アクセスを使用して User オブジェクトのプロパティの値を取得します。

Kotlin では、通常、プロパティはゲッターとセッターを介して公開されます。これには、val プロパティが含まれます。

この動作は、@JvmField アノテーションを使用して変更できます。クラスのプロパティに適用すると、Kotlin はゲッター(および var プロパティのセッター)メソッドの生成をスキップし、バッキング フィールドに直接アクセスできるようになります。

User オブジェクトは不変であるため、各プロパティをフィールドとして公開します。そのため、それぞれに @JvmField のアノテーションを付けます。

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

UseCase.formatUser() を確認すると、エラーが修正されていることがわかります。

@JvmField または const

また、UseCase.java ファイルにも同様のエラーがあります。

Repository.saveAs(Repository.BACKUP_PATH);

ここで自動補完を使用すると、Repository.getBACKUP_PATH() があることがわかります。そのため、BACKUP_PATH のアノテーションを @JvmStatic から @JvmField に変更したくなるかもしれません。

試してみましょう。Repository.kt に戻り、アノテーションを更新します。

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

UseCase.java を確認すると、エラーは消えていますが、BACKUP_PATH にもメモが表示されています。

Kotlin では、const にできるのは、intfloatString などのプリミティブ型のみです。この場合、BACKUP_PATH は文字列であるため、@JvmField でアノテーションが付けられた val ではなく const val を使用することで、フィールドとして値にアクセスする機能を維持しながら、パフォーマンスを向上させることができます。

Repository.kt で変更しましょう。

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

UseCase.java を見ると、エラーが 1 つだけ残っていることがわかります。

最終的なエラーは Exception: 'java.io.IOException' is never thrown in the corresponding try block. です。

Repository.ktRepository.saveAs のコードを見ると、例外がスローされていることがわかります。どうしましたか?

Java には「チェック済み例外」という概念があります。これらは、ユーザーがファイル名を誤って入力した場合や、ネットワークが一時的に利用できなくなった場合など、復元可能な例外です。チェック済みの例外がキャッチされた後、デベロッパーは問題を解決する方法についてユーザーにフィードバックを提供できます。

チェック済みの例外はコンパイル時にチェックされるため、メソッドのシグネチャで宣言します。

public void openFile(File file) throws FileNotFoundException {
   // ...
}

一方、Kotlin にはチェック済みの例外がないため、ここで問題が発生しています。

解決策は、Kotlin に Repository.saveAs() のシグネチャに潜在的にスローされる IOException を追加するよう依頼し、JVM バイトコードにチェック例外として含めることです。

これは、Java と Kotlin の相互運用性を高める Kotlin の @Throws アノテーションを使用して行います。Kotlin では、例外は Java と同様に動作しますが、Java とは異なり、Kotlin には未チェックの例外しかありません。したがって、Kotlin 関数が例外をスローすることを Java コードに通知する場合は、Kotlin 関数のシグネチャに @Throws アノテーションを使用する必要があります。Repository.kt file に切り替えて、新しいアノテーションを含めるように saveAs() を更新します。

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

@Throws アノテーションを配置すると、UseCase.java のコンパイラ エラーがすべて修正されていることがわかります。送信しました

Kotlin から saveAs() を呼び出すときに try ブロックと catch ブロックを使用する必要があるかどうか疑問に思うかもしれません。

いいえ。Kotlin にはチェック済みの例外がなく、メソッドに @Throws を追加してもその点は変わりません。

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

例外を処理できる場合はキャッチすることが有用ですが、Kotlin では例外の処理は強制されません。

この Codelab では、自然な Java コードの記述もサポートする Kotlin コードを記述する方法の基本について説明しました。

アノテーションを使用して、Kotlin が JVM バイトコードを生成する方法を変更する方法について説明しました。たとえば、次のような方法があります。

  • @JvmStatic を使用して、静的メンバーと静的メソッドを生成します。
  • @JvmOverloads: デフォルト値を持つ関数についてオーバーロード メソッドを生成します。
  • @JvmName を使用して、ゲッターとセッターの名前を変更します。
  • @JvmField: ゲッターやセッターではなく、フィールドとしてプロパティを直接公開します。
  • @Throws を使用して、チェック済みの例外を宣言します。

最終的なファイルの内容は次のとおりです。

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}