Kotlin への変換

この Codelab では、Java から Kotlin にコードを変換する方法を学びます。また、Kotlin 言語の慣例と、それに沿うようにコードを記述する方法についても学習します。

この Codelab は、現在 Java を使用し、Kotlin へのプロジェクトの移行を検討しているデベロッパーに適しています。最初に、IDE を使用していくつかの Java クラスを Kotlin に変換します。次に、変換後のコードをより慣用的なものに改善する方法と、よくある問題を回避する方法を学びます。

学習内容

Java を Kotlin に変換する方法を学びます。その過程で、以下の Kotlin 言語の機能とコンセプトについて学習します。

  • null 値許容の処理
  • シングルトンの実装
  • データクラス
  • 文字列の処理
  • Elvis 演算子
  • 分解
  • プロパティとバッキング プロパティ
  • デフォルトの引数と名前付きパラメータ
  • コレクションの操作
  • 拡張関数
  • 最上位の関数とパラメータ
  • letapplywithrun の各キーワード

前提条件

Java に精通している必要があります。

必要なもの

新しいプロジェクトの作成

IntelliJ IDEA を使用している場合は、Kotlin/JVM で新しい Java プロジェクトを作成します。

Android Studio を使用している場合は、Activity なしで新しいプロジェクトを作成します。最小 SDK には任意の値を設定できます。設定した値は結果には影響しません。

コード

User モデル オブジェクトと、Repository シングルトン クラスを作成します。このクラスは User オブジェクトと連携して、ユーザーのリストとフォーマットされたユーザー名を公開します。

User.java という新しいファイルを app/java/<パッケージ名> に作成し、次のコードを貼り付けます。

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

IDE に、@Nullable が定義されていないというメッセージが表示されます。Android Studio を使用している場合は androidx.annotation.Nullable、IntelliJ を使用している場合は org.jetbrains.annotations.Nullable をインポートします。

Repository.java という名前の新しいファイルを作成し、次のコードを貼り付けます。

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

IDE では Java コードから Kotlin コードへの変換がほぼ自動的に行われますが、一部で手動操作が必要になる場合もあります。IDE で最初の変換を実行してみましょう。次に生成されたコードを確認して、変換内容と、そのように変換された理由を把握します。

User.java ファイルに移動し、メニューバー -> [Code] -> [Convert Java File to Kotlin File] を選択して Kotlin に変換します。

IDE で変換後に修正するよう求められた場合は、[Yes] を押します。

25e57db5b8e76557.png

次の Kotlin コードが表示されます。

class User(var firstName: String?, var lastName: String?)

User.java の名前が User.kt に変更されたことを確認します(Kotlin ファイルの拡張子は .kt です)。

Java の User クラスでは、firstNamelastName の 2 つのプロパティがありました。各プロパティにゲッター メソッドとセッター メソッドがあるため、値は可変となっていました。Kotlin での可変変数のキーワードは var であるため、コンバータはこれらのプロパティに var を使用します。仮に Java プロパティにゲッターのみがあったとすると、これらのプロパティは不変となり、val 変数として宣言されていたはずです。val は、Java の final キーワードと似ています。

Kotlin と Java の主な違いの 1 つは、Kotlin では、変数が null 値を受け入れるかどうかを明示的に指定することです。これは、型宣言に ? を追加することによって行います。

firstNamelastName を null 値許容としてマークしていたため、自動コンバータによってこれらのプロパティが null 値許容として String? で自動的にマークされています。Java メンバーに(org.jetbrains.annotations.NotNull または androidx.annotation.NonNull を使用して) 非 null のアノテーションを付けると、コンバータによって認識され、Kotlin でもそのフィールドが非 null になります。

基本的な変換はすでに完了していますが、このコードはより慣用的な方法で記述できます。その方法を見てみましょう。

Data クラス

User クラスは、データのみを保持します。Kotlin には、このロールを持つクラスに使用する data というキーワードがあります。このクラスを data クラスとしてマークすると、コンパイラによって自動的にゲッターとセッターが作成されます。また、equals()hashCode()toString() 関数も取得されます。

data キーワードを User クラスに追加しましょう。

data class User(var firstName: String, var lastName: String)

