プログラマに優しい現実指向JVM言語 Kotlin入門

第4回クラスを学ぶ

前回はKotlinの基本文法と関数について解説しました。今回はKotlinにおけるオブジェクト指向プログラミングを実現するクラスと、それを取り巻く機能を紹介します。

クラスの定義とインスタンスの生成

KotlinはJavaと同じくクラスベースのオブジェクト指向言語です。すなわち、まずクラスがあり、それからインスタンスを生成し利用するというスタイルです。

メンバをいっさい持たないクラスの定義例をリスト1に示します。Javaと同様classキーワードが必要です。

リスト1 最小のクラス定義
class MyClass

このMyClassクラスをインスタンス化してみましょうリスト2⁠。

リスト2 クラスのインスタンス化
val obj = MyClass()
println(obj) // => MyClass@604e9f7f

MyClass()によりMyClassクラスのインスタンスが生成されます。newのようなキーワードは必要ありません(Kotlinではnewはキーワードではありません⁠⁠。リスト2ではMyClassクラスのインスタンスを生成して、その参照を変数objに代入しています。それをprintlnするとクラス名とハッシュコードが出力されます。

メソッド

メソッドを持ったクラスを定義しましょう。リスト3Greeterクラスはgreetメソッドを持っています。このようにクラスはメンバを波括弧{}で括る必要があります。メソッドはfunキーワードを使って関数のように記述します。

リスト3 メソッドを持ったクラス
class Greeter {
  fun greet() {
    println("Hello!")
  }
}

リスト4のようにGreeterクラスをインスタンス化して、そのインスタンスに対してgreetメソッドを呼び出します。

リスト4 メソッドの呼び出し class User {
val greeter = Greeter()
greeter.greet() // => Hello!

プロパティ

クラスはプロパティを持つことができます。プロパティとは、平たく言えばJavaにおけるフィールドとそのアクセサ(setterやgetter)が一緒になったものと説明できます。

リスト5Userクラスはidnameという名前のプロパティを持っています。インスタン化してプロパティに値を設定したり取得したりしてみましょう。

リスト5 プロパティを持ったクラス
var id: Long = 0
  var name: String = ""
  }

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

リスト6 プロパティの使い方
val taro = User()
taro.name = "Taro"
println("${taro.name}(${taro.id})") // = > Taro(0)

setterやgetterをカスタマイズしたい場合はリスト7のように、関数に似た記法を用いて自由に処理を盛り込めます。誌面の都合上、詳細は割愛させていただきます。公式ドキュメントをご覧ください。

リスト7 setterとgetterのカスタマイズ
class User {
  var id: Long = 0

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

コンストラクタ引数

クラス名の後に続けてコンストラクタの引数リストを宣言できます。ここで受け取った引数でもってプロパティを初期化することができます。

リスト8はもっとシンプルになります。

リスト8 コンストラクタ引数でプロパティ初期化
class User(id: Long, name: String) {
  val id = id
  val name = name
}

リスト9のようにコンストラクタ引数の名前の前にvalvarを付けることでプロパティの定義も兼ねることができ、スッキリした見た目にできます。

リスト9 コンストラクタ引数でプロパティ定義
class User(val id: Long, val name: String)

継承

Javaやそのほかの言語にもあるように、クラスは別のクラスを継承できます。継承すると継承元クラス(スーパクラス)の型のサブ型となり、またスーパクラスのメンバを自クラスのもののように扱うことができるようになります。

このことをシンプルな例から確認しましょう。まずスーパクラスとなるFooクラスを定義しますリスト10⁠。

リスト10 openなクラス
open class Foo {
  fun foo() {
  println("Foo")
  }
}

Fooクラスは、コンストラクタ引数、プロパティを持たず、fooメソッドを持ったクラスです。さらに注目すべきポイントはopenという修飾子が付いていることです。Kotlinでは、クラスが継承可能であることをopen修飾子を付けて明示する必要があります。逆を言えばopenが付いていないクラスは継承できません[1]⁠。

リスト11Fooを継承したBarクラスを定義します。

リスト11 Fooのサブクラス
class Bar: Foo() {
  fun bar() {
    println("Bar")
  }
}

クラス名の後に続けてコロン:⁠、スーパクラスのコンストラクタ呼び出しを記述します。今回、Fooクラスはコンストラクタ引数がないのでFoo()となっています。

ではBarのインスタンスからスーパクラスで定義したメソッドを呼び出せるか確かめてみましょうリスト12⁠。そしてBarは、Fooのサブ型なのでval foo: Foo = barは有効なコードです。

リスト12 スーパクラスのメソッド呼び出し
val bar: Bar = Bar()
bar.foo() // => Foo
bar.bar() // => Bar

抽象クラス

インスタンス化できない抽象クラスというものを定義できます。abstract修飾子をクラスに付けるだけです。抽象クラスは抽象メンバを持つことができ、これを継承する具象クラスでの実装を義務づけることができます。

リスト13Greeter抽象クラスは抽象プロパティnameと抽象メソッドgreetを持っています。これらの抽象メンバはシグネチャだけで実装を持たないことがわかります。Greeterの具象サブクラスをリスト14に定義します。ここでは各抽象メンバをoverride修飾子付きでオーバライドしています。Kotlinではオーバライドする際にはoverride修飾子が必須です。ちなみに、abstractなクラスはopenなしで継承可能なクラスとなります。

リスト13 抽象クラスの例
abstract class Greeter {
  abstract val name: String
  abstract fun greet()
}
リスト14 具象サブクラスの例
class JapaneseGreeter(
    override val name: String
  ): Greeter() {

  override fun greet() {
    println("こんにちは,私は${name}です")
  }
}

インターフェース

Javaのようにインターフェースを定義することもできます。インターフェースは実装を持つことができますが、プロパティの初期値を持てなかったり、コンストラクタを持てない点などで抽象クラスと異なります。

Kotlinでインターフェースを定義するにはtraitキーワード[2]を用いますリスト15⁠。

リスト15 インターフェースの例
trait Greeter {
  val name: String
  fun greet()
}

インターフェースでは抽象メンバにabstractを付ける必要はありません。インターフェースを実装するにはクラス名の後に続けてコロンとインターフェース名を記述するだけです。クラスの継承とは異なり、コンストラクタ呼び出しはありません。リスト16とリスト14の違いはGreeterGreeter()かだけです。

リスト16 インターフェース実装の例
class JapaneseGreeter(
    override val name: String
  ): Greeter {

  override fun greet() {
    println("こんにちは,私は${name}です")
  }
}

ところで、インターフェースはデフォルトの実装を持てますリスト17⁠。

リスト17 実装を持ったインターフェース
trait Foo {
  fun say() {
    println("foo")
  }
}

// sayをオーバライドする義務はない
class FooImpl: Foo

ここでFooインターフェースの具象メソッドであるsayと同じ名前(と引数型)を具象メソッドとして持つほかのインターフェースBarがあったとき、FooBar両方を実装するクラスのsayメソッドは、デフォルトでどちらの実装を使用するのでしょうかリスト18⁠。

リスト18 同名の具象メソッドを持ったインターフェース
trait Bar {
  fun say() {
    println("bar")
  }
}

その答えは、デフォルトではどちらの実装も使用しません。正確に言うとFooBarの実装クラスは、sayメソッドのオーバライドの義務が発生しますリスト19⁠。

リスト19 FooとBarの実装クラス
// これはコンパイルエラーとなる
class FooBar1: Foo, Bar

// sayをオーバライドしているのでOK
class FooBar2: Foo, Bar {
  override fun say() {
    println("foobar")
  }
}

インターフェースのデフォルト実装を使用したい場合は、オーバライドしたメソッド内で明示的に呼び出しますリスト20⁠。このようにしてKotlinでは具象メソッドの衝突の問題を解決しています。

リスト20 使用する実装を明示
class FooBar: Foo, Bar {
  override fun say() {
    // Fooのsay実装を使用する
  super<Foo>.say()
  }
}

委譲

既存のクラスに機能を追加したい状況に直面したことのある人は多いはずです。

たとえばStringクラスに自身が表現する文字列を対象に挨拶するようなhelloというメソッドを追加したいとしましょう。

アプローチの1つとして委譲による実現を採用します。

リスト21GreetableCharSeqのようなクラスを定義すれば、helloメソッドが追加されたCharSequence[3]を得ることができます。

リスト21 普通に委譲パターンをコーディングする
class GreetableCharSeq(val cs: CharSequence): CharSequence {
  // 追加したいメソッド
  fun hello() {
    println("Hello, $cs")
  }

  // 実装を委譲する
  override fun charAt(index: Int): Char = cs.charAt(index)
  override fun length(): Int = cs.length()

  // その他多数のメソッド...
}

リスト22は、CharSequenceの実装クラスとしてStringクラスのインスタンスである"World"をコンストラクタに渡してGreetableCharSeqをインスタンス化しています。実行すると期待どおりHello, Worldと出力されます。

リスト22 GreetableCharSeqの使い方
val greetable = GreetableCharSeq("World")
greetable.hello() // => Hello, World

helloメソッドの追加に成功しました! しかし1つ問題があります。それはリスト21の「実装を委譲する」⁠その他多数のメソッド...」のコメントのとおり、今回はとくに関心のないメソッドに関してもオーバライドして、委譲して、という一連の苦痛な作業が強いられることです。

安心してください! Kotlinではこの問題の解決策を言語機能として提供しています。リスト21を書き直したリスト23をご覧ください。

リスト23 Kotlinの機能ですっきり
class GreetableCharSeq(val cs: CharSequence): CharSequence by cs {
  // 追加したいメソッド
  fun hello() {
    println("Hello, $cs")
  }
  // 以上
}

: CharSequenceの部分が: CharSequence bycsに変わりました。by csは、コンストラクタ引数として受け取ったCharSequence型のcsに委譲する、という意味になります。この記述のおかげで、独自にオーバライドする必要のないメソッドをコーディングせずに済みます。

この機能の別の例とちょっとした解説を筆者のブログに書いていますのでご参照ください。

拡張関数

前節のようなhelloメソッドを追加するためだけに新しいクラスを定義するのは大変ですし、インスタンス化のオーバヘッドも気になります。メソッドを既存クラスに追加するという観点では少し異なりますが、リスト24のようなアプローチのほうがよさそうです。

リスト24 ユーティリティメソッド
fun hello(s: String) {
  println("Hello, $s")
}

hello("World") // => Hello, World

実は、まさにこのように展開され、かつ「既存クラスにメソッド追加」のように見える機能がKotlinには用意されています。拡張関数(Extension Function)と呼ばれる言語機能です。

リスト25を見ると、本当にhelloメソッドがStringに追加されたかのように見えます。実際にはstatic void hello(String s)に相当するコードをコンパイラは生成します。重要なのは動的に生成されるのではなく、静的に解決されるということです。型安全、低コストを期待できます。

リスト25 拡張関数の例
fun String.hello() {
  println("Hello, $this")
}

"World".hello() // => Hello, World

演算子オーバロード

Kotlinには演算子オーバロードと呼ばれるしくみがあります。既存の演算子をほかの型に対しても使えるように拡張する機能です。たとえば独自にRationalというクラスを定義したとします。この有理数を表現するクラスのインスタンス同士、加算や乗算などできると便利です。この時にメソッド呼び出しによる記法ではなく、演算子を用いてa + bのように記述できるようにしてくれるのが演算子オーバロードです。

演算子は、特定のシグネチャを持ったメソッドと対応しています。Rational同士に対して使える+演算子は、Rationalfun plus(rational: Rational): Rationalというようなシグネチャのメソッドを定義することで使えるようになります。

具体例を示すにあたって話を簡単にするためにリスト26のようなMyIntクラスを定義します。Int値を1つだけ持っている単純なクラスです。

リスト26 plusメソッドを持ったMyInt
class MyInt(val value: Int) {
  fun plus(myInt: MyInt): MyInt =
    MyInt(value + myInt.value)
}

plusメソッドにより、自身と他のMyIntと加算ができます。そして、このplusメソッドにより+演算子が使えるようになりますリスト27⁠。

リスト27 +演算子が使える
// 普通のメソッド呼び出しによる記法
val sum = MyInt(3).plus(MyInt(5))
println(sum.value) // => 8

// 演算子を使った記法
val sum2 = MyInt(2) + MyInt(7)
println(sum2.value) // => 9

リスト28にもう1つ面白い例を示しましょう。演算子に対応するメソッドは、拡張関数として定義しても有効です。リスト28では拡張関数としてStringBuilderplusAssignメソッドを追加しています。このシグネチャは+=演算子に対応しますリスト29⁠。そのほかの演算子と、そのメソッドシグネチャについては公式ドキュメントに掲載されている対応表を参照してください。

リスト28 +=演算子相当の拡張関数
fun StringBuilder.plusAssign(any: Any) {
  append(any)
}
リスト29 +=演算子が使える
val sb = StringBuilder()
sb += "I"
sb += " am"
sb += " Taro"
println(sb) // => I am Taro

まとめ

今回はKotlinにおけるクラスとその周辺機能について紹介しました。

クラスやメソッドの定義、インスタンスの生成などはJavaのそれらと似ていますが、クラスはフィールドとアクセサが一緒になったようなプロパティを持てるという点でJava より強力です。

インターフェースは実装を持てる点で抽象クラスに似ています。同名の具象メソッドの衝突問題を回避するためのルールが用意されています。

既存の型の機能強化をしたい場合には委譲や拡張関数などの言語機能を使用すると便利です。

特定のシグネチャを持ったメソッドを定義することで演算子が使えるようになる演算子オーバロードを紹介しました。演算子を使用することで可読性の向上を期待できます。

次回はKotlinのユニークな機能であるNULL安全についてじっくり解説します。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