Kotlin のクラス定義は Java と同様に class
キーワードを使用しますが、デフォルトで public final 扱いという違いがあります。
これは、多くのケースで public
の方が都合がよいことと、意図しない継承を防ぐことを意図した仕様です。
次の Book
クラスは、リードオンリーな title
プロパティ持つ、シンプルなクラスの実装例と使用例です。
class Book(val title: String)
val b = Book("Title1")
println(b.title) //=> Title1
詳しくは後述しますが、Kotlin にはメソッドを簡潔に記述するための仕組みがたくさん用意されています。
メソッド実装などの記述が必要ない場合は、上記のようにクラス本体部分を示す { }
ブロックすら省略して記述することができます。
Java とは異なり、コンストラクタを呼び出すときの new
キーワードも省略できます(というより new
は存在しません)。
上記は title
プロパティをリードオンリーとして定義していますが、プロパティの値を書き換え可能にするには、val
キーワードを var
キーワードに置き換えるだけで済みます。
class Book(var title: String)
val b = Book("Title1")
b.title = "Title2"
println(b.title) //=> Title2
上記のように b.title
という形で title
フィールドにアクセスできるのは、内部で getter/setter メソッドが定義されて呼び出されているからです。
title
フィールドそのものが public
になっているわけではなく、あくまで public
な getter/setter メソッドが暗黙的に呼び出されています。
この Book
クラスを Java のコードから使用する場合、title
フィールドへのアクセスは、b.getTitle()
、b.setTitle("...")
のように記述することになります。
Kotlin のクラスのコンストラクタには、プライマリ・コンストラクタ (primary constructor) とセカンダリ・コンストラクタ (secondary constructor) の 2 種類があります。
例えば、プライマリ・コンストラクタと、セカンダリ・コンストラクタを 2 つ持つクラスがあった場合、コンストラクタの呼び出しは下記のような感じで、プライマリ・コンストラクタによる初期化が起点となってオブジェクトの構築が行われます。 コード上はセカンダリ・コンストラクタからプライマリ・コンストラクタを呼び出しているかのように見えるかもしれませんが、あくまで実行順序はプライマリ・コンストラクタが先です。
上記のセカンダリ・コンストラクタの説明では、「バリエーション」という言葉を使いましたが、コンストラクタのパラメータとしてデフォルト引数の仕組みが使えるので、プライマリ・コンストラクタだけでもある程度の生成の「バリエーション」を持たせることは可能です。
プライマリ・コンストラクタは、インスタンスの生成時に必ず呼び出されます。 省略記法がいろいろありますが、一番冗長な書き方から順番に見ていきます。
プライマリ・コンストラクタが受け取るパラメータは、クラス名の後ろに続けて constructor(...)
という形で宣言します。
プライマリ・コンストラクタが呼び出されると、(1) プロパティの定義部分 (property initializer)、(2) init
で囲まれた初期化ブロック (initializer block) が順番に実行されます。
(1) と (2) の中では、プライマリ・コンストラクタに渡されたパラメータを直接参照することができます。
下記の Book
クラスは、プライマリ・コンストラクタで 1 つの値を受け取り、それをリードオンリーなプロパティとして保持しています。
class Book constructor(title: String) {
// (1) プロパティ定義 (property initializer)
val title: String
// (2) 初期化ブロック (initializer block)
init {
this.title = title
}
}
// 使用例
val b = Book("タイトル")
println(b.title)
この例のように、初期化ブロック内で単純なプロパティ代入しか行っていない場合は、初期化ブロック (init
) の記述を省略して、プロパティの定義部分で値の設定まで済ませてしまうことができます。
プロパティ定義部分では型推論が働くので、型の指定を省略することができます。
class Book constructor(title: String) {
val title = title
}
さらに、プライマリ・コンストラクタに、アノテーションや可視性の指定がない場合は、constructor
キーワードを省略することができます。
class Book(title: String) {
val title = title
}
さらに、パラメータで受け取った値を、プロパティの定義部分で単純に代入しているだけであれば、パラメータ名の前に val
、あるいは var
キーワードを付けることによって、パラメータとプロパティの定義を同時に行ってしまうことができます。
val
を付けた場合はリードオンリーなプロパティとなり、コンストラクタで設定された値から変更することができなくなります。
最終的に Book
クラスは下記のようにシンプルに記述できることになります。
class Book(val title: String)
コンストラクタの(パラメータの)定義を省略した場合は、パラメータを取らないプライマリ・コンストラクタが自動的に生成されます。
class Book {
var title: String = "Unknown"
init {
println("初期化ブロックはいつでも書けるよ")
}
}
val b = Book()
b.title = "ぴよぴよ"
println(b.title) //=> ぴよぴよ
コンストラクタのパラメータには、デフォルト値を持たせることができます(通常の関数と同様です)。 下記の例では、2 つのパラメータにデフォルト値を設定しています。
class Book(val title: String = "無題", val author: String = "著者不明") {
override fun toString() = "$title, $author"
}
コンストラクタの呼び出し時に引数を省略すると、デフォルト値として指定した値が使用されます。
println(Book()) //=> 無題, 著者不明
println(Book("ああ")) //=> ああ, 著者不明
println(Book("ああ", "まく")) //=> ああ, まく
また、コンストラクタに引数を渡す時に パラメータ名=値
という形で指定すると、任意の順序でパラメータを指定することができます (名前付き引数)。
println(Book(author = "まく", title="ああ")) //=> ああ, まく
型が同じパラメータが複数ある場合、名前付き引数の仕組みを使うと、引数の順番を間違えるといったミスを防ぐことができます。
プライマリ・コンストラクタだけではカバーしきれないような、パラメータのバリエーションを持たせたい場合は、セカンダリ・コンストラクタ (secondary constructor) を定義します。 Kotlin にはデフォルト値や名前付き引数の仕組みがあるので、多くの場合はプライマリ・コンストラクタだけで十分ですが、フレームワークで定義されているクラスを継承するようなケースで必要になったりします(親クラスのコンストラクタに合わせてパラメータ定義する必要があったりするため)。
セカンダリ・コンストラクタは、クラス本体部分で constructor
キーワードを使って定義します。
下記の Indenter
クラスは、テキストの前にインデントを入れて出力するためのクラスです。
コンストラクタで渡した文字数分のスペース、あるいは、渡された文字列そのものをインデントとして出力します。
パラメータに応じて異なる初期化処理を行う必要があるため、2 つのセカンダリ・コンストラクタを作成して、それぞれの初期化処理を定義しています。
class Indenter {
val text: String
constructor(size: Int) {
text = " ".repeat(size)
}
constructor(text: String) {
this.text = text
}
fun puts(message: String) {
println("$text$message")
}
}
fun main() {
Indenter(4).puts("Hello") //=> " Hello"
Indenter("----").puts("Hello") //=> "----Hello"
}
上記のように、パラメータ付きのセカンダリ・コンストラクタのみを定義した場合、パラメータなしのプライマリ・コンストラクタが自動生成されることはありません。
Indenter() // NG(パラメータなしのコンストラクタはない)
プライマリ・コンストラクタとセカンダリ・コンストラクタの両方を定義する場合、セカンダリ・コンストラクタから this
を使って、間接的、あるいは、直接的にプライマリ・コンストラクタを呼び出しておく必要があります。
プライマリ・コンストラクタはいかなる場合にも呼び出されるからです。
下記の例では、Int
値を受け取るセカンダリ・コンストラクタから、String
値を受け取るプライマリ・コンストラクタを呼び出しています(実装部分は空なので後ろの {}
を省略しています)。
class Indenter(val text: String) {
// プライマリ・コンストラクタを呼び出す
constructor(size: Int) : this(" ".repeat(size))
}
this
キーワードは、プライマリ・コンストラクタの呼び出しだけでなく、別のセカンダリ・コンストラクタの呼び出しにも使用できます。
下記の例では、1 つ目のセカンダリ・コンストラクタから、2 つ目のセカンダリ・コンストラクタを呼び出しています。
結果的にプライマリ・コンストラクタの呼び出しにつながるため、このようなコンストラクタ定義も正しいものとなります(これを「間接的」なプライマリ・コンストラクタの呼び出しと呼んでいます)。
class Indenter(val text: String) {
// 別のセカンダリ・コンストラクタを呼び出すセカンダリ・コンストラクタ
constructor(size: Int) : this(size, " ")
// プライマリ・コンストラクタを呼び出すセカンダリ・コンストラクタ
constructor(size: Int, text: String) : this(text.repeat(size))
}
セカンダリ・コンストラクタを呼び出した場合でも、先にプライマリ・コンストラクタの処理が実行されることに注意してください。 下記のような順番で実行されていきます。
init
)下記のようなテストコードを実行してみれば、処理の流れを理解できると思います。
class Book(title: String) {
// (1) プライマリ・コンストラクタによるフィールドの初期化
val title = title
var author = "作者不明"
// (2) プライマリ・コンストラクタの初期化ブロック
init {
println("---- init ----")
println(author)
}
// (3) セカンダリ・コンストラクタの中身は最後に実行される
constructor(title: String, author: String) : this(title) {
println("---- secondary ----")
println(this.author)
this.author = author
}
}
fun main() {
// セカンダリ・コンストラクタを呼び出し
val b = Book("タイトル", "まく")
println("---- main ----")
println(b.author) //=> 作者
}
---- init ----
作者不明
---- secondary ----
作者不明
---- main ----
まく
データクラスにセカンダリ・コンストラクタを定義する場合は、データクラスによって自動生成されるコードが、プライマリ・コンストラクタで定義されたフィールドのみを参照するという点に注意してください。 そもそも、セカンダリ・コンストラクタのパラメータでフィールドを定義することはできませんが、セカンダリ・コンストラクタ内の実装で、クラス本体部分で定義したフィールドを初期化するということをやってしまいがちです。 クラスが保持するべき値は、できるだけプライマリ・コンストラクタのパラメータとしてフィールド定義してしまうのが安全です。
データクラスに関する詳しい説明は下記を参照してください。
プライマリ・コンストラクタをクラスの外部から呼び出せないようにするには、constructor
の前に private
キーワードを付けます。
このように可視性を付加する場合、constructor
の記述は省略できなくなります。
class Book private constructor() {}
private
なコンストラクタしか存在しないクラスのインスタンスを生成するには、クラスのコンパニオン・オブジェクト (companion object) からコンストラクタを呼び出す必要があります。
コンパニオン・オブジェクトに定義した関数は、インスタンスがなくても呼び出すことができるため、外部から間接的に private なコンストラクタを呼び出すための入り口として使用できます。
コンストラクタのパラメータが複雑な場合、直観的な名前のついたファクトリ・メソッドをコンストラクタの代わりに提供すると可読性を向上させることができます。
下記の Book
クラスは、インスタンスの生成をファクトリ・メソッド経由で行うことを強制しています。
class Book private constructor(val title: String, val price: Int) {
companion object {
fun newFreeBook(title: String) = Book(title, 0)
fun newDamnedBook(title: String) = Book(title, -1)
}
}
fun main() {
val b = Book.newFreeBook("はじめてのKotlin")
println(b.title)
println(b.price)
}
この例だとコンストラクタのパラメータがシンプルすぎて、ファクトリ・メソッドを導入するメリットは感じられないかもしれませんが、こういった設計パターンがあることを覚えておくといつか役に立つでしょう。 こういった抽象度の高いデザインパターンは、Kotlin に限らず、一般的なベストプラクティスとして受け入れられています。
プライマリ・コンストラクタを private にする目的が、シングルトンを作成したいということであれば、代わりに Kotlin の object
宣言を使用すると簡潔な記述が可能です。
単なる静的なユーティリティ関数を集めただけのユーティリティ・クラスを作りたいということであれば、パッケージのトップレベルに関数を定義してしまうのが手っ取り早いです。