Java と同様に、Kotlin では 1 つのプライマリ コンストラクタと、1 つまたは複数のセカンダリ コンストラクタを使用できます。上記の例は、User クラスのプライマリ コンストラクタです。複数のコンストラクタを持つ Java クラスを変換する場合、コンバータは Kotlin でも自動的に複数のコンストラクタを作成します。コンストラクタは constructor キーワードを使用して定義されます。

このクラスのインスタンスを作成する場合は、次のようにします。

val user1 = User("Jane", "Doe")

等価性

Kotlin には、次の 2 種類の等価性があります。

  • 構造の等価性では == 演算子を使用して equals() を呼び出し、2 つのインスタンスが等しいかどうかを判断します。
  • 参照の等価性では === 演算子を使用して、2 つの参照が同じオブジェクトを指しているかどうかをチェックします。

データクラスのプライマリ コンストラクタで定義したプロパティは、構造の等価性のチェックに使用されます。

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

Kotlin では、関数呼び出しの引数にデフォルト値を割り当てることができます。デフォルト値は、引数を省略した場合に使用されます。Kotlin ではコンストラクタも関数であるため、デフォルトの引数を使用して、lastName のデフォルト値が null であることを指定できます。これを行うには、nulllastName に割り当てます。

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

Kotlin では、関数の呼び出し時に、引数にラベルを付けることができます。

val john = User(firstName = "John", lastName = "Doe")

別のユースケースで、firstName のデフォルト値として null が指定されていて、lastName のデフォルト値は指定されていないとします。この場合、デフォルト パラメータがデフォルト値のないパラメータの前に来るため、名前付き引数を指定して関数を呼び出す必要があります。

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Kotlin コードでは、デフォルト値は重要な、頻繁に使用されるコンセプトです。この Codelab では、常に User オブジェクト宣言で姓と名を指定するため、デフォルト値は不要です。

Codelab を続ける前に、User クラスが data クラスであることを確認してください。次に、Repository クラスを Kotlin に変換します。自動変換の結果は次のようになります。

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

