Edit Page

object式とobject宣言

時々、あるクラスをわずかに修正しただけのオブジェクトを、それのための新しいサブクラスを明示的に宣言せずに作成したい事があります。 Kotlinではobject式object宣言 でこの事態に対処します。

object式

object式(object expression) は無名クラス(anonymous class)のオブジェクトを作ります、 無名クラスとはこの場合、明示的にclassで宣言しないクラスの事です。 そのようなクラスは一度限りの使用の時に便利です。 そのようなクラスを様々な方法で定義する事が出来ます ー 何も無い所から、既存のクラスを継承して、インターフェースを実装する事で、など。 無名クラスのインスタンスは無名オブジェクト(anonymous object)とも呼ばれます。 なぜなら、これは名前により定義されるのでは無く式により定義されるからです。

無名オブジェクトを何も無い所から作る

object式はキーワードobjectで始めます。

もし自明でない基底型が必要で無いようなオブジェクトを単に欲しいだけなら、 objectのあとに中括弧でメンバを書けばよろしい:

fun main() { //sampleStart val helloWorld = object { val hello = "Hello" val world = "World" // object式はAny型を継承します。だから`toString()`には`override`が必要です override fun toString() = "$hello $world" } //sampleEnd print(helloWorld) }

基底型を継承して無名オブジェクトを作る

なんらかの型(ときには複数)を継承した無名クラスのオブジェクトを作るには、 その型をobjectの後ろにコロンを足して:、その後ろに書きます。 この型のメンバを実装したりオーバーライドするのは通常の継承のように行います。

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 { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

returnでの無名オブジェクトの使用とその型

無名オブジェクトがローカルの型か、privateかつinline定義で無い(関数またはプロパティ)ものに使われた時は、 この関数やプロパティを通してすべてのメンバにアクセス可能となります。

class C {
    private fun getObject() = object {
        val x: String = "x"
    }

    fun printX() {
        println(getObject().x)
    }
}

もしこの関数やプロパティがpublicかprivate inlineなら、その実際の型は以下のようになる:

  • 無名オブジェクトが宣言された基底型を持たないなら Any
  • 基底型が一つだけ宣言されているならその基底型
  • 複数の基底型が宣言されている場合には明示的に宣言した型

このすべてのケースで、無名オブジェクトのメンバはアクセス不可能です。 オーバーライドしたメンバはその実際の型がそのメンバの関数やプロパティを宣言しているならアクセス可能です:

interface A {
    fun funFromA() {}
}
interface B

class C {
    // 戻りの型は Any; xはアクセス出来ない
    fun getObject() = object {
        val x: String = "x"
    }

    // 戻りの型はA; xはアクセス出来ない
    fun getObjectA() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }

    // 戻りの型はB; funFromA()とxはアクセス出来ない
    fun getObjectB(): B = object: A, B { // 明示的なreturnの型の指定が必要
        override fun funFromA() {}
        val x: String = "x"
    }
}

無名オブジェクトからの変数へのアクセス

object式のコードは、内包するスコープの変数にアクセスすることができます。

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

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

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

object宣言

シングルトンパターンが有用な場合はしばしばあり、 Kotlinは、シングルトンを容易に宣言できます:

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

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

これはobject宣言(object declaration)と呼ばれ、 それは常に object キーワードの後に名前を持ちます。 ちょうど変数宣言と同じように、object宣言は式ではなく、代入文の右側に使用することはできません。

object宣言の初期化はスレッドセーフで、最初のアクセスの時に行われます。

オブジェクトを参照するために、その名前を直接使用します。

DataProviderManager.registerDataProvider(...)

このようなオブジェクトは、スーパータイプを持つことができます:

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

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

object宣言はローカルにすることはできません(つまり、関数内にネストする事は出来ません)。 ただし、他のobject宣言または内部クラスでないクラスにネストすることはできます。

データオブジェクト

Kotlinで通常のobject宣言をprintすると、 そのオブジェクトの文字列表現はその名前とハッシュ値を含んだものになります:

object MyObject

fun main() {
    println(MyObject) // MyObject@1f32e575
}

データクラスと同様に、 object宣言にもdata修飾子をつける事が出来ます。 これはコンパイラに、幾つかの関数を自動生成する事を指示します:

  • データオブジェクト(data object)の名前を返す toString()
  • equals()/hashCode()のペア

データオブジェクトには、カスタムのequalshashCodeを実装する事は出来ません

toString()関数はデータオブジェクトの名前を返します:

data object MyDataObject {
    val x: Int = 3
}

fun main() {
    println(MyDataObject) // MyDataObject
}

データオブジェクトequals()関数は、同じデータオブジェクトのクラスから作られる全オブジェクトがイコールである事を保証します。 多くのケースでは一つのデータオブジェクトのクラスから作られるインスタンスは一つだけです(結局、データオブジェクトもシングルトンを宣言するものですから)。 しかしながら、エッジケースでは実行時に同じクラスの別のオブジェクトが作られてしまうケースも存在します(例えばプラットフォームのリフレクション、java.lang.reflectを使ったり、JVMのシリアライゼーションライブラリで内部でリフレクションを使っているものなど)。 このケースでもすべてのオブジェクトがequalになる事を保証します。

データオブジェクトの比較はいつも構造的に行う(==演算子を使って)事。 リファレンス比較(===演算子を使って)は決して行わないようにしましょう。 そうすることで、一つ以上のデータオブジェクトが存在する場合があるという落とし穴にはまらないで済みます。

import java.lang.reflect.Constructor

data object MySingleton

fun main() {
    val evilTwin = createInstanceViaReflection()

    println(MySingleton) // MySingleton
    println(evilTwin) // MySingleton

    // ライブラリが強制的にMySingletonの2番目のインスタンスを作る場合でも、その`equals`メソッドはtrueを返します:
    println(MySingleton == evilTwin) // true

    // データオブジェクトの比較を === では行わないように
    println(MySingleton === evilTwin) // false
}

fun createInstanceViaReflection(): MySingleton {
    // Kotlinのリフレクションはデータオブジェクトのインスタンスを作る事は許可していません。
    // このコードは新規のMySingletonを無理やり作ります(Javaのプラットフォームリフレクションを用いて)
    // こんな事はしないように!
    return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}

生成されるhashCode()関数もequals()関数と一貫した振る舞いをします。 つまり、同じデータオブジェクトのクラスから生成されたインスタンスのハッシュコードは同じになります。

データオブジェクトとデータクラスの違い

データオブジェクトとデータクラスは一緒に用いられて多くの類似点がありますが、 データオブジェクトの方にだけ生成されない関数が幾つかあります:

  • copy()関数は生成されません。データオブジェクトはシングルトンのオブジェクトとして使う意図で使われるものですから、copy関数は生成されません。シングルトンパターンはインスタンスを一つに制限するものですが、インスタンスのコピーを許せばこの制約を違反してしまいます。
  • componentN()関数は生成されません。データクラスと違い、データオブジェクトにはデータプロパティがありません。データプロパティの無いオブジェクトの分割代入(destructure)は意味をなさないので、componentN()関数は生成されないのです。

selaedの継承階層でのデータオブジェクトの使用

データオブジェクト宣言はsealedなクラスとインターフェースで述べたようなsealedの継承階層で使うととても便利です。 なぜならこれを使えば、そのオブジェクトと同列に定義しているデータクラスと対称性を維持する事が出来るからです:

sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult

fun printReadResult(r: ReadResult) {
    when(r) {
        is Number -> println("Num(${r.number}")
        is Text -> println("Txt(${r.text}")
        is EndOfFile -> println("EOF")
    }
}

fun main() {
    printReadResult(EndOfFile) // EOF
}

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

(訳注: Companion Objects)

クラス内のobject宣言を、 companion キーワードでマークすることができます。

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

コンパニオンオブジェクトのメンバーは限定子(qualifier)として単にクラス名を使用して呼び出すことができます:

val instance = MyClass.create()

コンパニオンオブジェクトの名前は省略することができます。この場合、 Companion という名前が使用されます。

class MyClass {
    companion object { }
}

val x = MyClass.Companion

クラスのメンバは、そのコンパニオンオブジェクトのprivateなメンバにもアクセス出来ます。

クラスの名前を(他の名前の前に限定子として使うのでは無く)単体で使うと、 そのクラスのコンパニオンオブジェクトへの参照のように振る舞います(コンパニオンオブジェクトに名前があっても無くても):

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

class MyClass2 {
    companion object { }
}

val y = MyClass2

コンパニオンオブジェクトのメンバは、他の言語のスタティックメンバのように見えますが、 実行時にはそれらは実際のオブジェクトのインスタンスメンバである事には注意が必要で、たとえばインターフェイスを実装できたりします:

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

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

val f: Factory<MyClass> = MyClass

しかしながら、JVM上では、 @JvmStatic アノテーションを使用すると、コンパニオンオブジェクトのメンバを実際の静的メソッドやフィールドとして生成することもできます。 詳細については、Javaの相互運用性のセクションを参照してください。

object式とobject宣言の間の意味(semantics)の違い

object式とobject宣言の間には、ある重要な意味上の違いがあります:

  • object式は 使用された場所ですぐに (初期化されて)実行されます
  • object宣言は、初回アクセス時まで 遅延して 初期化されます
  • コンパニオンオブジェクトは、対応するクラスが読み込まれた(解決)されたときに初期化され、これは Java の静的初期化子のセマンティクスに一致します