リスト2はプロパティを持つクラスの例です。普通の変数宣言のように「var」を使った書き換え可能なプロパティや、「val」を使った書き換え不可能なプロパティを定義できます。また、推論可能な初期値を設定することで、データ型を省略することもできます。
//プロパティを持つクラス定義の例
class Counter(){
//書き換え可能なプロパティの定義
var counter: Int = 0
//書き換え不可能なプロパティ。ここではデータ型を省略して推論させている
val maxValue = 10
fun countUp(){
//プロパティの値に1を加算
counter += 1
}
fun print(){
println("現在のカウント値: ${counter}")
}
}
……
//プロパティの使用例
val counter = Counter()
counter.print() //「現在のカウント値: 0」が出力される
counter.countUp()
counter.countUp()
counter.print() //「現在のカウント値: 2」が出力される
リスト2 プロパティの使用例
さて、これだけだとJavaのフィールドと同じに思えるかもしれませんが、前述の通りKotlinのプロパティは、取得、設定の際にリスト3のように「getter」「setter」と呼ばれるロジックを書くことができます。これらを「アクセサ」と呼びます。ここでは、時計クラスを作成し、0時から何秒経過したかを取得、設定する「pastSeconds」というプロパティを定義しています。
//時計クラスの定義
class Clock(){
//時・分・秒のプロパティ
var hour: Int = 0
var minute: Int = 0
var second: Int = 0
//00:00:00から何秒経過したかを取得・設定するプロパティ
var pastSeconds: Int
get(){
//getキーワードでgetter(取得の際のロジック)を定義
return ((hour * 60) + minute) * 60 + second
}
set(value){
//setキーワードでsetter(設定の際のロジック)を定義
//セットする値はvalueという変数に入っている
hour = value / 3600
minute = (value % 3600) / 60
second = value % 60
}
fun print(){ //現在のプロパティの値を表示
//pastSecondsプロパティはgetterロジックを実行
println("${hour}時${minute}分${second}秒, 0時から${pastSeconds}秒経過")
}
}
……
val clock = Clock()
//普通にプロパティに値を設定。ここはフィールドと同様
clock.hour = 1
clock.minute = 10
clock.second = 5
clock.print() // 「1時10分5秒, 0時から4205秒経過」が出力される
//pastSecondsプロパティに値を設定。setterロジックを実行
clock.pastSeconds = 4101
clock.print() //「1時8分21秒, 0時から4101秒経過」が出力される
リスト3 「getter」「setter」を持つプロパティの例
Javaではクラスのフィールドを直接外部に公開せずにprivateフィールドとした上で、「getXXX」「setXXX」といった名前を持つpublicメソッドを使って値を入出力するカプセル化が一般的です。Kotlinのプロパティは、このカプセル化が言語レベルでサポートされていると考えるといいでしょう。アクセサを使ったプロパティとgetter/setterによるカプセル化は原理的にほぼ同じ仕組みですが、書きやすさ、読みやすさはプロパティの方が格段に上といえるでしょう。
リスト4は、あるプロパティの値に1を加算するコードを、プロパティとJavaのgetter/setterを使って書いたサンプルです。
//Kotlinでhourプロパティに1を加える
clock.hour = clock.hour + 1
//Javaでgetter/setterを使って同様の処理を書く
clock.setHour(clock.getHour() + 1);
リスト4 getter/setterとプロパティの使い勝手の違い
getter/setterを使うよりも、プロパティを使った方が、より直感的な書き方ができますね(この辺りは、C#や他のプロパティをサポートした言語のプログラマーであればよく理解できることでしょう)。
プライマリコンストラクタ
続いてはコンストラクタの書き方についてです。Kotlinでは、Javaとは少し異なり、「プライマリコンストラクタ」と呼ばれるコンストラクタをclassキーワード直後に記述します。プライマリコンストラクタ以外のコンストラクタは「セカンダリコンストラクタ」と呼ばれ、「constructor」キーワードで定義します*1。
*1)なお、プライマリ、セカンダリ以降の「3番目の(ターシャリ?)」コンストラクタはなく、プライマリコンストラクタ以外は全てセカンダリコンストラクタと呼ばれます。
リスト5は姓、名のプロパティを持つPersonクラスについてプライマリコンストラクタとセカンダリコンストラクタを定義した例です。
//プライマリコンストラクタ。name引数で名のみを指定
class Person(name: String){
//姓、名プロパティ
var firstName: String
var lastName: String
//プライマリコンストラクタの処理内容はinitで記述
init{
this.firstName = name //プライマリコンストラクタの引数をプロパティに代入
this.lastName = "" //姓は未指定とする
}
//セカンダリコンストラクタ。姓、名両方を指定
//this()でプライマリコンストラクタを呼び出す
constructor(firstName: String, lastName: String): this(firstName){
this.lastName = lastName //姓に2番目の引数を代入する
}
fun print(){
println("お名前: ${firstName} ${lastName}")
}
}
……
//プライマリコンストラクタの呼び出し例
val person = Person("Doi")
person.print() //結果: 「お名前: Doi」
//セカンダリコンストラクタの呼び出し例
val person2 = Person("Tsuyoshi", "Doi")
person2.print() //結果: 「お名前: Tsuyoshi Doi」
リスト5 プライマリコンストラクタとセカンダリコンストラクタ
Javaの書き方と雰囲気がガラッと変わったので混乱するかもしれませんが、前述の通りclassキーワードの直後がそのままプライマリコンストラクタの定義となるので、そこに引数を記述しています。プライマリコンストラクタの処理内容は「init」キーワードで別途記述します。initキーワードの中では、「firstName」プロパティにプライマリコンストラクタの「name」引数の値を設定しています。
セカンダリコンストラクタはconstructorキーワードを使って定義します。なお、セカンダリコンストラクタの定義の後に「this(firstName)」のように書いています。これはセカンダリコンストラクタの呼び出しの際に、他のコンストラクタ(ここではプライマリコンストラクタ)を呼び出す書き方です。Javaにおいてコンストラクタの先頭でthis()を使って他のコンストラクタを呼び出すのと同じですね。
なおKotlinにおいては、セカンダリコンストラクタから別のセカンダリコンストラクタを呼び出すことは許されますが、いずれのケースでも最終的に必ずプライマリコンストラクタが呼び出される必要があります。各コンストラクタが独立してインスタンス生成することを許すJavaとは異なることに注意してください。
なお、コンストラクタを呼び出す際にはJavaと同様に、引数の数や型に合わせて適切なコンストラクタが呼び出されます。
プライマリコンストラクタでのプロパティ定義とデフォルト値
さて、リスト5を見ていて「コンストラクタの引数をわざわざ代入するだけのコードはかったるいなぁ」と思いませんか? 多くのコンストラクタの役割は上記のように「プロパティの値を設定する」ものですので、Kotlinではリスト6のようにプライマリコンストラクタの中でプロパティ自体を定義できるようになっています。
//プライマリコンストラクタでプロパティを定義。デフォルト値も設定
class Person2(val firstName: String, val lastName: String = ""){
fun print(){
//プライマリコンストラクタで定義されたプロパティを参照
println("お名前: ${firstName} ${lastName}")
}
}
//第2引数を省略してプライマリコンストラクタを呼び出す
val person3 = Person2("Doi")
person3.print() //結果: 「お名前: Doi」
//第2引数を指定してプライマリコンストラクタを呼び出す
val person4 = Person2("Tsuyoshi", "Doi")
person4.print() //結果: 「お名前: Tsuyoshi Doi」
リスト6 プライマリコンストラクタでプロパティ定義とデフォルト値を設定したサンプル
ここでは、プライマリコンストラクタで「firstName」「lastName」という引数を指定していますが、各引数の前に「val」を付けることで、この引数が自動的に書き換え不能プロパティとして扱われます。
また、lastNameプロパティの後に代入文があることにも注目してください。これは引数省略時のデフォルト値です。この機能を使えば、引数の数が異なるだけのコンストラクタを別個に定義する必要がなくなるケースも多いでしょう。
このプライマリコンストラクタでのプロパティ定義とデフォルト値設定により、先ほどのリスト5で20行ほどだったコードが、一気に半分以下に納まりました。圧倒的な差ですね。
コラム「Kotlinにメソッドは存在しない???」
本連載では、今回「KotlinのクラスではJavaと同様にメソッドを定義可能」、連載第2回で「Kotlinではクラス内のメソッドとは別に、クラス外に関数として処理を書くことができる」と書いてきましたが、実はこれらは厳密に言うと正しくありません。「Kotlinにはメソッドという概念はなく、クラス内に宣言されるものも含めて全て関数である」というのがより正確な言い方になります。
ただ「クラス内に定義された関数をメソッドと呼ぶ」のはオブジェクト指向言語プログラマーにとって非常に分かりやすいため、そのように書かれている情報源も少なくないようです。本連載でも分かりやすさを優先して、クラス内で定義された関数(Kotlinではメンバ関数とも呼ばれる)についてメソッドという表現を用いています。
メソッドと関数は言語上同じ概念であるため、本文で説明した引数省略時のデフォルト値の設定などは、クラスのメソッドだけの特別な機能ではなく、関数においても利用可能です。今後の回でも関数のいろいろな機能について解説しますが、クラスのメソッド、関数の両方で使えることを覚えておいてください。
ただし、コンストラクタと関数は見た目が似ているものの別物であることにも注意してください。引数にval、varを付けることでプロパティを定義できるのはコンストラクタ専用の機能で、関数では使用できません。
データクラス
Javaを含む多くのオブジェクト指向言語において、クラスはデータ(フィールドやプロパティ)と操作(メソッド)の集合体として定義されます。しかし、実際のプログラミングにおいては、オプションを指定する専用のクラスなど、データのみを保持するクラスもしばしば登場します。
Kotlinではその名の通り、「データクラス」という概念があり、データのみを持つクラスを簡潔に定義できます。データクラスはリスト7のようにclassキーワードの前にdataキーワードを付けて定義します。また、プライマリコンストラクタのみで定義が完成するため、クラス本体を{}で記述する必要はありません
//データクラス。プライマリコンストラクタ後に{}がないことに注目
data class Person3(val firstName: String, val lastName: String = "")
……
//データクラスを定義
val person5 = Person3("Tsuyoshi", "Doi")
リスト7 データクラスのサンプル
データクラスはプライマリコンストラクタのみで完結しており、保持するデータをプロパティとして定義します。データクラスを定義すると、幾つかの便利なメソッドが自動的に定義されます(リスト8)。
//データクラスには自動でtoStringメソッドが定義される
println(person5) //結果: 「Person3(firstName=Tsuyoshi, lastName=Doi)」
リスト8 データクラスに自動的に定義されるtoString()メソッド
まずは文字列表現で、「クラス名(プロパティ名=プロパティ値、プロパティ名=……)」のような形式の文字列を返すtoString()メソッドが自動的に定義されます*2。
*2)これまで触れてきませんでしたが、Kotlinにおいてもオブジェクトの文字列表現を取得する際にはJavaと同じくtoString()メソッドが使用されます。
また、データクラスには、インスタンスをコピーするcopy()メソッドも定義されています。copy()メソッドを使えば、同じプロパティ値を持つインスタンスを簡単に生成できます。リスト9はcopy()メソッドを使ってインスタンスのコピーを作り、その一部のプロパティだけを書き換えるサンプルです。
//データクラスはインスタンスをコピーするcopy()メソッドを持つ
val person6 = person5.copy()
person6.firstName = "TsuyoTsuyo"
println(person6) //結果: 「PersonByDataClass(firstName=TsuyoTsuyo, lastName=Doi)」
リスト9 copyメソッド
またデータクラスには、宣言された順番にプロパティを返すcomponentNメソッド(Nには定義順の番号が入る)も定義されています(リスト10)。
//データクラスはN番目に定義されたプロパティを返すcomponentNメソッドを持つ
println(person6.component2()) //結果:「Doi」←2番目のプロパティであるlastNameの値
リスト10 N番目のプロパティを返すcomponentNメソッド
データクラスはそれほど特別な機能ではありませんが、不要なものが省かれるので、クラス定義がより簡潔で分かりやすいものになりますね。
シングルトンオブジェクト(オブジェクト宣言)
連載第3回でサンプルとして挙げた「あるクラスのインスタンスが常に1つであることを保証する」デザインパターンであるSingletonパターンですが、Kotlinでは「object」キーワードを使うことで簡潔に定義可能です。
//Java版Singletonパターン(マルチスレッドバグ無し版)
public class Singleton{
//マルチスレッドバグ無し版ではフィールド宣言でいきなりnewする
private static Singleton instance = new Singleton();
public Singleton getInstance(){ //唯一のインスタンスを取得するメソッド
return instance; //インスタンスを返す
}
private Singleton(){} //外部からインスタンスを作れないようにする
}
これだけだと分かりづらいので、リスト12で使い方も見ておきましょう。
//設定ファイル読み書きオブジェクト。常に実体は1つ
object Config{
//設定ファイル読み書きダミー実装用Map
val map = mutableMapOf<String, String>()
fun read(key: String): String{
return map[key] ?: ""
}
fun write(key: String, value: String){
map[key] = value
}
}
……
//シングルトンオブジェクトの使用例
//Configはクラスではなくオブジェクトなので、そのままメソッドを呼び出せる
val value = Config.read("test1") //設定読み込み
Config.write("test2", "value2") //設定書き込み
リスト12 シングルトンオブジェクトの使用例
ここでは、設定ファイルの読み書きを行うConfigオブジェクトをシングルトンとして定義しています。設定ファイルは1つで、複数のConfigインスタンスから読み書きさせるのは危険なため、Singletonパターンを使用することを想定しています(ただしここではダミー実装として書き換え可能なMapを使って実装しています)。
また、シングルトンオブジェクトとして定義したConfigオブジェクトから「Config.read()」のように、直接メソッドを呼び出しています。
実はKotlinのシングルトンオブジェクトはクラスではなくオブジェクトそのものです。クラスをインスタンス化することでオブジェクトを作成するJavaとは異なり、Kotlinにおいてはobjectキーワードを使うことで、その場でオブジェクトを宣言できます(その名の通り「オブジェクト宣言」と呼ばれます)。シングルトンオブジェクトにおいては、そもそもクラス自体が存在しないため、常に単一のインスタンスであることが保証されます。
コンパニオンオブジェクト
先ほど、「Kotlinにはstaticメソッド(クラスメソッド)がない」と記しましたが、Kotlinにはクラスのstatic変数(クラス変数)も同じく存在しません。staticメソッドはクラス外の関数である程度代替できますが、static変数については、直接の代替はありません。
Kotlinには、クラス内で「companion」キーワードを使って「コンパニオンオブジェクト」と呼ばれるオブジェクトを宣言でき、staticメソッド、static変数の代替手段として用いることができます。リスト13はコンパニオンオブジェクトを使ってメソッドと変数を定義する例です。
//コンパニオンオブジェクトの使用例
class CompanionSample(val firstName: String, val lastName: String = ""){
//companionキーワードを使ってコンパニオンオブジェクトを宣言
companion object{
//helloメソッドを定義
fun hello(name: String){
println("こんにちは $name さん")
}
//プロパティを定義
val PI = 3.141592653589793
}
}
……
//コンパニオンオブジェクトで定義されたメソッドをstaticメソッドのように呼び出し
CompanionSample.hello("土井") //結果: 「こんにちは 土井 さん」
//コンパニオンオブジェクトで定義されたプロパティをstatic変数のように呼び出し
println(CompanionSample.PI) //結果: 「3.141592653589793」
リスト13 コンパニオンオブジェクトの使用例
コンパニオンオブジェクト内で定義されたメソッドやプロパティは、「クラス名.メソッド名」や「クラス名.プロパティ名」といった形式で呼び出すことができ、Javaのstaticメソッド、static変数に近い形で使用できます。これも先ほどのオブジェクト宣言の応用で、クラス定義の中に、そのままオブジェクトを宣言していることになります。
次回は、継承や可変長引数、拡張関数など
今回はKotlinのクラスに関連した機能を解説しました。プロパティやデフォルト引数などはJavaにはなかった機能ではあるものの、比較的なじみやすい、すぐに使えそうな機能ですね。一方、プライマリコンストラクタでのプロパティ定義やデータクラスなどは、ちょっと毛色が異なると感じた方も多いことでしょう。しかし、これらの機能は強力で、冗長なコードを省き、より簡潔で直感的な実装を可能にしてくれます。
また、オブジェクト宣言については、直接Javaに対応する機能がないため、やや戸惑うかもしれません。まずは今回紹介したようなシングルトンオブジェクトやstatic変数、メソッドの代替といった一般的なユースケースで使い方をつかむようにしましょう。