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 なしで新しいプロジェクトを作成します。

コード

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

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

}

プロジェクトの種類に応じて、Android プロジェクトを使用する場合は androidx.annotation.Nullable を、それ以外の場合は 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 コードへのリファクタリングをほぼ自動的に行えますが、一部で手動操作が必要になる場合もあります。まずこれを行ってから、リファクタリングされたコードを確認して、そのようにリファクタリングされた方法と理由を理解します。

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

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

次の 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("John", "Doe")

関数のパラメータの名前は、関数を呼び出すときに指定できます。

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

先に進む前に、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)
   }
}

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

  • init ブロックが追加されました(Repository.kt#L50)
  • static フィールドが companion object ブロックの一部になりました(Repository.kt#L33)
  • 宣言時にオブジェクトがインスタンス化されなかったため、users のリストは null 値許容になります(Repository.kt#L7)
  • getFormattedUserNames() メソッドが formattedUserNames というプロパティになりました(Repository.kt#L11)
  • ユーザーリストの反復処理(当初は getFormattedUserNames( の一部であったもの)の構文は、Java のもの(Repository.kt#L15)とは異なる

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

  • users 型宣言内の User? から ? を削除します。
  • getUsersList<User>? を返す必要があります。

また、自動コンバータはユーザー変数と init ブロックで定義された変数の変数宣言を不必要に 2 行に分割しています。変数宣言を 1 行で記述します。コードは次のようになります。

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

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 バージョンでは、宣言で users プロパティが初期化されています。

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 users = Repository.users

分解

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

たとえば、データクラスは分解をサポートしているため、for ループ内の User オブジェクトを (firstName, lastName) に分解できます。これにより、firstNamelastName の値を直接処理できます。次のように for ループを更新してみましょう。

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

Repository クラスを Kotlin に変換した際、自動コンバータによってユーザーのリストが null 値許容になりました。これは、宣言時にオブジェクトに初期化されなかったためです。users オブジェクトを使用する場合は、必ず非 null アサーション演算子 !! が使用されます。任意の変数を非 null 型に変換し、値が null の場合は例外をスローします。!! を使用すると、実行時に例外がスローされるリスクがあります。

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

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

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

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

private var users: MutableList<User>? = null

簡素化するには、mutableListOf() 関数を使用し、リスト要素型を指定して、init ブロックから ArrayList コンストラクタ呼び出しを削除し、users プロパティの明示的な型宣言を削除します。

private val users = mutableListOf<User>()

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

上記の変更により users プロパティが非 null になったため、不要な !! 演算子をすべて削除できます。

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

users 変数はすでに初期化されているため、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)
}

lastNamefirstName はどちらも null となる可能性があるため、フォーマットされたユーザー名のリストを作成する際、null 値許容を処理する必要があります。どちらの名前もない場合は "Unknown" を表示するため、型宣言から ? を削除して名前を null 以外にできます。

val name: String

lastName が null の場合、namefirstName または "Unknown" になります。

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

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

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

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

Kotlin では、文字列テンプレート を使用してString簡単に作業できます。文字列テンプレートを使用すると、文字列宣言内で変数を参照できます。

自動コンバータは、姓と連結を連結して、文字列内で変数名を直接参照するように $ 記号を使用し、{ } の間に式を配置しました。

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

コードで、文字列の連結を次のように置き換えます。

name = "$firstName $lastName"

Kotlin では、ifwhenforwhile は式を返します。これらは値を返します。また、割り当てを解除する必要があるという警告が if に表示されます。

IDE の提案に従って、両方の if ステートメントの割り当てを解除します。if ステートメントの最後の行が割り当てられます。このように、このブロックの唯一の目的は名前の値の初期化です。

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

次に、name 宣言を割り当てに結合できるという警告が表示されます。これも同様に適用します。name 変数の型は推測できるため、明示的な型宣言を削除できます。formattedUserNames は次のようになります。

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

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
            }
        }

これをさらに簡素化するために、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() 関数でトリガーされたロジックが、formattedUserNames Kotlin プロパティのゲッターを呼び出したときにトリガーされるようになりました。

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

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

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

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

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

まず、users の名前を _users に変更します。次に、ユーザーのリストを返す公開不変プロパティを追加します。users とします。

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

この変更により、非公開 _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() 関数は、標準ライブラリから直接 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 クラスの一部であるかのように使用できます。

フォーマットされた名前はユーザーのプロパティであり、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 リストに追加しています。これらの呼び出しは、スコープ関数を利用することでより慣用的に記述することができます。

名前に基づいてオブジェクトにアクセスすることなく、特定のオブジェクトのコンテキストでのみコードを実行するために、Kotlin では 5 つのスコープ関数(letapplywithrunalso)を作成しました。簡潔で強力なこれらの関数はすべて、レシーバー(this)を持ち、引数(it)を持ち、値を返すことができます。使用する目標に応じてどちらを使用するかを決めます。

その覚え方に役立つ便利なクイック リファレンスをご紹介します。

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

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

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

User.kt

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 の使用方法について詳しくは、以下のリソースをご覧ください。