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

同じファイル内において、クラスと同じ名前で定義されたシングルトンオブジェクトをコンパニオンオブジェクトと呼ぶ。コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っていて、private なフィールドに対してもアクセスできる。

試してみよう。なお、REPL で試すには :paste コマンドを使って、ペーストモードになってからクラスとコンパニオンオブジェクトを一緒に定義する。こうしないと、REPL が正しく認識できないらしい。先のエントリーで警告が出てたのはこのせいだな。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Person(val name: String, var age: Int, private var weight: Int)

object Person {
    def printWeight(): Unit = {
        val andy = new Person("Andy", 27, 65)
        println(andy.weight)
    }
}

// Exiting paste mode, now interpreting.

defined class Person
defined object Person

ペーストモードを抜けるには Ctrl-D。

で、上のように定義したのがクラス Person とそのコンパニオンオブジェクト Person だ。コンパニオンオブジェクトの中から、プライベートは weight フィールドにアクセスしている。

じゃあ試してみよう。

scala> Person.printWeight
65

この通り、weight の値が出力された。

シングルトンオブジェクト

Scala では、クラスのほかに、object キーワードを使うことでシングルトンオブジェクトを作ることができる。シングルトンオブジェクトは、クラスとは違ってインスタンス化せずにそのまま使えるオブジェクトだ。

使い道としては次の2つがあげられる。

  • ユーティリティメソッドやグローバルな状態の置き場
  • 同名クラスのインスタンスのファクトリメソッド

まず、1つ目の使い道を見てみよう。シングルトンオブジェクトの定義構文はクラスの定義とほぼ同じで、class キーワードの代わりに object キーワードを使う。

scala> object Foo {
     |     def hello(): Unit = println("Hello, World!")
     | }
defined object Foo

この例では使っていないけど、クラスやトレイトを継承することもできる。そしてこの Foo オブジェクトはインスタンス化することなくそのまま使うことができる。

scala> Foo.hello
Hello, World!

もう1つの使い道、ファクトリメソッドについても見てみよう。同名のクラス Point をシングルトンオブジェクト Point を定義する。

scala> class Point(val x: Int, val y: Int) {
     |     override def toString(): String = "(" + x + ", " + y + ")"
     | }
defined class Point

scala> object Point {
     |     def apply(x: Int, y: Int): Point = new Point(x, y)
     | }
defined object Point
warning: previously defined class Point is not a companion to object Point.
Companions must be defined together; you may wish to use :paste mode for this.

警告が出ているけどとりあえずわきに置いておく。

シングルトンオブジェクトの apply というメソッドは Scala によって特別扱いされ、Point(x, y) という記述があったときに Point.apply(x, y) と解釈される。つまり、Point クラスのインスタンスを作るのに、new Point(2, 3) とする代わりにシングルトンオブジェクトを使って Point(2, 3) とすることができる。試してみよう。

scala> val p = Point(2, 3)
p: Point = (2, 3)

scala> p.toString
res1: String = (2, 3)

ちなみに toString というメソッドはどんなクラスにも定義されているらしい。上の Point クラスは、明示的には何も継承していないけど暗黙に何かのクラスを継承していて(たぶん)、そのクラスで toString メソッドが定義されているので、override キーワードを使って再定義している。

toString メソッドをオーバーライドすると、REPL のレスポンスに現れる値の表示も変わって (2, 3)  となる。これは REPL が値を表示するのに、暗黙に toString を呼び出しているからだね(たぶん)。