Help us understand the problem. What is going on with this article?

Kotlin文法 - ネストされたクラス、Enumクラス、オブジェクト、委譲、委譲されたプロパティ

More than 3 years have passed since last update.

はじめに

Kotlin文法 - データクラス, ジェネリクスの続き。

Kotlin ReferenceのClasses and Objects章Nested Classes, Enum Classes, Objects, Delegation, Delegated Propertiesの大雑把日本語訳。かなり説明を端折ったり少し補足したりしている。

Classes and Objects章はこれでおしまい。

ネストされたクラス

クラスはネストできる。

class Outer {
  private val bar: Int = 1
  class Nested {
    fun foo() = 2
  }
}

val demo = Outer.Nested().foo() // == 2

内部クラス

外側クラスのメンバにアクセスできるように、クラスの前に inner って付けられる。

class Outer {
  private val bar: Int = 1
  inner class Inner {
    fun foo() = bar // 外側のprivateなメンバにアクセスできる
  }
}

val demo = Outer().Inner().foo() // == 1

Enumクラス

Enumクラスの最も一般的な使い方は型安全な列挙。

enum class Direction {
  NORTH, SOUTH, WEST, EAST
}

初期化

各Enum定数はEnumクラスのインスタンスなので初期化できる。

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

無名クラス

各Enum定数はそれ自身の無名クラスを宣言することもできる。

enum class ProtocolState {
  // WAITINGはProtoclStateを継承してsignal()をオーバーライドした無名クラスのインスタンス
  WAITING {
    override fun signal() = TALKING
  },

  TALKING {
    override fun signal() = WAITING
  };
  // ProtoclStateクラスのメンバがこの後に続くのでその前をセミコロンで区切る。
  // これはJavaのenumと一緒だね。

  abstract fun signal(): ProtocolState
}

Enum定数とあそぼ

Javaと同様に valueOf()values() メソッドが用意されている。

// 名前を指定して定数を取得する
EnumClass.valueOf(value: String): EnumClass
// 全ての定数の配列を取得する
EnumClass.values(): Array<EnumClass>

valueOf() で指定された名前が定義されていなかった場合、IllegalArgumentException例外が投げられる。

各Enum定数は名前と位置を取得するプロパティを持っている。

val name: String    // 定数名
val ordinal: Int    // 定数の宣言された順番

またEnum定数は Comparable インターフェースを実装している。比較順はEnumクラス内での定義順になる。

オブジェクト

あるクラスをちょっとだけ変更したクラスのオブジェクトを、新しいサブクラスを宣言することなしに作りたいことがある。Javaはこれを無名内部クラスで扱う。Kotlinはこの概念をオブジェクト式オブジェクト宣言を使ってもうちょっと一般化している。

オブジェクト式

ある型を継承した無名クラスのオブジェクトを作るには、こう書く。

// MouseAdapterを継承した無名クラスのオブジェクトを作ってaddMouseListenerに渡す
window.addMouseListener(object : MouseAdapter() {
  override fun mouseClicked(e: MouseEvent) {
    // ...
  }

  override fun mouseEntered(e: MouseEvent) {
    // ...
  }
})

もし親クラスがコンストラクタを持つなら、適切なパラメータを渡さなければならない。インターフェースを複数実装する場合、クラス宣言と同様にコンマで区切って指定する。

open class A(x: Int) {
  public open val y: Int = x
}

interface B {...}

// Aクラスを継承してBインターフェースを実装する無名クラスのオブジェクトを作る
val ab = object : A(1), B {
  override val y = 15
}

なんも継承を指定しないオブジェクトも作れるよ。

val adHoc = object {
  var x: Int = 0
  var y: Int = 0
}
print(adHoc.x + adHoc.y)

Javaの無名内部クラスのように外側にアクセスできる(Javaだとfinalが付いてないとダメだけど、KotlinではvalでもvarでもOK)。

fun countClicks(window: JComponent) {
  var clickCount = 0
  var enterCount = 0

  // 無名クラスのオブジェクトを作って渡す。
  window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
      clickCount++    // この無名クラスの外側にあるclickCountにアクセスできる。
                      // しかもvalじゃなくてvarであっても。
    }

    override fun mouseEntered(e: MouseEvent) {
      enterCount++
    }
  })
  // ...
}

オブジェクト宣言

Kotlinではシングルトンをとても簡単に宣言できる。

object DataProviderManager {
  fun registerDataProvider(provider: DataProvider) {
    // ...
  }

