前回はKotlinの基本文法と関数について解説しました。今回はKotlinにおけるオブジェクト指向プログラミングを実現するクラスと、それを取り巻く機能を紹介します。
クラスの定義とインスタンスの生成
KotlinはJavaと同じくクラスベースのオブジェクト指向言語です。すなわち、まずクラスがあり、それからインスタンスを生成し利用するというスタイルです。
メンバをいっさい持たないクラスの定義例をリスト1に示します。Javaと同様class
キーワードが必要です。
このMyClass
クラスをインスタンス化してみましょう(リスト2)。
MyClass()
によりMyClass
クラスのインスタンスが生成されます。new
のようなキーワードは必要ありません(Kotlinではnew
はキーワードではありません)。リスト2ではMyClass
クラスのインスタンスを生成して、その参照を変数obj
に代入しています。それをprintln
するとクラス名とハッシュコードが出力されます。
メソッド
メソッドを持ったクラスを定義しましょう。リスト3のGreeter
クラスはgreet
メソッドを持っています。このようにクラスはメンバを波括弧({}
)で括る必要があります。メソッドはfun
キーワードを使って関数のように記述します。
リスト4のようにGreeter
クラスをインスタンス化して、そのインスタンスに対してgreet
メソッドを呼び出します。
プロパティ
クラスはプロパティを持つことができます。プロパティとは、平たく言えばJavaにおけるフィールドとそのアクセサ(setterやgetter)が一緒になったものと説明できます。
リスト5のUser
クラスはid
とname
という名前のプロパティを持っています。インスタン化してプロパティに値を設定したり取得したりしてみましょう。
リスト6の2行目ではname
プロパティに値を設定しています。そして3行目でname
プロパティとid
プロパティの値を取得しています。Javaにおけるフィールドに直接アクセスしているように見えますが、実際には(デフォルトで生成された)setterやgetterを介してアクセスしています。
setterやgetterをカスタマイズしたい場合はリスト7のように、関数に似た記法を用いて自由に処理を盛り込めます。誌面の都合上、詳細は割愛させていただきます。公式ドキュメントをご覧ください。
コンストラクタ引数
クラス名の後に続けてコンストラクタの引数リストを宣言できます。ここで受け取った引数でもってプロパティを初期化することができます。
リスト8はもっとシンプルになります。
リスト9のようにコンストラクタ引数の名前の前にval
やvar
を付けることでプロパティの定義も兼ねることができ、スッキリした見た目にできます。
継承
Javaやそのほかの言語にもあるように、クラスは別のクラスを継承できます。継承すると継承元クラス(スーパクラス)の型のサブ型となり、またスーパクラスのメンバを自クラスのもののように扱うことができるようになります。
このことをシンプルな例から確認しましょう。まずスーパクラスとなるFoo
クラスを定義します(リスト10)。
Foo
クラスは、コンストラクタ引数、プロパティを持たず、foo
メソッドを持ったクラスです。さらに注目すべきポイントはopen
という修飾子が付いていることです。Kotlinでは、クラスが継承可能であることをopen
修飾子を付けて明示する必要があります。逆を言えばopen
が付いていないクラスは継承できません[1]。
リスト11にFoo
を継承したBar
クラスを定義します。
クラス名の後に続けてコロン(:
)、スーパクラスのコンストラクタ呼び出しを記述します。今回、Foo
クラスはコンストラクタ引数がないのでFoo()
となっています。
ではBar
のインスタンスからスーパクラスで定義したメソッドを呼び出せるか確かめてみましょう(リスト12)。そしてBar
は、Foo
のサブ型なのでval foo: Foo = bar
は有効なコードです。
抽象クラス
インスタンス化できない抽象クラスというものを定義できます。abstract
修飾子をクラスに付けるだけです。抽象クラスは抽象メンバを持つことができ、これを継承する具象クラスでの実装を義務づけることができます。
リスト13のGreeter
抽象クラスは抽象プロパティname
と抽象メソッドgreet
を持っています。これらの抽象メンバはシグネチャだけで実装を持たないことがわかります。Greeter
の具象サブクラスをリスト14に定義します。ここでは各抽象メンバをoverride
修飾子付きでオーバライドしています。Kotlinではオーバライドする際にはoverride
修飾子が必須です。ちなみに、abstract
なクラスはopen
なしで継承可能なクラスとなります。
インターフェース
Javaのようにインターフェースを定義することもできます。インターフェースは実装を持つことができますが、プロパティの初期値を持てなかったり、コンストラクタを持てない点などで抽象クラスと異なります。
Kotlinでインターフェースを定義するにはtrait
キーワード[2]を用います(リスト15)。
インターフェースでは抽象メンバにabstract
を付ける必要はありません。インターフェースを実装するにはクラス名の後に続けてコロンとインターフェース名を記述するだけです。クラスの継承とは異なり、コンストラクタ呼び出しはありません。リスト16とリスト14の違いはGreeter
かGreeter()
かだけです。
ところで、インターフェースはデフォルトの実装を持てます(リスト17)。
ここでFoo
インターフェースの具象メソッドであるsay
と同じ名前(と引数型)を具象メソッドとして持つほかのインターフェースBar
があったとき、Foo
とBar
両方を実装するクラスのsay
メソッドは、デフォルトでどちらの実装を使用するのでしょうか(リスト18)。
その答えは、デフォルトではどちらの実装も使用しません。正確に言うとFoo
とBar
の実装クラスは、say
メソッドのオーバライドの義務が発生します(リスト19)。
インターフェースのデフォルト実装を使用したい場合は、オーバライドしたメソッド内で明示的に呼び出します(リスト20)。このようにしてKotlinでは具象メソッドの衝突の問題を解決しています。
委譲
既存のクラスに機能を追加したい状況に直面したことのある人は多いはずです。
たとえばString
クラスに自身が表現する文字列を対象に挨拶するようなhello
というメソッドを追加したいとしましょう。
アプローチの1つとして委譲による実現を採用します。
リスト21のGreetableCharSeq
のようなクラスを定義すれば、hello
メソッドが追加されたCharSequence
[3]を得ることができます。
リスト22は、CharSequence
の実装クラスとしてString
クラスのインスタンスである"World"
をコンストラクタに渡してGreetableCharSeq
をインスタンス化しています。実行すると期待どおりHello, Worldと出力されます。
hello
メソッドの追加に成功しました! しかし1つ問題があります。それはリスト21の「実装を委譲する」「その他多数のメソッド...」のコメントのとおり、今回はとくに関心のないメソッドに関してもオーバライドして、委譲して、という一連の苦痛な作業が強いられることです。
安心してください! Kotlinではこの問題の解決策を言語機能として提供しています。リスト21を書き直したリスト23をご覧ください。
: CharSequence
の部分が: CharSequence bycs
に変わりました。by cs
は、コンストラクタ引数として受け取ったCharSequence
型のcs
に委譲する、という意味になります。この記述のおかげで、独自にオーバライドする必要のないメソッドをコーディングせずに済みます。
この機能の別の例とちょっとした解説を筆者のブログに書いていますのでご参照ください。
拡張関数
前節のようなhello
メソッドを追加するためだけに新しいクラスを定義するのは大変ですし、インスタンス化のオーバヘッドも気になります。メソッドを既存クラスに追加するという観点では少し異なりますが、リスト24のようなアプローチのほうがよさそうです。
実は、まさにこのように展開され、かつ「既存クラスにメソッド追加」のように見える機能がKotlinには用意されています。拡張関数(Extension Function)と呼ばれる言語機能です。
リスト25を見ると、本当にhello
メソッドがString
に追加されたかのように見えます。実際にはstatic void hello(String s)
に相当するコードをコンパイラは生成します。重要なのは動的に生成されるのではなく、静的に解決されるということです。型安全、低コストを期待できます。
演算子オーバロード
Kotlinには演算子オーバロードと呼ばれるしくみがあります。既存の演算子をほかの型に対しても使えるように拡張する機能です。たとえば独自にRational
というクラスを定義したとします。この有理数を表現するクラスのインスタンス同士、加算や乗算などできると便利です。この時にメソッド呼び出しによる記法ではなく、演算子を用いてa + b
のように記述できるようにしてくれるのが演算子オーバロードです。
演算子は、特定のシグネチャを持ったメソッドと対応しています。Rational
同士に対して使える+
演算子は、Rational
のfun plus(rational: Rational): Rational
というようなシグネチャのメソッドを定義することで使えるようになります。
具体例を示すにあたって話を簡単にするためにリスト26のようなMyInt
クラスを定義します。Int値を1つだけ持っている単純なクラスです。
plus
メソッドにより、自身と他のMyInt
と加算ができます。そして、このplus
メソッドにより+
演算子が使えるようになります(リスト27)。
リスト28にもう1つ面白い例を示しましょう。演算子に対応するメソッドは、拡張関数として定義しても有効です。リスト28では拡張関数としてStringBuilder
にplusAssign
メソッドを追加しています。このシグネチャは+=演算子に対応します(リスト29)。そのほかの演算子と、そのメソッドシグネチャについては公式ドキュメントに掲載されている対応表を参照してください。
まとめ
今回はKotlinにおけるクラスとその周辺機能について紹介しました。
クラスやメソッドの定義、インスタンスの生成などはJavaのそれらと似ていますが、クラスはフィールドとアクセサが一緒になったようなプロパティを持てるという点でJava より強力です。
インターフェースは実装を持てる点で抽象クラスに似ています。同名の具象メソッドの衝突問題を回避するためのルールが用意されています。
既存の型の機能強化をしたい場合には委譲や拡張関数などの言語機能を使用すると便利です。
特定のシグネチャを持ったメソッドを定義することで演算子が使えるようになる演算子オーバロードを紹介しました。演算子を使用することで可読性の向上を期待できます。
次回はKotlinのユニークな機能であるNULL安全についてじっくり解説します。
- 第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
- 第2特集
「知りたい」「使いたい」「発信したい」をかなえる
OSSソースコードリーディングのススメ
- 特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
- 短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
- 短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
- 短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)