この記事は ウェブクルー Advent Calendar 2025 の9日目の記事です。
昨日は @wc-terashima さんの「「紙の山」が教えてくれた、検索できることの価値」でした。
コンパニオンオブジェクトとは
「同じソースファイル内に、クラスと同じ名前で定義されるオブジェクト」のこと
例えば以下のように、クラスが定義されていて、同じ名前のオブジェクトのことを指します。
例えば下記のようなもの
class MyClass(val x: Int) {
def printX(): Unit = println(s"x = $x")
}
object MyClass {
def apply(x: Int): MyClass = new MyClass(x)
}
上記コードではMyClassクラスに対して同名のMyClassオブジェクトを用意している。
このオブジェクトをコンパニオンオブジェクトと呼ぶ
・・・と、ここまでは私も知ってはいたものの、見よう見まねで書いていて何のために同名のオブジェクトを用意するのかわからなかったので調べてみました。
コンパニオンオブジェクトの特徴(通常のオブジェクトではできないこと)
- クラスのprivateな要素にアクセス可能(逆にクラスからprivateなオブジェクトの要素も可能)
上記の特徴によってクラスは秘匿し、コンパニオンオブジェクトでのみ操作を行うといったことが可能になる。
構成例:
クラス
- 値の定義(private)
コンパニオンオブジェクト
- クラスを操作するメソッドの定義(ここからクラスのprivateの要素にアクセス可能)
- ファクトリメソッド(applyメソッド)
- クラスに関するユーティリティ関数の定義
privateな要素にアクセス可能かどうかを試すためのコードを書いてみた。
// フィールドもメソッドもprivate
class MyClass(private val x: Int) {
private def printX(): Unit = println(s"x = $x")
}
// オブジェクトと異なるクラス名
class MyClass2(private val x: Int) {
private def printX(): Unit = println(s"x = $x")
}
object MyClass {
def apply(x: Int): MyClass = new MyClass(x)
// クラスのprivateメソッドにアクセスするメソッド
def callPrintX(instance: MyClass): Unit = instance.printX()
// クラスのprivateフィールドにアクセスするメソッド
def callPrintX2(instance: MyClass): Unit = println(instance.x)
// オブジェクト名と異なるクラス名のファンクションにアクセス→エラー
//def callPrintX3(instance: MyClass2): Unit = instance.printX()
// オブジェクト名と異なるクラス名のフィールドにアクセス→エラー
//def callPrintX4(instance: MyClass2): Unit = println(instance.x)
}
object Main extends App {
val obj = MyClass(42)
MyClass.callPrintX(obj) // OK:「x = 42」 ← privateメソッド呼び出し成功
MyClass.callPrintX2(obj) // OK:「42」 ← privateフィールド呼び出し成功
val obj2 = new MyClass(421)
MyClass.callPrintX(obj2) // OK
MyClass.callPrintX2(obj2) // OK
//val obj3 = new MyClass2(32)
//MyClass.callPrintX3(obj3)
}
上記を実行すると確かにコンパニオンオブジェクトからはクラスのprivate要素にアクセス可能で、コンパニオンオブジェクトではないとアクセスできないことが分かった。
クラスとオブジェクトが別名でprivateな要素にアクセスしようとすると
method printX cannot be accessed as a member of (instance : MyClass2) from object MyClass.
private method printX can only be accessed from class MyClass2.
というようなエラーになる。
コンパニオンオブジェクトの活用
コンパニオンオブジェクトはprivateな要素にアクセス可能という特徴をもちつつ、いろいろな活用方法があるようなのでまとめてみた。
主要な使い方は下記の模様。
- クラスの中身をprivateで秘匿して、クラスのインスタンス生成が可能になる
- applyメソッドを使ったファクトリメソッドを定義することでnew不要でインスタンス生成可能(主にこの使い方で使われる)
それ以外にも下記のようなこともできる。
- 複数の生成パターンを作成できる
- クラスへの参照や取得の際に処理を挟んだアクセスが可能(入力チェックや変換処理など)
- クラスの共通処理を定義できる
コード例
文字だけではイメージしづらいためコンパニオンオブジェクトの利点を活用したコードを書いてみた。
// クラスの中身はprivateコンストラクタで隠し、内部ロジックもprivateに
class Person private(val name: String, val age: Int) {
// 外部からは見えない詳細情報を返すprivateメソッド
private def info(): String = s"Name: $name, Age: $age"
// 外部に公開する表示用メソッド
def showInfo(): Unit = println(info())
}
object Person {
// applyメソッドでnewすることなくインスタンス生成
// また、apply実施時に特定の処理を挟むことができる。それにより、入力チェックや整形を行うことも可能
// (ケースクラスを使うのであれば省略可能だが、定義することで独自に実装ができる)
def apply(name: String, age: Int): Person = {
// バリデーション
require(name.nonEmpty, "名前は空であってはならない")
require(age >= 0, "年齢は0以上でなければならない")
// 値の整形(例:名前の前後の空白除去と先頭大文字化)
val cleanedName = name.trim.capitalize
// インスタンス生成
new Person(cleanedName, age)
}
// 複数のインスタンス生成パターンを提供可能
// ここでは名前だけ指定してインスタンス作成可能なメソッドを用意
// 誕生日を渡して年齢を計算させる処理を持たせるなども可能
def withDefaultAge(name: String): Person = apply(name, 20)
}
// 複数のインスタンスの作成方法を提供できる
val p1 = Person("alice ", 30) // apply経由で入力チェック&整形して生成(newが不要で関数呼び出しのように書ける)
val p2 = Person.withDefaultAge("bob") // 別生成メソッドでデフォルト値を設定して生成
p1.showInfo() // 出力: Name: Alice, Age: 30
p2.showInfo() // 出力: Name: bob, Age: 20
まとめ
- クラスのインスタンス生成時に主に活用され、applyメソッドを定義することで、new無しでインスタンス生成が可能(ファクトリメソッドの定義が可能)
- クラスとコンパニオンオブジェクト間で相互にprivateな要素にもアクセスできるというのが固有の特徴
知識は得られたので後は実戦で活用してみようと思います。
明日は@wc-shibata さんの投稿になります。よろしくお願いします。