Kotlin でクラスとオブジェクトを使用する

1. 始める前に

この Codelab では、Kotlin でクラスとオブジェクトを使用する方法について説明します。

クラスは、オブジェクトを作成する際の土台にする設計図を提供します。オブジェクトは、そのオブジェクト用のデータで構成されるクラスのインスタンスです。オブジェクトとクラス インスタンスは同じ意味です。

家を建てることを考えてみましょう。クラスは建築家が描いた設計プランに似ています。これは設計図とも呼ばれます。設計図は家ではありません。家を建てる方法を示した説明書です。家は実在物であり、設計図に従って作成されたオブジェクトであると言えます。

家の設計図に複数の部屋があり、各部屋に独自の設計と用途があるように、各クラスには独自の設計と用途があります。クラスの設計方法を理解するには、データ、ロジック、動作をオブジェクトで包み込む方法を指南する、オブジェクト指向プログラミング(OOP)というフレームワークについて理解する必要があります。

OOP を使用すると、実世界の複雑な問題を小さなオブジェクトにまとめて単純化することができます。OOP には 4 つの基本コンセプトがあります。それぞれのコンセプトの詳細については、この Codelab で後ほど説明します。

  • カプセル化。関連するプロパティと、それらのプロパティにアクションを実行するメソッドをクラスで包みます。スマートフォンについて考えてみましょう。カメラ、ディスプレイ、メモリカードなどのハードウェア部品やソフトウェア部品がカプセル化されています。部品が内部でどのように接続されているかを気にする必要はありません。
  • 抽象化。カプセル化の延長で、内部の実装ロジックを可能な限り隠すという考え方です。たとえば、スマートフォンで写真を撮るには、カメラアプリを開き、撮影するシーンにスマートフォンを向けて、ボタンをクリックする必要があります。カメラアプリの作成方法や、スマートフォンのカメラ ハードウェアの仕組みを知る必要はありません。つまり、カメラアプリの内部の仕組みやモバイルカメラが写真を撮影する方法が抽象化され、重要なタスクを実行できるようになっています。
  • 継承。親子関係を作ることで、他のクラスの特性と動作のうえにクラスを作成できるようにします。たとえば、各メーカーは Android OS を搭載したさまざまなモバイル デバイスを製造していますが、デバイスごとに UI は異なります。つまり、メーカーは Android OS の機能を継承し、そのうえにカスタマイズを行っていると言えます。
  • ポリモーフィズム。この単語は、ギリシャ語の「ポリ」(多数)と「モーフィズム」(形態)を合わせたものです。ポリモーフィズムとは、異なるオブジェクトを単一かつ共通の方法で使用することを指します。たとえば、Bluetooth スピーカーをスマートフォンに接続する場合、スマートフォンが知る必要があるのは Bluetooth で音声を再生できるデバイスがあることだけです。さまざまな Bluetooth スピーカーを使用できますが、スマートフォンが各スピーカーの使い方を個別に知っている必要はありません。

最後に、プロパティ委譲についても学習します。これを使用すると、プロパティ値を操作する再利用可能なコードを簡潔な構文で記述できます。この Codelab では、スマートホーム アプリのクラス構造を構築することを題材にして、こうしたコンセプトについて学習します。

前提条件

  • Kotlin のプレイグラウンドでコードを開き、編集し、実行できる。
  • Kotlin プログラミングの基礎知識(変数と関数を含む)、および println() 関数と main() 関数の知識

学習内容

  • OOP の概要
  • クラスとは何か
  • コンストラクタ、関数、プロパティを備えたクラスを定義する方法
  • オブジェクトをインスタンス化する方法
  • 継承とは何か
  • IS-A 関係と HAS-A 関係の違い
  • プロパティと関数をオーバーライドする方法
  • 可視性修飾子とは
  • 委譲とは何か、by 委譲の使用方法

作成するコードの概要

  • スマートホームのクラス構造
  • スマートテレビやスマートライトなどのスマート デバイスを表すクラス

必要なもの

  • ウェブブラウザがインストールされた、インターネットに接続できるパソコン

2. クラスを定義する

クラスを定義するときには、そのクラスのすべてのオブジェクトに必要なプロパティとメソッドを指定します。

クラス定義は class キーワードで始まり、その後に名前、中括弧の組が続きます。構文の左中括弧の前の部分は、クラスヘッダーとも呼ばれます。中括弧の内側では、クラスのプロパティと関数を指定できます。プロパティと関数については後ほど説明します。クラス定義の構文を次の図に示します。

class キーワードで始まり、名前、左中括弧と右中括弧の組と続きます。中括弧には、設計図を記述したクラスの本体が入ります。

