Kotlin クラス(Class)

ここでは、クラス、インスタンスの生成、メソッド、コンストラクタなどについて学ぶ。

クラス(class)およびインスタンス生成

KotlinはJavaと同じく、クラスベースのオブジェクト指向言語である。つまり、まずクラスからインスタンスを生成して利用する方式である。

メンバーを持たないクラスの定義は次のように表す。Javaと同じく、classキーワードでクラスを定義する。

例: 最小のクラス定義

class MyClass

このMyClassクラスをインスタンス化してみよう。

例: クラスのインスタンス生成

fun main() {
    val obj = MyClass()
    println(obj)
}

Output:

MyClass@5a07e868

MyClass()MyClassクラスのインスタンスを生成する。Javaではインスタンスを生成するときにnew演算子を明示する必要があるが、Kotlinでは明示しなくてもよい。 上の例では、MyClassクラスのインスタンスを生成し、その参照を変数objに代入している。そしてprintlnメソッドに渡して実行すると、クラス名とハッシュコードが出力される。

メソッド(method)

今度はメソッドを持つクラスを定義してみよう。次の例のGreeterクラスにはgreetメソッドがある。このようにクラスのメンバーは中括弧({})で囲む。メソッドはfunキーワードを使い、関数のように記述する。

例: メソッドを持つクラス

class Greeter {
  fun greet() {
    println("Hello!")
  }
}

次のようにGreeterクラスのインスタンスを生成し、そのインスタンスのgreetメソッドを呼び出す。

例: メソッド呼び出し

fun main() {
    val greeter = Greeter()
    greeter.greet()
}

Output:

Hello!

Kotlinではメソッド宣言時にfunキーワードを使うため、メソッドというより関数と呼ぶほうが正確かもしれない。機能や意味は同じなので、ここではメソッドと関数を混用する。

プロパティ(property)

クラスはプロパティを持つことができる。プロパティとは、簡単に言えばJavaにおけるフィールドとそのアクセサ(setter、getter)を備えたものだと説明できる。

次の例のUserクラスは、idnameというプロパティを持っている。インスタンスを生成し、プロパティに値を設定したり取得したりする。

例: プロパティを持つクラス

class User {
    var id: Long = 0
    var name: String = ""
}

次の例の3行目ではnameプロパティに値を設定している。そして4行目でnameプロパティとidプロパティの値を取得している。Javaでフィールドに直接アクセスしているように見えるが、実際には(デフォルトで生成された)setterまたはgetterを通じてアクセスしている。

例: プロパティの使い方

fun main() {
   val user = User()
   user.name = "devkuma"
   println("${user.name}(${user.id})")
}

Output:

devkuma(0)

setter / getter

settergetterをカスタマイズしたい場合は、次の例のように、関数に似た記法を使って自由に処理を入れられる。

例: setterとgetterのカスタマイズ

class User {
    var id: Long = 0

    var name: String = ""
        // 値を設定するときにログを出力する。
        set(value) {
            println("set: $value")
            field = value
        }
        // 値を取得するときにログを出力する。
        get() {
            println("get")
            return field
        }
}

Output:

set: devkuma
get
devkuma(0)

クラスが持つプロパティでは、set()が値を設定する関数(setter)であり、get()が値を取得する関数(getter)である。関数内ではfieldがプロパティのバッキングフィールドの値を表す。

コンストラクタ(constructor)

constructorは、クラスのインスタンスが生成されるときに自動で呼び出されるコンストラクタを定義する。

例: コンストラクタの作成

class User1 constructor(id: Long, name: String) {
    var name = name
}

fun main() {
    var user1 = User1(1,"devkuma")
    println(user1.name)
}

output:

devkuma

コンストラクタにアクセス修飾子やアノテーションなどを指定する必要がない場合は、constructorを省略できる。 次の例のように、クラス名の後にコンストラクタの引数リストを宣言できる。ここで受け取った引数でプロパティを初期化できる。

例: constructorの省略

class User2(name: String) {
    var name = name
}

コンストラクタが単にプロパティを初期化するだけなら、次のようにさらに省略できる。 コンストラクタ引数の名前の前にvalまたはvarを付けることでプロパティを定義でき、コードを簡潔にできる。

例: コンストラクタ引数でプロパティを定義

class User3(var name: String)