  val allDataProviders: Collection<DataProvider>
    get() = // ...
}

これは式ではなく宣言。何かの変数に入れて使ったりするものじゃない。使うときはこんな風に名前でアクセスできる。

// シングルトン取得用のメソッドを呼ばなくてもいいので楽チン。
DataProviderManager.registerDataProvider(dataProvider)

もちろん親を持てる。

object DefaultListener : MouseAdapter() {
  override fun mouseClicked(e: MouseEvent) {
    // ...
  }

  override fun mouseEntered(e: MouseEvent) {
    // ...
  }
}

NOTE: オブジェクト宣言はローカルではできない(つまり関数内ではできない)。でも他のオブジェクトや inner でないクラス内では宣言できる。

コンパニオンオブジェクト

クラス内で宣言するオブジェクトには companion キーワードが付けられる。

class MyClass {
  companion object Factory {
    fun create(): MyClass = MyClass()
  }
}

コンパニオンオブジェクトのメンバーはオブジェクト名を指定しないで呼びだせる。

// MyClass.Factory.create()とは書かない。Factoryは書かない。
val instance = MyClass.create()

なのでオブジェクト名は省略可能。オブジェクトそのものを取得したいときは Companion を使う。

class MyClass {
  // オブジェクト名は別になくてもいいよ
  companion object {
  }
}

// オブジェクトそのものを取得したい場合はCompanionを指定する。
val x = MyClass.Companion

コンパニオンオブジェクトのメンバは他の言語のstaticメンバのように見えるけど、実行時においても実際のオブジェクトのインスタンスメンバである。例えばインターフェースを実装できる。

interface Factory<T> {
  fun create(): T
}


class MyClass {
  // Facotry<MyClass>を実装した無名クラスの無名コンパニオンオブジェクト
  companion object : Factory<MyClass> {
    override fun create(): MyClass = MyClass()
  }
}

けどJVM環境では、@JvmStatic アノテーションを付けることで、本物のstaticメソッドやフィールドとしてコンパニオンオブジェクトのメンバを生成することができる。詳細はJava interoperabilityの章を参照。

オブジェクト式とオブジェクト宣言の意味論上の違い

オブジェクト式とオブジェクト宣言には重要な意味論(semantic)上の違いがある。

  • オブジェクト宣言は初めて利用されるときに遅延的に(lazy)初期化される。
  • オブジェクト式はそれが使われている場所で即座に(immediately)に実行(そして初期化)される。

委譲(Delegation)

クラス委譲

委譲パターンは継承による実装の良い代替となることが証明されてきた1。Kotlinは一切の決まり切ったコードを要求することなく、ネイティブにこれをサポートする。

interface Base {
  fun print()
}

class BaseImpl(val x: Int) : Base {
  override fun print() { print(x) }
}

// こいつはBaseインターフェースの全てのpublicメソッドをbに委譲する。
class Derived(b: Base) : Base by b

fun main() {
  val b = BaseImpl(10)
  Derived(b).print() // Derivedはbに委譲するので 10 を表示する
}

by 節は bDerived オブジェクトの中に格納されて、コンパイラが Base の全てのメソッドを b に委譲することを示す。

プロパティ委譲(Delegated Properties)

ある種のありがちな動作をするプロパティの実装をライブラリ化して、コードを共通化できたらとても素敵だと思わない?例えばこんなの。

  • 遅延プロパティ: 最初のアクセスで初めて計算される値
  • 観測可能(observable)プロパティ: 値に変化があったらリスナに通知される
  • マップに格納されるプロパティ: マップ内の各値を個別のフィールドにするんじゃなく

こういう場合のためにKotlinはプロパティ委譲をサポートしている。

class Example {
  var p: String by Delegate()
}

文法はこう:val/var <property name>: <Type> by <expression>

このプロパティのgetter/setterは by の後に指定したオブジェクトに委譲される。ここで指定されるオブジェクトは getValue() と setValue() メソッドだけ持ってればいい。

class Delegate {
  operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    return "$thisRef, thank you for delegating '${property.name}' to me!"
  }

  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    println("$value has been assigned to '${property.name} in $thisRef.'")
  }
}

Exampleインスタンスの p を読み出すと、Delegateクラスの getValue() が呼ばれる。最初の引数は p を読み出そうとしているExampleインスタンスで、2番目の引数は p そのものに関する情報(ここからプロパティ名を取り出せる)。

val e = Example()
println(e.p)

は以下を表示する。

Example@33a17727, thank you for delegating ‘p’ to me!

