Mokelab Blog

Android 向け Kotlin 入門(クラス編)

前回は Kotlin の基本文法について説明しました。今回はクラスに関するものを説明します。

クラスとは

クラスとはオブジェクト(モノ)の設計図のようなもので、Kotlin では Int や String のようなデータ型になります。このあたりの説明はなぜか Kotlin公式ドキュメントには書かれていません。。。

クラスを定義する

Kotlin でクラスを定義する(新しいデータ型を作る)には、class クラス名{} とします。例として、アプリのアカウント情報を表す Account というクラスを定義してみます。

class Account { }

オブジェクトを作る

クラスを定義しただけではオブジェクトは作られません。クラスをもとにオブジェクトを作るには、クラス名()とします。Java や他の言語では new クラス名()としますが、Kotlin では new をつけません。

val a = Account()

プロパティを追加する

現時点では Account オブジェクトは何もデータを持っていません。そこで、Account オブジェクトを「名前とメールアドレスを保持しているもの」と決めてみましょう。この「名前」や「メールアドレス」にあたるものをプロパティと呼びます。Kotlin でプロパティを追加するには、変数と同様に varval を使います。

class Account {
    var name: String = ""
    var email: String = ""
}

プロパティを読み書きする

プロパティを読み書きするには、(オブジェクトが入っている変数名).(プロパティ名)とし、変数のように扱います。

val a = Account()
a.name = "moke"
a.email = "moke@example.com"
println("メールアドレスは${a.email})

コンストラクタを作る

オブジェクトを作る際、同時にプロパティにデータを入れておくとデータの入れ忘れが防止できます。そのための仕組みとしてコンストラクタというものがあります。

// 説明のためにコンストラクタの引数名を1文字にしています 
class Account(n: String, e: String) {
    var name: String = n
    var email: String = e
 }

クラス名の直後に、関数の引数のように記述します。このようにして定義したコンストラクタをプライマリーコンストラクタと呼びます。

コンストラクタを追加したので、オブジェクトを作る場面は次のようになります。プロパティのセット忘れが防止できています。

val a = Account("moke", "moke@example.com")
println("メールアドレスは${a.email}")

「コンストラクタでデータを受け取り、それをそのままプロパティにセットする」という場面はとても多いです。そのため Kotlin では、次のようにプロパティ定義もまとめてプライマリーコンストラクタで行う構文が用意されています。

class Account(val name: String, val email: String) { }

クラスの継承

別のクラスを元にして新しいクラスを作る機能をクラスの継承と呼びます。元にするクラスをスーパークラスと呼びます。Kotin でクラスの継承を行うには、class クラス名: スーパークラス名()とします。

// Fragmentを継承したクラスを作るゾ
class MyFragment: Fragment() {     
}

なお、スーパークラスには次のように open がついている必要があります。

open class Fragment() {
    ...
}

open がついていないクラスを継承しようとした場合、次のようなコンパイルエラーが発生します。

error: this type is final, so it cannot be inherited from

lateinit var

プロパティはコンストラクタの引数で初期化するか、初期値をいれておく必要があります。しかし Android では Activity や Fragment など、引数なしコンストラクタしか使えず、実際の初期化は onCreate()でやる場面がいくつかあります。

とりあえず null をいれておく方法がぱっと思いつきます。

class MyFragment: Fragment() { 
    var name: String? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        this.name = /* 初期値 */
    }
}

しかしこの方法だと初期化が必ず行われるのに毎回 null チェックが必要となってしまいます。そこで登場するのが lateinit var です。これは「あとで初期化することを保証するので、使う時は Non-null として扱わせてね」といった動作になります。

class MyFragment: Fragment() {
    lateinit var name: String //Non-nullにできる
    
    override fun onCreate(savedInstanceState: Bundle?) {
        this.name = /* 初期値 */ 
    }
}

初期化前のプロパティを読もうとすると、kotlin.UninitializedPropertyAccessException が投げられます。

kotlin.UninitializedPropertyAccessException: lateinit property memo has
      not been initialized

メソッド

メソッドとはオブジェクトに対し、どのような命令/質問ができるかを記述したものです。Kotlin でクラスにメソッドを追加するには、クラスの中で fun メソッド名(引数) {}とします。{}の中には呼ばれたときの処理を記述します。

class Account(val name: String, val email: String) {
    var loginCount: Int = 0
    
    fun increaseLoginCount() {
        this.loginCount++
    }
}

メソッドを定義しただけでは中身は実行されません。メソッドを呼び、中身を実行してもらうには(オブジェクトが入っている変数名).メソッド名()とします。

val a = Account("moke", "moke@example.com")
a.increaseLoginCount()

メソッド内では、メソッドが呼ばれたオブジェクトが this に入っています。increaseLoginCount()は「呼ばれたオブジェクトのログイン回数を 1 増やす」メソッドになっています。