上のコンストラクタをプライマリ(Primary)コンストラクタという。また、次のようにセカンダリ(Secondary)コンストラクタを定義できる。セカンダリコンストラクタは、引数の数とデータ型に応じて複数書くことができる。

例: プライマリコンストラクタとセカンダリコンストラクタ

class User4 {
    var name: String
    var age: Int = -1

    constructor(name: String) {
        this.name = name
    }

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

クラスにプライマリコンストラクタがある場合、セカンダリコンストラクタはthisを使ってプライマリコンストラクタを呼び出さなければならない。

class User5(var name: String, var age: Int) {
    constructor(name: String) : this(name, -1)
}

初期化ブロック

クラスはinit {...}で初期化ブロックを記述できる。初期化ブロックはコンストラクタの本体が実行される前に呼び出される。

class Foo {
    init {
        println("Foo is created.")
    }
}

クラスの継承

Javaや他のオブジェクト指向言語と同じように、クラスは他のクラスを継承できる。継承すると親クラス(スーパークラス)のサブタイプになり、スーパークラスのメンバーをサブクラスのメンバーのように扱えるようになる。

継承可能なクラスの作成(open)

まず、スーパークラスになるAnimalクラスを定義してみよう。

例: スーパークラスの定義

open class Animal {
    fun running() {
        println("running")
    }
}

Animalクラスは、コンストラクタ引数やプロパティを持たず、runningメソッドを持つクラスである。ここで注目すべき点は、openキーワードが付いていることである。Kotlinでは、クラスが継承可能であることをopen修飾子で明示しなければならない。逆に言えば、openが付いていない通常のクラスは継承できない。

また、次のようにfinalキーワードを付けて継承できないクラスであることを明示することもできるが、デフォルトでそうなっているため通常は省略する。

final class Animal() { ... }

これは厳しい制約に見えるかもしれないが、「Effective Java 第2版」の項目17に準拠した設計思想である。

次に、Animalを継承するCatクラスを定義してみよう。次のようにして、スーパークラス(Animal)を継承するサブクラス(Cat)を定義できる。

例: 子クラスの定義

class Cat : Animal() {
    fun sound() {
        println("sound")
    }
}

クラス名の後にコロン(:)と、親クラスのコンストラクタ呼び出しを書く。ここではAnimalクラスにコンストラクタ引数がないため、単にAnimal()と書いている。

では、Catインスタンスからスーパークラスで定義したメソッドを呼び出せるか確認してみよう。

例: スーパークラスのメソッド呼び出し

fun main() {
    val cat = Cat()
    cat.running()
    cat.sound()
}

Output:

running
sound

また、CatAnimalクラスのサブタイプなので、val animal: Animal = catは有効なコードである。

val animal: Animal = cat
animal.sound() // Error

ただし、このようにするとanimalからサブクラス固有のメンバーは呼び出せない。

クラスの関数を再定義する(override)

overrideは、親クラスの関数を再定義できる。

open class Foo {
    open fun print() {
        println("Foo")
    }
}

class Bar() : Foo() {
    override fun print() {
        println("Bar")
    }
}

fun main() {
    var bar = Bar()
    bar.print()
}

Output:

Bar

親クラスのコンストラクタを呼び出す(super)

親クラスのコンストラクタを呼び出すにはsuper()を使う。

open class Animal(var type: String)

class Cat: Animal {
    var name: String
    constructor(name: String): super("Cat") {
        this.name = name
    }
}

また、次のようにも書ける。

open class Animal(var type: String)
class Cat(var name: String): Animal("Cat") {}

クラスのネスト(nested and inner class)

クラスはネストできる。

class A {
    class B { ... }
}

var obj = A.B()

innerは、ネストされたクラスが内部クラスであることを示す。

class A {
    inner class B { ... }
}

var obj = A().B()

.toString()

すべてのクラスには文字列化のための.toString()関数がある。println()で出力するときなどに暗黙的に呼び出される。次のように上書きできる。

class Person(val name: String, var age: Int) {
    override fun toString() = "$name($age)"
}

fun main() {
    println(Person("devkuma", 26))  // devkuma(26)
}

.equals()

すべてのクラスには比較のための.equals()メソッドがある。==で比較するときなどに暗黙的に呼び出される。次のように上書きできる。

class Foo(val name: String) {
    override fun equals(other: Any?) = this.name == (other as Foo).name
}

fun main() {
    val a1 = Foo("devkuma")
    val a2 = Foo("devkuma")
    println(a1 == a2)        // true
}

参考