クラスには、以下のような命名規則が推奨されています。

  • クラス名は任意に選択できますが、Kotlin のキーワードは使用しません(例: fun キーワード)。
  • クラス名は PascalCase(各単語が大文字で始まり、単語間にスペースがない)で記述します。たとえば、SmartDevice では、各単語の先頭を大文字にし、単語間にはスペースを入れません。

クラスには、主に 3 つの構成要素があります。

  • プロパティ。クラスのオブジェクトの属性を指定する変数。
  • メソッド。クラスの動作とアクションを含んでいる関数。
  • コンストラクタ。クラスを定義しているプログラムの中でそのクラスのインスタンスを作成する特別なメンバー関数。

クラスを扱う課題は今回が初めてではありません。これまでの Codelab で、IntFloatStringDouble などのデータ型について学習しました。Kotlin では、これらのデータ型がクラスとして定義されています。次のコード スニペットに示すように変数を定義すると、1 の値でインスタンス化された Int クラスのオブジェクトが作成されます。

val number: Int = 1

SmartDevice クラスを定義しましょう。

  1. Kotlin のプレイグラウンドで、内容を空の main() 関数に置き換えます。
fun main() {
}
  1. main() 関数の前の行で、本体に // empty body というコメントが入った SmartDevice クラスを定義します。
class SmartDevice {
    // empty body
}

fun main() {
}

3. クラスのインスタンスを作成する

すでに学習したとおり、クラスはオブジェクトの設計図です。Kotlin ランタイムは、クラス(設計図)を使用して、その型のオブジェクトを作成します。この SmartDevice クラスを、スマート デバイスの設計図として使用できます。プログラムで実際のスマート デバイスを使うには、SmartDevice のオブジェクト インスタンスを作成する必要があります。インスタンス化構文は、次の図に示すように、クラス名で始まり、その後に括弧の組が続きます。

1d25bc4f71c31fc9.png

オブジェクトを使用するには、変数の定義と同様に、オブジェクトを作成して変数に代入します。不変変数は val キーワードを使用して作成し、可変変数は var キーワードを使用して作成します。val キーワードまたは var キーワードの後に、変数名、= 代入演算子、クラス オブジェクトのインスタンス化と続きます。この構文を図にすると次のようになります。

f58430542f2081a9.png

SmartDevice クラスをオブジェクトとしてインスタンス化しましょう。

  • main() 関数で、val キーワードを使用して smartTvDevice という名前の変数を作成し、SmartDevice クラスのインスタンスとして初期化します。
fun main() {
    val smartTvDevice = SmartDevice()
}

4. クラスメソッドを定義する

ユニット 1 では、以下のことを学びました。

  • 関数の定義には、fun キーワードの後に、括弧の組と中括弧の組を続けたものを使用します。中括弧の中には、タスクを実行するために必要な手順であるコードが含まれています。
  • 関数を呼び出すと、その関数に含まれるコードが実行されます。

クラスが実行できるアクションは、クラスの関数として定義されます。たとえば、スマート デバイス、スマートテレビ、スマートライトを所有していて、スマートフォンでオンとオフを切り替えられるとします。スマート デバイスは、プログラミングでは SmartDevice クラスに置き換えられ、オンとオフを切り替えるアクションは、オン / オフ動作を実現する turnOn() 関数と turnOff() 関数で表されます。

クラスで関数を定義する構文は、前に学習したものと同じです。唯一の違いは、関数がクラス本体にあることです。クラス本体で定義された関数は、メンバー関数またはメソッドと呼ばれ、クラスの動作を表します。この Codelab の残りの部分では、クラスの本体にある関数をメソッドと呼びます。

SmartDevice クラスで turnOn() メソッドと turnOff() メソッドを定義しましょう。

  1. SmartDevice クラスの本体で、本体が空の turnOn() メソッドを定義します。
class SmartDevice {
    fun turnOn() {

    }
}
  1. turnOn() メソッドの本体に println() 文を追加し、"Smart device is turned on." という文字列を渡します。
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. turnOn() メソッドの後に、"Smart device is turned off." という文字列を出力する turnOff() メソッドを追加します。
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

オブジェクトのメソッドを呼び出す

ここまで、スマート デバイスの設計図となるクラスを定義し、そのクラスのインスタンスを作成して変数に代入しました。次に、SmartDevice クラスのメソッドを使用して、デバイスの電源をオンにしてからオフにします。

クラス内でのメソッドの呼び出しは、前の Codelab の main() 関数から他の関数を呼び出す方法に似ています。たとえば、turnOn() メソッドから turnOff() メソッドを呼び出す必要がある場合は、次のコード スニペットのように記述します。

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