オブジェクトが 2 つある場合はどうでしょう。

val a = Account("moke","moke@example.com")
a.increaseLoginCount()
val b = Account("piyo", "piyo@example.com")
b.increaseLoginCount()
b.increaseLoginCount()

println("aは${a.loginCount}回ログインしたよ")
println("bは${b.loginCount}回ログインしたよ")

実行すると、次のように表示されます。

aは1回ログインしたよ
bは2回ログインしたよ

メソッドのオーバーライド

クラス Aを次のように定義したとします。

open class A {
    open fun sayHello() {
        println("こんにちは")
    }
}

そして、クラス A を元にしてクラス B を定義してみます。

class B : A() { }

クラス B はクラス A でもあるので、次のように sayHello()が呼べます。

val b = B()
b.sayHello() // こんにちは と表示される

open のついているメソッドは次のようにオーバーライド(上書き)ができます。上書きなので引数の種類や型を変更してはいけません。

class B : A() {
    override fun sayHello() {
        println("ちーっす")
    }
} 

オーバーライドしたメソッドの中で、スーパークラスのメソッドを呼びたい場合は super.メソッド名()とします。Android では onCreate()など、一部のメソッドはスーパークラスのメソッドを呼ばないと実行時にエラーとなるものがあるので呼び方を覚えておきましょう。

class B : A() {
    override fun sayHello() {
        super.sayHello()
        println("ちーっす")
    }
}

抽象クラスと抽象メソッド

クラスはオブジェクトを作るための設計書ですが、時には「継承されること前提」のクラスを定義したい場合もあります。このようなクラスを抽象クラスと呼び、Kotlin では次のように abstract をつけます。

abstract class A {
    // 抽象メソッドの定義
    abstract fun sayHello()
}

「A を継承したクラスのオブジェクトに対し、sayHello()が呼べるよ。でもどんな処理をするかは継承先に聞いてね」という扱いになります。 継承前提なので、次のようにオブジェクトを作ろうとすると error: cannot create an instance of an abstract class というコンパイルエラーが発生します。

val a = A() // Aは抽象クラス

抽象クラスを継承した場合、abstract のついている抽象メソッドはすべてオーバーライドしなければなりません。

class B : A() { 
    override fun sayHello() {
        println("ちーっす")
    }
}

もしオーバーライド忘れがあった場合、次のようなコンパイルエラーが発生します。

error: class 'B' is not abstract and does not implement abstract base class member public abstract fun sayHello(): Unit defined in main.A 

抽象クラスはうまく使わないと混乱の元となります。データ型の設計をする際、次に紹介するインターフェースで十分かどうかを検討しましょう。

インターフェース

オブジェクトの利用について考えてみましょう。Kotlin は静的型付け言語なので、変数には型がついています。つまり、オブジェクトを利用する際は「この型のオブジェクトが必要」と事前に宣言しておく必要があります。 クラスは型ですが、メソッドでの処理内容も型の情報として含まれてしまいます。一方、オブジェクトを使う側は処理内容まで気にする必要のないことがあります。

例えば次のようにデータの保存処理を記述したとします。

fun saveMyData(a: A) {
    a.save(this.data)
}

ここでの主目的は A というオブジェクトを使ってデータを保存することです。データが保存できていれば、どのように保存されるかは興味がありません。 しかし A が次のようなクラスであった場合、saveMyData()は「データをファイルに保存することのできるオブジェクト」が必要という意味になってしまいます。

class A {
    fun save(data: GameData) {
        // ファイルに保存する処理
    }
}

ここで登場するのがインターフェースという仕組みです。クラスと同様に型ですが、オブジェクトに対し呼べるメソッドの形だけを定義したものになります。

interface Storage {
    fun save(data: GameData)
}

インターフェースを実装する

インターフェースはメソッドの形しか定義していないので、実際に呼ばれた時の処理をどこかに記述しないといけません。インターフェースで定義されているメソッドの処理を記述することを実装すると呼びます。

次の例は、クラス A でインターフェースを実装する例です。クラスの継承と似ていますが、()を付けない点が異なります。

open class A: Storage {
    override fun save(data: GameData) {
        // ファイルに保存する処理
    }
}

クラス A は Storage インターフェースを実装しているので、次のように Storage 型の変数に代入することができます。

val s: Storage = A()
s.save(GameData(1))

ボタンをタップしたときのリスナーなど、わざわざクラスを定義したくない場面もあるでしょう。その場合は object: インターフェース名とすることで、無名クラスで実装することができます。

val s: Storage = object: Storage {
    override fun save(data: GameData) {
        // 保存する処理
    }
}
s.save(GameData(1)) 

まとめ

Kotlin のクラスに関する言語機能について説明しました。公式ドキュメントは、クラスやインターフェースがどのようなものかを知っている前提で記述されているようなので、本記事が学習の参考になれば幸いです。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。