この Codelab では、Java から Kotlin にコードを変換する方法を学びます。また、Kotlin 言語の慣例と、それに沿うようにコードを記述する方法についても学習します。
この Codelab は、現在 Java を使用し、Kotlin へのプロジェクトの移行を検討しているデベロッパーに適しています。最初に、IDE を使用していくつかの Java クラスを Kotlin に変換します。次に、変換後のコードをより慣用的なものに改善する方法と、よくある問題を回避する方法を学びます。
学習内容
Java を Kotlin に変換する方法を学びます。その過程で、以下の Kotlin 言語の機能とコンセプトについて学習します。
- null 値許容の処理
- シングルトンの実装
- データクラス
- 文字列の処理
- Elvis 演算子
- 分解
- プロパティとバッキング プロパティ
- デフォルトの引数と名前付きパラメータ
- コレクションの操作
- 拡張関数
- 最上位の関数とパラメータ
let
、apply
、with
、run
の各キーワード
前提条件
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] を押します。
次の Kotlin コードが表示されます。
class User(var firstName: String?, var lastName: String?)
User.java
の名前が User.kt
に変更されたことを確認します(Kotlin ファイルの拡張子は .kt です)。
Java の User
クラスでは、firstName
と lastName
の 2 つのプロパティがありました。各プロパティにゲッター メソッドとセッター メソッドがあるため、値は可変となっていました。Kotlin での可変変数のキーワードは var
であるため、コンバータはこれらのプロパティに var
を使用します。仮に Java プロパティにゲッターのみがあったとすると、これらのプロパティは不変となり、val
変数として宣言されていたはずです。val
は、Java の final
キーワードと似ています。
Kotlin と Java の主な違いの 1 つは、Kotlin では、変数が null 値を受け入れるかどうかを明示的に指定することです。これは、型宣言に ?
を追加することによって行います。
firstName
と lastName
を 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
であることを指定できます。これを行うには、null
を lastName
に割り当てます。
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 で提供されているヘルパー関数を使用することで、コードを読みやすくして柔軟性を高めることができます。ここでは、users
に MutableList
を使用しています。
private var users: MutableList<User>? = null
簡素化のために、mutableListOf()
関数を使用してリストの要素タイプを指定できます。mutableListOf<User>()
は、User
オブジェクトを保持できる空のリストを作成します。これで変数のデータ型がコンパイラによって推定されるようになったため、users
プロパティの明示的な型宣言を削除します。
private val users = mutableListOf<User>()
また、users にはユーザーのリストへの不変参照が含まれるため、var
を val
に変更しました。参照は不変ですが、リスト自体は可変です(要素の追加や削除が可能です)。
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)
に分解できます。これにより、firstName
と lastName
の値を直接処理できます。下記のように for
ループを更新します。user.firstName
のすべてのインスタンスを firstName
に置き換え、user.lastName
を lastName
に置き換えます。
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 のリスト内の名前は、まだ目的の形式にはなっていません。lastName
と firstName
はどちらも null
となる可能性があるため、フォーマットされたユーザー名のリストを作成する際、null 値許容を処理する必要があります。姓と名のいずれかがない場合は、"Unknown"
を表示します。name
変数は設定後に変更されることはないため、var
の代わりに val
を使用できます。最初にこの変更を行います。
val name: String
name 変数を設定するコードを見てみましょう。変数がイコールと、コードの if
/ else
ブロックに設定されているのは見たことがないかもしれません。これが可能なのは、Kotlin では if
、when
、for
、while
が式であり、値を返すためです。if
ステートメントの最後の行が name
に割り当てられます。このブロックの用途は、name
値を初期化することだけです。
基本的に、ここで示すロジックは、lastName
が null の場合、name
が firstName
または "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}
)。
コードでは現在、文字列の連結を使用して、firstName
と lastName
を 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.lastName
が String?
型であり、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 の機能を拡張しているため、Iterable
と Collection
の機能の多くが拡張関数として実装されています。たとえば、前のステップで使用した map
関数は Iterable
の拡張関数です。
Repository
クラスのコードで、_users
リストに複数の User
オブジェクトを追加しています。これらの呼び出しは、Kotlin のスコープ関数を利用することで、より慣用的に記述することができます。
名前に基づいてオブジェクトにアクセスすることなく、特定のオブジェクトのコンテキストでのみコードを実行できるように、Kotlin には 5 つのスコープ関数(let
、apply
、with
、run
、also
)が用意されています。これらの関数を使用すると、コードを読みやすく簡潔にすることができます。すべてのスコープ関数にはレシーバ(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 により、コードを簡潔でわかりやすく記述することができます。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 |
|
|
|
|
|
|
データのみを保持するクラス |
|
コンストラクタでの初期化 |
|
|
|
シングルトン クラス |
|
Kotlin の詳細と、プラットフォームでの Kotlin の使用方法について詳しくは、以下のリソースをご覧ください。