クラスの外部でクラスメソッドを呼び出すには、クラス オブジェクトの後に、. 演算子、関数名、括弧の組と続けたものを使用します。必要に応じて、メソッドに必要な引数を括弧内に入れます。この構文を図にすると次のようになります。

fc609c15952551ce.png

このオブジェクトの turnOn() メソッドと turnOff() メソッドを呼び出しましょう。

  1. main() 関数の smartTvDevice 変数の後の行で、turnOn() メソッドを呼び出します。
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. turnOn() メソッドの後の行で、turnOff() メソッドを呼び出します。
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. コードを実行します。

次のような出力が表示されます。

Smart device is turned on.
Smart device is turned off.

5. クラスのプロパティを定義する

ユニット 1 では、変数(1 つのデータを納めるコンテナ)について学びました。val キーワードを使用して読み取り専用変数を作成する方法と、var キーワードを使用して可変変数を作成する方法を学びました。

メソッドはクラスが実行できるアクションを定義しますが、プロパティはクラスの特性またはデータ属性を定義します。たとえば、スマート デバイスには次のような特性があります。

  • 名前。デバイスの名前。
  • カテゴリ。スマート デバイスのタイプ(エンターテイメント、設備、調理など)。
  • デバイスのステータス。デバイスがオンかオフか、オンラインかオフラインか。デバイスは、インターネットに接続されているときにオンラインとみなされ、そうでないときにはオフラインとみなされます。

プロパティは基本的に、関数本体ではなくクラス本体で定義される変数だと言えます。定義する構文は変数と同じだということです。不変プロパティは val キーワードで定義し、可変プロパティは var キーワードで定義します。

上で述べた特性を SmartDevice クラスのプロパティとして実装しましょう。

  1. turnOn() メソッドの前の行で、name プロパティを定義して "Android TV" という文字列を代入します。
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. name プロパティの後の行で、category プロパティを定義して "Entertainment" という文字列を代入し、deviceStatus プロパティを定義して "online" という文字列を代入します。
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. smartTvDevice 変数の後の行で、println() 関数を呼び出して、"Device name is: ${smartTvDevice.name}" という文字列を渡します。
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. コードを実行します。

次のような出力が表示されます。

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

プロパティのゲッター関数とセッター関数

プロパティでは、変数よりも多くの処理を行えます。たとえば、スマートテレビを表すクラス構造を作成するとします。一般的な操作の一つに、音量の上げ下げがあります。このアクションをプログラミングで表現するために、speakerVolume という名前のプロパティを作成します。このプロパティには、テレビのスピーカーに設定されている現在の音量レベルが保持されていますが、音量の値には設定可能な範囲があります。設定できる音量の最小値は 0 で、最大値は 100 です。speakerVolume プロパティが 100 を超えたり、0 を下回ったりしないように、セッター関数を作成します。プロパティの値を更新するときに、値が 0 から 100 の範囲内にあるかどうかを確認する必要があります。別の例として、名前を常に大文字で記述する必要がある場合を考えます。ゲッター関数を実装すれば、そこで name プロパティを大文字に変換できます。

これらのプロパティを実装する方法を詳しく確認する前に、宣言するための完全な構文を理解しておく必要があります。可変プロパティを定義する完全な構文は、変数定義から始まり、その後に省略可能な get() 関数と set() 関数が続きます。この構文を図にすると次のようになります。

f2cf50a63485599f.png

プロパティにゲッター関数とセッター関数を定義しない場合は、Kotlin コンパイラが内部的に作成します。たとえば、var キーワードを使用して speakerVolume プロパティを定義し、2 という値を代入すると、コンパイラは次のコード スニペットのようにゲッター関数とセッター関数を自動生成します。

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

これらの行は、コンパイラがバックグラウンドで追加するものであり、コードには現れません。

不変プロパティの完全な構文は、次の 2 つの点が異なります。

  • val キーワードで始まります。
  • val 型の変数は読み取り専用であるため、set() 関数がありません。

Kotlin プロパティは、メモリに値を保持するためにバッキング フィールドを使用します。バッキング フィールドは、基本的には、プロパティで内部的に定義されているクラス変数です。バッキング フィールドのスコープはプロパティです。つまり、get() プロパティ関数や set() プロパティ関数からのみアクセスできます。

get() 関数内でのプロパティ値の読み取りと、set() 関数内での値の更新には、プロパティのバッキング フィールドを使用する必要があります。これは Kotlin コンパイラにより自動生成され、field 識別子で参照されます。