自動コンバータによって行われた処理を見てみましょう。

  • 宣言時にオブジェクトがインスタンス化されなかったため、users のリストは null 値許容になっています。
  • getUsers() などの Kotlin 関数が fun 修飾子で宣言されています。
  • getFormattedUserNames() メソッドが formattedUserNames というプロパティになっています。
  • ユーザーリストの反復処理(当初は getFormattedUserNames( の一部でした)の構文が、Java の構文とは異なっています。
  • static フィールドが companion object ブロックの一部になっています。
  • init ブロックが追加されています。

先に進む前に、コードを少しクリーンアップしましょう。コンストラクタを見ると、コンバータによって users リストが、null 値許容オブジェクトを保持する可変リストに変換されていることがわかります。実際にはリストが null である可能性がありますが、ここでは null ユーザーを保持できないと仮定して、次のようにクリーンアップを行います。

  • users 型宣言内の User? から ? を削除します。
  • getUsers() の戻り値の型の User? から ? を削除し、List<User>? を返すようにします。

init ブロック

Kotlin ではプライマリ コンストラクタにコードを含めることができないため、初期化コードは init ブロックに配置されますが、機能としては同じです。

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

init コードの多くは、プロパティの初期化を処理します。これは、プロパティの宣言で行うこともできます。たとえば、Repository クラスの Kotlin バージョンでは、宣言で user プロパティが初期化されています。

private var users: MutableList<User>? = null

Kotlin の static プロパティおよびメソッド

Java では、フィールドまたは関数に対して static キーワードを使用して、これらがクラスに属しているものの、クラスのインスタンスには属していないことを示します。この理由から、Repository クラスに静的 INSTANCE フィールドを作成していました。Kotlin でこれと同じ役割を果たすのは、companion object ブロックです。静的フィールドと静的関数もここで宣言します。コンバーターによりコンパニオン オブジェクト ブロックが作成され、ここに INSTANCE フィールドが移動されています。

シングルトンの処理

Repository クラスに必要なインスタンスは 1 つだけなので、Java ではシングルトン パターンを使用しました。Kotlin では、class キーワードを object に置き換えることで、コンパイラ レベルでこのパターンを適用できます。

プライベート コンストラクタを削除し、クラス定義を object Repository に置き換えます。コンパニオン オブジェクトも削除します。

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

object クラスを使用する場合、次のように関数とプロパティをオブジェクトで直接呼び出すことができます。

val formattedUserNames = Repository.formattedUserNames

プロパティに可視性修飾子がない場合は、Repository オブジェクトの formattedUserNames プロパティの場合と同様に、デフォルトで公開されます。

Repository クラスを Kotlin に変換した際、自動コンバータによってユーザーのリストが null 値許容になりました。これは、宣言時にオブジェクトに初期化されなかったためです。したがって、users オブジェクトを使用する場合は、必ず非 null アサーション演算子 !! を使用する必要があります(変換されたコード全体では users!!user!! が表示されます)。!! 演算子によって任意の変数が非 null 型に変換されるため、そのプロパティにアクセスしたり、関数を呼び出すことができます。ただし、変数値が実際に null の場合は例外がスローされます。!! を使用すると、実行時に例外がスローされるリスクがあります。

代わりに、次のいずれかの方法で null 値許容を処理することをおすすめします。

  • null チェックを行う(if (users != null) {...}
  • Elvis 演算子 ?: を使用する(この Codelab で後述します)
  • Kotlin の標準関数を使用する(この Codelab で後述します)

この例では、オブジェクトの作成後に(init ブロックで)すぐに初期化されるため、ユーザーのリストを null 値許容にする必要はありません。したがって、users オブジェクトを宣言する際に直接インスタンス化できます。

コレクション型のインスタンスを作成する際に、Kotlin で提供されているヘルパー関数を使用することで、コードを読みやすくして柔軟性を高めることができます。ここでは、usersMutableList を使用しています。

private var users: MutableList<User>? = null

簡素化のために、mutableListOf() 関数を使用してリストの要素タイプを指定できます。mutableListOf<User>() は、User オブジェクトを保持できる空のリストを作成します。これで変数のデータ型がコンパイラによって推定されるようになったため、users プロパティの明示的な型宣言を削除します。

private val users = mutableListOf<User>()

また、users にはユーザーのリストへの不変参照が含まれるため、varval に変更しました。参照は不変ですが、リスト自体は可変です(要素の追加や削除が可能です)。

users 変数はすでに初期化されているため、この初期化を init ブロックから削除します。

users = ArrayList<Any?>()

init ブロックは次のようになります。

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

上記の変更により users プロパティが非 null になったため、不要な !! 演算子をすべて削除できます。Android Studio には引き続きコンパイル エラーが表示されますが、Codelab の以降のステップに進むとこのエラーは解消されます。

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

また、userNames の値に対して、ArrayList の型を Strings として指定することで推定されるようになるため、宣言内の明示的な型を削除できます。

val userNames = ArrayList<String>(users.size)

分解

Kotlin では、分解宣言という構文を使用して、オブジェクトを複数の変数に分解できます。分解宣言では複数の変数を作成して、個別に使用することが可能です。

たとえば、data クラスは分解をサポートしているため、for ループの User オブジェクトを (firstName, lastName) に分解できます。これにより、firstNamelastName の値を直接処理できます。下記のように for ループを更新します。user.firstName のすべてのインスタンスを firstName に置き換え、user.lastNamelastName に置き換えます。

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if 式

userName のリスト内の名前は、まだ目的の形式にはなっていません。lastNamefirstName はどちらも null となる可能性があるため、フォーマットされたユーザー名のリストを作成する際、null 値許容を処理する必要があります。姓と名のいずれかがない場合は、"Unknown" を表示します。name 変数は設定後に変更されることはないため、var の代わりに val を使用できます。最初にこの変更を行います。

val name: String

name 変数を設定するコードを見てみましょう。変数がイコールと、コードの if / else ブロックに設定されているのは見たことがないかもしれません。これが可能なのは、Kotlin では ifwhenforwhile が式であり、値を返すためです。if ステートメントの最後の行が name に割り当てられます。このブロックの用途は、name 値を初期化することだけです。

基本的に、ここで示すロジックは、lastName が null の場合、namefirstName または "Unknown" に設定されるということです。

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Elvis 演算子

Elvis 演算子 ?: を使用することで、このコードをより慣用的に記述できます。Elvis 演算子は、左辺の式が null でない場合は左辺の式を返し、左辺の式が null の場合は右辺の式を返します。

したがって、次のコードでは、左辺の式が null でない場合は firstName が返されます。firstName が null の場合、右辺の値である "Unknown" が返されます。

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

Kotlin では、文字列テンプレートを使って String を簡単に操作できます。文字列テンプレートで変数の前に $ 記号を使用することで、文字列宣言内で変数を参照できるようになります。また、式を { } 内に配置してその前に $ 記号を使用することにより、文字列宣言内に式を配置することも可能です(例: ${user.firstName})。

コードでは現在、文字列の連結を使用して、firstNamelastName を 1 つのユーザー名に結合しています。

if (firstName != null) {
    firstName + " " + lastName
}

代わりに、文字列の連結を次のように置き換えます。

if (firstName != null) {
    "$firstName $lastName"
}

文字列テンプレートを使用すると、コードを簡素化できます。

より慣用的な方法でコードを記述できる場合、IDE に警告が表示されます。コード内に波線で下線が引かれ、その部分にカーソルを合わせると、コードのリファクタリング方法が表示されます。

現在、name 宣言と割り当てを統合できることを示す警告が表示されているはずです。これをリファクタリングしてみましょう。name 変数の型は推測できるため、明示的な String 型宣言を削除できます。formattedUserNames は次のようになります。

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

さらに調整を加えることもできます。UI ロジックで姓と名が見つからない場合は "Unknown" が表示されるため、null オブジェクトをサポートしていません。そのため、formattedUserNames のデータ型の List<String?>List<String> に置き換えます。

val formattedUserNames: List<String>

formattedUserNames ゲッターの詳細と、ゲッターをより慣用的に記述する方法を確認しましょう。現在、コードは次の処理を行います。

  • 文字列の新しいリストを作成する
  • ユーザーのリストの反復処理を行う
  • ユーザーの姓名に基づいて、各ユーザーのフォーマットされた名前を作成する
  • 新しく作成されたリストを返す
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

Kotlin は、Java Collections API の機能を拡張することで、開発を高速かつ安全に行えるようにするコレクション変換の幅広いリストを提供しています。そのうちの 1 つが map 関数です。この関数は、特定の変換関数を元のリストの各要素に適用した結果を含む、新しいリストを返します。したがって、新しいリストを作成して、ユーザーのリストの反復処理を手動で行う代わりに、map 関数を使って、for ループ内のロジックを map 本文内に移動させることができます。デフォルトでは、map で使用されている現在のリスト項目の名前は it ですが、読みやすくするために it を独自の変数名で置き換えることができます。ここでは、user という名前を付けます。

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

ここでは、Elvis 演算子を使用して、user.lastName が null の場合に "Unknown" を返しています。これは、user.lastNameString? 型であり、String には name が必要であるためです。

...
else {
    user.lastName ?: "Unknown"
}
...

これをさらに簡素化するために、name 変数を完全に削除します。

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

自動コンバータが getFormattedUserNames() 関数をカスタムのゲッターを持つ formattedUserNames というプロパティに置き換えたことを確認しました。内部では、Kotlin は引き続き List を返す getFormattedUserNames() メソッドを生成します。

Java では、ゲッター関数とセッター関数を介してクラス プロパティを公開します。Kotlin を使用すると、フィールドによって表現されるクラスのプロパティと、関数によって表現され、クラスが実行できる機能やアクションを、より適切に区別できるようになります。この例では、Repository クラスは非常にシンプルでアクションを行わないため、フィールドだけが含まれます。

Java では getFormattedUserNames() 関数でトリガーされていたロジックが、Kotlin では formattedUserNames プロパティのゲッターを呼び出したときにトリガーされるようになりました。

formattedUserNames プロパティに対応するフィールドを明示的に含んではいませんが、Kotlin には、必要に応じてカスタムのゲッターやセッターからアクセスできる field という名前の自動バッキング フィールドが用意されています。

ただし、自動バッキング フィールドでは提供されない追加機能が必要な場合があります。

例を使って確認しましょう。

Repository クラス内には、次のように Java コードから生成された関数 getUsers() で公開されているユーザーの可変リストがあります。

fun getUsers(): List<User>? {
    return users
}

ここでの問題は、users を返すことで Repository クラスのすべてのユーザーがユーザーのリストを変更できてしまう点であり、あまりよくありません。この問題は、バッキング プロパティを使用して修正できます。

まず、users の名前を _users に変更しましょう。変数名をハイライト表示して右クリックし、[Refactor] > [Rename] から変数名を変更します。次に、ユーザーのリストを返す公開不変プロパティを追加します。これを users とします。

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

この時点で、getUsers() メソッドを削除できます。

上記の変更により、非公開 _users プロパティは公開 users プロパティのバッキング プロパティになっています。Repository クラスの外部では、クラスのユーザーは users を介さないとリストにアクセスできないため、_users リストを変更できません。

完全なコード:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

現時点では、Repository クラスは User オブジェクトのフォーマットされたユーザー名を計算する方法を認識しています。ただし、他のクラスで同じフォーマット ロジックを再利用する場合は、そのパスをコピーして貼り付けるか、User クラスに移動する必要があります。

Kotlin には、クラス、オブジェクト、インターフェースの外部で関数やプロパティを宣言する機能が用意されています。たとえば、List の新しいインスタンスを作成するために使用した mutableListOf() 関数は、Kotlin 標準ライブラリの Collections.kt ですでに定義されています。

Java では、ユーティリティ機能が必要な場合は常に Util クラスを作成し、その機能を静的関数として宣言する必要があります。Kotlin では、クラスを使用せずにトップレベル関数を宣言できます。ただし、Kotlin には拡張関数を作成する機能もあります。拡張関数とは、特定の型を拡張するものの、その型の外部で宣言される関数です。

拡張関数と拡張プロパティの可視性を制限するには、可視性修飾子を使用します。これにより、拡張機能が必要なクラスだけに使用が限定されるため、名前空間が汚染されません。

User クラスの場合、フォーマットされた名前を計算する拡張関数を追加するか、フォーマットされた名前を拡張プロパティで保持できます。次のように Repository クラスの外部で、同じファイル内に追加できます。

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

次に、拡張関数と拡張プロパティを、User クラスの一部であるかのように使用できます。

フォーマットされた名前は User クラスのプロパティであり、Repository クラスの機能ではないため、拡張プロパティを使用します。Repository ファイルは次のようになります。

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Kotlin 標準ライブラリは、拡張関数を使用して複数の Java API の機能を拡張しているため、IterableCollection の機能の多くが拡張関数として実装されています。たとえば、前のステップで使用した map 関数は Iterable の拡張関数です。

Repository クラスのコードで、_users リストに複数の User オブジェクトを追加しています。これらの呼び出しは、Kotlin のスコープ関数を利用することで、より慣用的に記述することができます。

名前に基づいてオブジェクトにアクセスすることなく、特定のオブジェクトのコンテキストでのみコードを実行できるように、Kotlin には 5 つのスコープ関数(letapplywithrunalso)が用意されています。これらの関数を使用すると、コードを読みやすく簡潔にすることができます。すべてのスコープ関数にはレシーバ(this)があります。また、引数(it)を持ったり、値を返したりすることができます。

各関数の用途については、次のクイック リファレンスをご覧ください。

6b9283d411fb6e7b.png

ここでは _users オブジェクトを Repository 内で設定しているため、apply 関数を使用してコードをより慣用的に記述することができます。

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

この Codelab では、コードを Java から Kotlin に変換するために必要な基本事項について説明しました。この変換は開発プラットフォームから独立しており、記述したコードが慣用的な Kotlin になっていることを確認するのに役立ちます。

慣用的な Kotlin により、コードを簡潔でわかりやすく記述することができます。Kotlin が提供するすべての機能を使用して、コードの安全性、簡潔性、読みやすさを向上させる方法は多数あります。たとえば、次のようにユーザーが含まれる _users リストを宣言で直接インスタンス化し、init ブロックを取り除くことで、Repository クラスを最適化することもできます。

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

null 値許容、シングルトン、文字列、コレクションの処理から、拡張関数、トップレベル関数、プロパティ、スコープ関数に至るまで、幅広い内容を取り上げました。最終的には、2 つの Java クラスを次のような 2 つの Kotlin クラスに変換しました。

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Java の各機能と、それらの Kotlin へのマッピングの要約を次に示します。

Java

Kotlin

final オブジェクト

val オブジェクト

equals()

==

==

===

データのみを保持するクラス

data クラス

コンストラクタでの初期化

init ブロックでの初期化

static フィールドおよび関数

companion object で宣言されているフィールドと関数

シングルトン クラス

object

Kotlin の詳細と、プラットフォームでの Kotlin の使用方法について詳しくは、以下のリソースをご覧ください。