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

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

あなたが学ぶこと

  • @JvmField@JvmStatic 、およびその他の注釈を利用する方法。
  • Javaコードから特定のKotlin言語機能にアクセスする際の制限。

あなたがすでに知っていなければならないこと

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

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

簡単にするために、 UseCase.javaという単一の.javaファイルがあります。これは、既存のコードベースを表します。

元々Javaで書かれたいくつかの機能をKotlinで書かれた新しいバージョンに置き換えたところを想像してみてください。そして、それを統合し終える必要があります。

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

プロジェクトのコードは、GitHubプロジェクトから複製できます: GitHub

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

Zipをダウンロード

IntelliJ IDEAを使用している場合は、[プロジェクトのインポート]を選択します。

Android Studioを使用している場合は、[プロジェクトのインポート(Gradle、Eclipse ADTなど)]を選択します。

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(...)両方のエラーは同じです:「静的でないコンテキストから静的でないコンテキストにアクセスすることはできません。」

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

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

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

リポジトリのパブリックプロパティとメソッドに@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もはや以外、エラーの原因とされていません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

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

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

ここで、 UseCase.javaを振り返ると、 StringUtils.nameToLogin()エラーが解決されていることがわかります。

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

Kotlinは、パラメーターのデフォルト値をサポートしていますRepository.kt initブロックの内部を見ると、それらがどのように使用されているかがわかりRepository.kt

Repository.kt:

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

User "warlow"の場合、User.ktにデフォルト値が指定されているため、 displayName値の入力をスキップできることがUser.ktます。

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.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.javaていることがわかります。

私たちの次のステップはに壊れ呼び出し修正することですuser.hasSystemAccess()UseCase.getSystemUsers()そのための次のステップに進むか、読み続けて、 @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を作成できusername

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

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

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

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

e33040916c6c32e4.png

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

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 、次の問題に対処しましょうuser.hasSystemAccess()メソッドでのUseCase.getSystemUsers()

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のオートコンプリート機能を使用すると、 getHasSystemAccess()名前が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 getterの命名規則について詳しく知りたい場合は、次のステップに進む前にここを読み続けてください。

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

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

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

実際に関数を呼び出してdisplayNameの値を取得しています。これを確認するには、メニューで[ツール]> [Kotlin]> [Kotlinバイトコードを表示]に移動し、[逆コンパイル]ボタンをクリックします。

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はgetter(およびvarプロパティのsetter)メソッドの生成をスキップし、バッキングフィールドに直接アクセスできます。

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のアノテーションを@JvmFieldから@JvmStaticに変更したくなるかもしれません。

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

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

ここでUseCase.javaを見ると、エラーがUseCase.javaことがわかりますが、 BACKUP_PATHも注意がBACKUP_PATH

Kotlinでは、 constできるタイプは、 intfloatStringなどのプリミティブのみです。ので、この場合は、 BACKUP_PATH文字列で、我々が使用してより良いパフォーマンスを得ることができるconst valではなく、 valで注釈さ@JvmFieldフィールドとして値にアクセスする能力を維持しながら、。

今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.saveAsRepository.kt 、我々はそれが、例外をスローしないことがわかります。どうしたの?

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 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アノテーションを配置すると、 @Throwsすべてのコンパイラエラーが修正されたことがUseCase.javaます。やったー!

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

いいえ! Kotlinは例外をチェックしておらず、メソッドに@Throwsを追加してもそれは変わりません。

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

例外を処理できる場合はそれをキャッチすることは依然として有用ですが、Kotlinは例外を処理するように強制しません。

このコードラボでは、慣用的なJavaコードの記述もサポートするKotlinコードの記述方法の基本について説明しました。

アノテーションを使用して、KotlinがJVMバイトコードを生成する方法を変更する方法について説明しました。

  • @JvmStatic 、静的メンバーとメソッドを生成します。
  • @JvmOverloadsは、デフォルト値を持つ関数のオーバーロードされたメソッドを生成します。
  • @JvmNameは、ゲッターとセッターの名前を変更します。
  • @JvmFieldは、ゲッターやセッターを@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
}