たとえば、set() 関数内でプロパティの値を更新する場合は、次のコード スニペットのように、value パラメータとして参照される set() 関数のパラメータを使用し、それを field 変数に代入します。

var speakerVolume = 2
    set(value) {
        field = value    
    }

たとえば、speakerVolume プロパティに割り当てる値を 0 から 100 の範囲にするには、次のコード スニペットに示すようなセッター関数を実装します。

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

set() 関数は、in キーワードと値の範囲を使用して、Int 値が 0 から 100 の範囲内にあるかどうかをチェックします。値が範囲内の場合、field の値が更新されます。それ以外の場合、プロパティの値は変更されません。

このプロパティは、この Codelab の「クラス間の関係を実装する」セクションでクラスに追加しますので、ここでセッター関数をコードに追加する必要はありません。

6. コンストラクタを定義する

コンストラクタの主な目的は、クラスのオブジェクトを作成する方法を定めることです。別の言い方をすると、コンストラクタがオブジェクトを初期化することで、そのオブジェクトが使用可能になるということです。この操作は、オブジェクトをインスタンス化するときに行いました。クラスのオブジェクトがインスタンス化されるときに、コンストラクタ内のコードが実行されます。コンストラクタは、パラメータの有無にかかわらず定義できます。

デフォルト コンストラクタ

デフォルト コンストラクタは、パラメータのないコンストラクタです。デフォルト コンストラクタは、次のコード スニペットに示すように定義します。

class SmartDevice constructor() {
    ...
}

Kotlin では簡潔な表現を目指しており、コンストラクタにアノテーションや可視性修飾子がない場合は、後で学習するように constructor キーワードを省くことができます。次のコード スニペットに示すように、コンストラクタにパラメータがない場合は、括弧も省くことができます。

class SmartDevice {
    ...
}

Kotlin コンパイラはデフォルト コンストラクタを自動生成します。自動生成されるデフォルト コンストラクタは、コンパイラがバックグラウンドで追加するため、コードには現れません。

パラメータ付きコンストラクタを定義する

SmartDevice クラス内では、name プロパティと category プロパティを変更できません。そのため、SmartDevice クラスのすべてのインスタンスで、必ず name プロパティと category プロパティを初期化する必要があります。現在の実装では、name プロパティと category プロパティの値がハードコードされています。これは、すべてのスマート デバイスが "Android TV" という文字列の名前を持ち、"Entertainment" という文字列のカテゴリに分類されることを意味します。

不変性を維持したまま、値のハードコードを避けるために、パラメータ付きコンストラクタを使用して初期化します。

  • SmartDevice クラスで、デフォルト値を指定せずに name プロパティと category プロパティをコンストラクタに移動します。
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

これで、プロパティを設定するためのパラメータをコンストラクタに渡せるようになったので、このクラスのオブジェクトをインスタンス化する方法も変わります。オブジェクトをインスタンス化するための完全な構文を次の図に示します。

bbe674861ec370b6.png

コードで表すと次のようになります。

SmartDevice("Android TV", "Entertainment")

このコンストラクタの引数はどちらも文字列です。どの値がどのパラメータのものなのか判別しにくくなっています。これを解決するには、関数に引数を渡した方法と同様に、次のコード スニペットに示すように名前付き引数を備えたコンストラクタを作成します。

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin のコンストラクタには、主に 2 つのタイプがあります。

  • プライマリ コンストラクタ。クラスには、プライマリ コンストラクタを 1 つだけ定義でき、これはクラスヘッダーの中で定義します。プライマリ コンストラクタは、デフォルト コンストラクタかパラメータ付きコンストラクタのいずれかです。プライマリ コンストラクタに本体はありません。つまり、コードがありません。
  • セカンダリ コンストラクタ。クラスには、複数のセカンダリ コンストラクタを定義できます。セカンダリ コンストラクタは、パラメータの有無にかかわらず定義できます。セカンダリ コンストラクタは、クラスを初期化することができ、本体を持たせてそこに初期化ロジックを入れることができます。クラスにプライマリ コンストラクタがある場合、各セカンダリ コンストラクタでプライマリ コンストラクタを初期化する必要があります。

プライマリ コンストラクタを使用すると、クラスヘッダー内でプロパティを初期化できます。コンストラクタに渡される引数は、プロパティに代入されます。プライマリ コンストラクタを定義する構文は、クラス名の後に constructor キーワード、括弧のペアと続きます。括弧内にはプライマリ コンストラクタのパラメータが入ります。パラメータが複数ある場合は、パラメータ定義をカンマで区切ります。プライマリ コンストラクタを定義する完全な構文を次の図に示します。

aa05214860533041.png

