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

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

ラボの内容

  • @JvmField@JvmStatic、その他のアノテーションを利用する方法。
  • Java コードから特定の Kotlin 言語機能にアクセスする場合の制限。

前提となる知識

この Codelab はプログラマー向けに用意されており、Java と Kotlin の基本的な知識があることを前提としています。

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

わかりやすくするため、既存のコードベースを表す UseCase.java という単一の .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 には静的コンテキストからはアクセスできません。"

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

このリポジトリは、オブジェクト キーワードを使用して宣言されたシングルトンです。問題は、Kotlin がクラスを静的プロパティおよびメソッドとして公開するのではなく、クラス内で静的インスタンスを生成していることです。

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

Kotlin に静的メソッドとプロパティを生成するには、リポジトリの公開プロパティとメソッドに @JvmStatic アノテーションを付けます。

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 のプロパティとメソッドがエラーを引き起こしなくなります(Repository.BACKUP_PATH を除く)。後ほど使用します。

ここでは、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

Kotlin にパッケージ レベルのメソッドに別の名前を使用するよう指示するには、@file:JvmName アノテーションを使用します。このアノテーションを使用して、クラスに 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 プログラミング言語では、デフォルト値はサポートされていません。この問題を解決するため、Kotlin では @JvmOverloads アノテーションを利用してコンストラクタのオーバーロードを発生させます。

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

User クラスには単一のプライマリ コンストラクタのみがあり、コンストラクタにはアノテーションが含まれていないため、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");

また、displayName の 3 番目のパラメータを含めながら groups のデフォルト値を使用して 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 名は、単に get + プロパティ名になります。これは、User.getHasSystemAccess()User.getDisplayName() で説明したとおりです。ただし、名前が「is」で始まるプロパティは例外です。この場合、ゲッターの Java 名は Kotlin プロパティの名前です。

たとえば、User に次のようなプロパティがあるとします。

val isAdmin get() = //...

次のコマンドで Java からアクセスされます。

boolean userIsAnAdmin = user.isAdmin();

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

セッター(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 ファイルには同様のエラーがもう 1 つあります。

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
}