代入時には setValue() が呼ばれ、最初の2つの引数は getValue() と同じ。3つ目の引数は代入する値。

e.p = "NEW"

は以下を表示する。

NEW has been assigned to ‘p’ in Example@33a17727.

プロパティ委譲の要件

プロパティ委譲のまとめをここで行う。

読み出しのみの(つまり val )プロパティの場合、次のパラメータを取る getValue() という名前のメソッドを提供する必要がある。

  • receiver - プロパティ保持者と同じかそのサブ型でなければならない2
  • metadata - KProperty<*>かそのサブ型でなければならない。

また戻り値はプロパティと同じ型かそのサブ型でなければならない。

変更可能(mutable)なプロパティ(つまり var)の場合は、それに加えて次のパラメータを取る setValue() という名前のメソッドを提供する必要がある。

  • receiver - getValue()と同じ
  • metadata - getValue()と同じ
  • new value - プロパティと同じかそのサブ型でなければならない

getValue()setValue() は、委譲クラスのメソッドか拡張メソッドとして提供される。後者はこれらのメソッドを提供していないオブジェクトにプロパティ委譲したい時に便利。どちらのメソッドも operator キーワードでマークされている必要がある。

標準ライブラリで用意されている委譲

Kotlinの標準ライブラリは幾つかの種類の便利な委譲を生成するファクトリメソッドを用意している。

遅延(Lazy)

lazy() はラムダを受け取る関数で Lazy<T> のインスタンスを返す。 Lazy<T> は遅延プロパティを実装する委譲を提供する。その委譲は、初めて get() が呼ばれるときに渡されたブロックを実行し、その結果を覚えておく。その後は get() が呼ばれるたびに覚えておいた値を返す。

val lazyValue: String by lazy {
    // 初めてlazyValueを取得するときにこのブロックが実行される
    println("computed!")
    "Hello"    // これを結果として覚えておき、以後はずっとこれを返す
}

fun main(args: Array<String>) {
    // 最初のアクセスなので"computed!"が表示された後、"Hello"が表示される
    println(lazyValue)
    // その後は"Hello"しか表示されない
    println(lazyValue)
}

観測可能(Observable)

Delegates.observable() は初期値と変更を処理するラムダの2つの引数を取る。渡されたラムダはプロパティへの代入のたびに(代入が終わった後で)呼び出される。このラムダにはプロパティ情報と前の値、新しい値の3つが引数として渡される。3

import kotlin.properties.Delegates

class User {
    // 初期値は"<no name>"
    var name: String by Delegates.observable("<no name>") { prop, old, new ->
        // 変更のたびにこのブロックが実行される。
        // propにはプロパティ情報, oldには前の値, newには新しい値が渡される
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"    // "<no name> -> first"と表示される
    user.name = "second"   // "first -> second"と表示される
}

obserbale() の代わりに vetoable() を使うと、プロパティへの代入前に渡したラムダが実行される。ラムダの戻り値は代入を行うかどうかを返す。これを使うと代入を拒否することができる。

マップに格納するプロパティ

JSONパースしたりするときに、プロパティの値をマップに入れておくことってあるよね。そういう場合にはマップのインスタンスそのものを委譲先に指定できる。利用するにはマップに getValue() を追加する拡張をインポートする。この getValue() はプロパティ名をキーにしてマップから値を返す。

import kotlin.properties.getValue // マップにgetValue()メソッドを拡張する

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

例えばコンストラクタにこんなマップを渡すと・・・

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

println(user.name) // "John Doe"と表示される
println(user.age)  // "25"と表示される

これは var プロパティでも動作する。Map の代わりに MutableMap を利用する。あと setValue() を追加する拡張もインポートしてね。

import kotlin.properties.getValue // マップにgetValue()メソッドを拡張する
import kotlin.properties.setValue // マップにsetValue()メソッドを拡張する

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

次の章へ

次はKotlin文法 - 関数とラムダへGO!


  1. よく言われる「継承よりコンポジションを使え」というやつ。 

  2. プロパティを拡張される場合があるのでサブ型でもオッケーにしておく 

  3. 正直期待してたのと違うなぁという印象。この例じゃ初期値与えてsetter定義するのに比べて何が便利なのかわからん。コンストラクタにDelegates.observableインスタンスを渡すとかするとプロパティの変更を外から監視できる仕組みにできるけど、監視者が1人だけじゃObserverパターンって言わないよね。別にRx使えばいんだけどさ。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした