前回はKotlinの基本文法と関数について解説しました。今回はKotlinにおけるオブジェクト指向プログラミングを実現するクラスと、
クラスの定義とインスタンスの生成
KotlinはJavaと同じくクラスベースのオブジェクト指向言語です。すなわち、
メンバをいっさい持たないクラスの定義例をリスト1に示します。Javaと同様class
キーワードが必要です。
class MyClass
このMyClass
クラスをインスタンス化してみましょう
val obj = MyClass()
println(obj) // => MyClass@604e9f7f
MyClass()
によりMyClass
クラスのインスタンスが生成されます。new
のようなキーワードは必要ありませんnew
はキーワードではありません)。リスト2ではMyClass
クラスのインスタンスを生成して、obj
に代入しています。それをprintln
するとクラス名とハッシュコードが出力されます。
メソッド
メソッドを持ったクラスを定義しましょう。リスト3のGreeter
クラスはgreet
メソッドを持っています。このようにクラスはメンバを波括弧{}
)fun
キーワードを使って関数のように記述します。
class Greeter {
fun greet() {
println("Hello!")
}
}
リスト4のようにGreeter
クラスをインスタンス化して、greet
メソッドを呼び出します。
val greeter = Greeter()
greeter.greet() // => Hello!
プロパティ
クラスはプロパティを持つことができます。プロパティとは、
リスト5のUser
クラスはid
とname
という名前のプロパティを持っています。インスタン化してプロパティに値を設定したり取得したりしてみましょう。
var id: Long = 0
var name: String = ""
}
リスト6の2行目ではname
プロパティに値を設定しています。そして3行目でname
プロパティとid
プロパティの値を取得しています。Javaにおけるフィールドに直接アクセスしているように見えますが、
val taro = User()
taro.name = "Taro"
println("${taro.name}(${taro.id})") // = > Taro(0)
setterやgetterをカスタマイズしたい場合はリスト7のように、
class User {
var id: Long = 0
var name: String = ""
// 値が設定されるときにログを出力する
set(value) {
println("set: $value")
$name = value
}
// 値が取得されるときにログを出力する
get() {
println("get")
return $name
}
}
コンストラクタ引数
クラス名の後に続けてコンストラクタの引数リストを宣言できます。ここで受け取った引数でもってプロパティを初期化することができます。
リスト8はもっとシンプルになります。
class User(id: Long, name: String) {
val id = id
val name = name
}
リスト9のようにコンストラクタ引数の名前の前にval
やvar
を付けることでプロパティの定義も兼ねることができ、
class User(val id: Long, val name: String)
継承
Javaやそのほかの言語にもあるように、
このことをシンプルな例から確認しましょう。まずスーパクラスとなるFoo
クラスを定義します
open class Foo {
fun foo() {
println("Foo")
}
}
Foo
クラスは、foo
メソッドを持ったクラスです。さらに注目すべきポイントはopen
という修飾子が付いていることです。Kotlinでは、open
修飾子を付けて明示する必要があります。逆を言えばopen
が付いていないクラスは継承できません
リスト11にFoo
を継承したBar
クラスを定義します。
class Bar: Foo() {
fun bar() {
println("Bar")
}
}
クラス名の後に続けてコロン:
)、Foo
クラスはコンストラクタ引数がないのでFoo()
となっています。
ではBar
のインスタンスからスーパクラスで定義したメソッドを呼び出せるか確かめてみましょうBar
は、Foo
のサブ型なのでval foo: Foo = bar
は有効なコードです。
val bar: Bar = Bar()
bar.foo() // => Foo
bar.bar() // => Bar
抽象クラス
インスタンス化できない抽象クラスというものを定義できます。abstract
修飾子をクラスに付けるだけです。抽象クラスは抽象メンバを持つことができ、
リスト13のGreeter
抽象クラスは抽象プロパティname
と抽象メソッドgreet
を持っています。これらの抽象メンバはシグネチャだけで実装を持たないことがわかります。Greeter
の具象サブクラスをリスト14に定義します。ここでは各抽象メンバをoverride
修飾子付きでオーバライドしています。Kotlinではオーバライドする際にはoverride
修飾子が必須です。ちなみに、abstract
なクラスはopen
なしで継承可能なクラスとなります。
abstract class Greeter {
abstract val name: String
abstract fun greet()
}
class JapaneseGreeter(
override val name: String
): Greeter() {
override fun greet() {
println("こんにちは,私は${name}です")
}
}
インターフェース
Javaのようにインターフェースを定義することもできます。インターフェースは実装を持つことができますが、
Kotlinでインターフェースを定義するにはtrait
キーワード
trait Greeter {
val name: String
fun greet()
}
インターフェースでは抽象メンバにabstract
を付ける必要はありません。インターフェースを実装するにはクラス名の後に続けてコロンとインターフェース名を記述するだけです。クラスの継承とは異なり、Greeter
かGreeter()
かだけです。
class JapaneseGreeter(
override val name: String
): Greeter {
override fun greet() {
println("こんにちは,私は${name}です")
}
}
ところで、
trait Foo {
fun say() {
println("foo")
}
}
// sayをオーバライドする義務はない
class FooImpl: Foo
ここでFoo
インターフェースの具象メソッドであるsay
と同じ名前Bar
があったとき、Foo
とBar
両方を実装するクラスのsay
メソッドは、
trait Bar {
fun say() {
println("bar")
}
}
その答えは、Foo
とBar
の実装クラスは、say
メソッドのオーバライドの義務が発生します
// これはコンパイルエラーとなる
class FooBar1: Foo, Bar
// sayをオーバライドしているのでOK
class FooBar2: Foo, Bar {
override fun say() {
println("foobar")
}
}
インターフェースのデフォルト実装を使用したい場合は、
class FooBar: Foo, Bar {
override fun say() {
// Fooのsay実装を使用する
super<Foo>.say()
}
}
委譲
既存のクラスに機能を追加したい状況に直面したことのある人は多いはずです。
たとえばString
クラスに自身が表現する文字列を対象に挨拶するようなhello
というメソッドを追加したいとしましょう。
アプローチの1つとして委譲による実現を採用します。
リスト21のGreetableCharSeq
のようなクラスを定義すれば、hello
メソッドが追加されたCharSequence
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と出力されます。
val greetable = GreetableCharSeq("World")
greetable.hello() // => Hello, World
hello
メソッドの追加に成功しました! しかし1つ問題があります。それはリスト21の
安心してください! Kotlinではこの問題の解決策を言語機能として提供しています。リスト21を書き直したリスト23をご覧ください。
class GreetableCharSeq(val cs: CharSequence): CharSequence by cs {
// 追加したいメソッド
fun hello() {
println("Hello, $cs")
}
// 以上
}
: CharSequence
の部分が: CharSequence bycs
に変わりました。by cs
は、CharSequence
型のcs
に委譲する、
この機能の別の例とちょっとした解説を筆者のブログに書いていますのでご参照ください。
拡張関数
前節のようなhello
メソッドを追加するためだけに新しいクラスを定義するのは大変ですし、
fun hello(s: String) {
println("Hello, $s")
}
hello("World") // => Hello, World
実は、
リスト25を見ると、hello
メソッドがString
に追加されたかのように見えます。実際にはstatic void hello(String s)
に相当するコードをコンパイラは生成します。重要なのは動的に生成されるのではなく、
fun String.hello() {
println("Hello, $this")
}
"World".hello() // => Hello, World
演算子オーバロード
Kotlinには演算子オーバロードと呼ばれるしくみがあります。既存の演算子をほかの型に対しても使えるように拡張する機能です。たとえば独自にRational
というクラスを定義したとします。この有理数を表現するクラスのインスタンス同士、a + b
のように記述できるようにしてくれるのが演算子オーバロードです。
演算子は、Rational
同士に対して使える+
演算子は、Rational
のfun plus(rational: Rational): Rational
というようなシグネチャのメソッドを定義することで使えるようになります。
具体例を示すにあたって話を簡単にするためにリスト26のようなMyInt
クラスを定義します。Int値を1つだけ持っている単純なクラスです。
class MyInt(val value: Int) {
fun plus(myInt: MyInt): MyInt =
MyInt(value + myInt.value)
}
plus
メソッドにより、MyInt
と加算ができます。そして、plus
メソッドにより+
演算子が使えるようになります
// 普通のメソッド呼び出しによる記法
val sum = MyInt(3).plus(MyInt(5))
println(sum.value) // => 8
// 演算子を使った記法
val sum2 = MyInt(2) + MyInt(7)
println(sum2.value) // => 9
リスト28にもう1つ面白い例を示しましょう。演算子に対応するメソッドは、StringBuilder
にplusAssign
メソッドを追加しています。このシグネチャは+=演算子に対応します
fun StringBuilder.plusAssign(any: Any) {
append(any)
}
val sb = StringBuilder()
sb += "I"
sb += " am"
sb += " Taro"
println(sb) // => I am Taro
まとめ
今回はKotlinにおけるクラスとその周辺機能について紹介しました。
クラスやメソッドの定義、
インターフェースは実装を持てる点で抽象クラスに似ています。同名の具象メソッドの衝突問題を回避するためのルールが用意されています。
既存の型の機能強化をしたい場合には委譲や拡張関数などの言語機能を使用すると便利です。
特定のシグネチャを持ったメソッドを定義することで演算子が使えるようになる演算子オーバロードを紹介しました。演算子を使用することで可読性の向上を期待できます。
次回はKotlinのユニークな機能であるNULL安全についてじっくり解説します。
本誌最新号をチェック!
Software Design 2022年9月号
2022年8月18日発売
B5判/
定価1,342円
- 第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識 - 第2特集
「知りたい」 「使いたい」 「発信したい」 をかなえる
OSSソースコードリーディングのススメ - 特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画] Red Hat Enterprise Linux 9最新ガイド - 短期連載
今さら聞けないSSH
[前編] リモートログインとコマンドの実行 - 短期連載
MySQLで学ぶ文字コード
[最終回] 文字コードのハマりどころTips集 - 短期連載
新生「Ansible」 徹底解説
[4] Playbookの実行環境 (基礎編)