セカンダリ コンストラクタは、クラスの本体の中にあり、その構文は以下の 3 つの部分で構成されます。

  • セカンダリ コンストラクタの宣言。セカンダリ コンストラクタの定義は、constructor キーワードで始まり、その後に括弧のペアが続きます。セカンダリ コンストラクタに必要なパラメータがあれば、それを括弧内に入れます。
  • プライマリ コンストラクタの初期化。初期化は、コロンで始まり、その後に this キーワード、括弧のペアと続きます。プライマリ コンストラクタに必要なパラメータがある場合は、それを括弧内に入れます。
  • セカンダリ コンストラクタの本体。セカンダリ コンストラクタの本体は、プライマリ コンストラクタの初期化の後ろに、中括弧で囲んで記述します。

この構文を図にすると次のようになります。

2dc13ef136009e98.png

例として、スマート デバイス プロバイダが開発した API を統合する場合を考えます。この API は、初期デバイス ステータスを示す Int タイプのステータス コードを返します。また、この API は、デバイスがオフラインの場合は 0 の値を返し、オンラインの場合は 1 の値を返します。それ以外の整数値の場合、ステータスは不明とみなされます。次のコード スニペットに示すように、SmartDevice クラスにセカンダリ コンストラクタを作成すると、この statusCode パラメータを文字列表現に変換できます。

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. クラス間で関係を作る

継承を使用すると、別のクラスの特性と動作に基づいてクラスを作成できます。これは、再利用可能なコードを記述し、クラス間で関係を作るための有効で強力なメカニズムです。

たとえば、スマートテレビ、スマートライト、スマート スイッチなど、多くのスマート デバイスが販売されています。プログラミングでスマート デバイスを表すとき、名前、カテゴリ、ステータスなどの共通の特性を各デバイスが共有します。また、オンとオフを切り替えられるといった共通の動作もあります。

ただし、スマート デバイスによってオンとオフの切り替え方は異なります。たとえば、テレビの電源をオンにするには、ディスプレイをオンにしてから、最後に設定されていた音量とチャンネルを設定し直す必要があります。一方、ライトをオンにする場合に必要なのは、明るさの増減だけです。

また、各スマート デバイスには、その他にも実行できる機能やアクションがあります。たとえば、テレビの場合は、音量の調節やチャンネルの変更ができます。ライトの場合は、明るさや色を調整できます。

つまり、すべてのスマート デバイスが異なる機能を持っている一方で、共通の特性もあるということです。こういった共通の特性は、各スマート デバイス クラスにコピーすることもできますが、継承してコードを再利用可能にすることもできます。

継承するには、SmartDevice の親クラスを作成し、上記の共通のプロパティと動作を定義する必要があります。さらに、親クラスのプロパティを継承する SmartTvDevice クラスや SmartLightDevice クラスなどの子クラスを作成します。

これをプログラミング用語では、SmartTvDevice クラスと SmartLightDevice クラスが SmartDevice 親クラスを「拡張」していると表現します。親クラスはスーパークラスとも呼ばれ、子クラスはサブクラスとも呼ばれます。これらの関係を次の図に示します。

クラスの継承関係を表す図。

ただし Kotlin では、すべてのクラスがデフォルトで final です。これはつまり、そのようなクラスは拡張できないということなので、クラス間の関係を定義する必要があります。

SmartDevice スーパークラスとそのサブクラスの関係を定義しましょう。

  1. SmartDevice スーパークラスで、class キーワードの前に open キーワードを追加して拡張可能にします。
open class SmartDevice(val name: String, val category: String) {
    ...
}

open キーワードは、このクラスが拡張可能であることをコンパイラに知らせるもので、これによって他のクラスがこのクラスを拡張できるようになります。

サブクラスを作成する構文は、すでに行ったように、クラスヘッダーの作成から始まります。コンストラクタの右括弧の後に、スペース、コロン、もう一つのスペース、スーパークラス名、括弧のペアと続けます。括弧内には、必要に応じて、スーパークラスのコンストラクタで必要なパラメータを入れます。この構文を図にすると次のようになります。

1ac63b66e6b5c224.png

  1. SmartDevice スーパークラスを拡張する SmartTvDevice サブクラスを作成します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDeviceconstructor 定義では、プロパティが可変か不変かを指定しません。つまり、deviceName パラメータと deviceCategory パラメータは、クラス プロパティではなく、単なる constructor パラメータです。このクラスでは使用できず、スーパークラスのコンストラクタに渡すだけです。

  1. SmartTvDevice サブクラスの本体で、ゲッター関数とセッター関数について学習したときに作成した speakerVolume プロパティを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }