Kotlinのcompanion objectについて

採用はこちら

Kotlinのcompanion objectは、クラスに関連づいた特別なオブジェクトです。

Javaのstaticに近い使い方ができますが、考え方は少し異なります。

Javaでは、staticを使うとクラスそのものに属するメンバーを定義できます。

一方、Kotlinではstaticキーワードの代わりに、companion objectを使ってクラスにひもづく共通処理や定数を表現します。

たとえば、次のように書けます。

class User {
    companion object {
        val defaultName = "Guest"

        fun createGuest(): User {
            return User()
        }
    }
}

この場合、defaultNamecreateGuest() は、インスタンスを作らなくてもクラス名から呼び出せます。

fun main() {
    println(User.defaultName)
    val guest = User.createGuest()
}

この書き方だけ見るとJavaのstaticとほとんど同じように見えます。

ただし、Kotlinではcompanion objectは単なる静的領域ではなく、実体を持つオブジェクトとして扱われます。

目次

Javaのstaticとの違い

companion objectを理解するうえで大切なのは、Javaのstaticと完全に同じものではないという点です。

Javaでは、staticメンバーはクラスそのものに属します。

class User {
    static String defaultName = "Guest";
}

一方、Kotlinでは次のように書きます。

class User {
    companion object {
        val defaultName = "Guest"
    }
}

見た目は似ていますが、Kotlinのcompanion objectは、クラスの中に定義された特別なシングルトンオブジェクトです。

そのため、Kotlinでは次のように考えるとわかりやすいです。

  • Javaのstatic
    クラスそのものに属するメンバー
  • Kotlinのcompanion object
    クラスに関連づいた特別なオブジェクト

この違いがあるので、Kotlinのcompanion objectインターフェースを実装できるなど、Javaのstaticにはない特徴を持っています。

companion objectの基本構文

名前なしのcompanion object

最も基本的な書き方は次の形です。

class Sample {
    companion object {
        fun hello() {
            println("Hello")
        }
    }
}

呼び出しは次のようになります。

Sample.hello()

名前付きのcompanion object

companion objectには名前を付けることもできます。

class Sample {
    companion object Factory {
        fun create(): Sample {
            return Sample()
        }
    }
}

この場合も、通常はクラス名からそのまま呼び出せます。

val s1 = Sample.create()

さらに、名前を使って明示的にアクセスすることもできます。

val s2 = Sample.Factory.create()

名前を省略した場合、内部的にはCompanionという名前になります。

companion objectの主な用途

companion objectは、主に次のような場面で使われます。

  • クラスに関連する定数をまとめたいとき
  • インスタンス生成用のファクトリメソッドを定義したいとき
  • クラス共通の処理をまとめたいとき
  • Javaから呼びやすいクラスメソッド風APIを作りたいとき
  • インターフェースを実装させたいとき

よくある使い方1: 定数の定義

companion objectは、クラスに関連する定数の置き場所としてよく使われます。

class ApiClient {
    companion object {
        const val BASE_URL = "https://example.com"
    }
}

使い方はシンプルです。

println(ApiClient.BASE_URL)

const valとvalの違い

ここは初心者がつまずきやすいポイントです。

const val

const valコンパイル時定数です。

文字列や数値のような、実行前に値が確定しているものに使えます。

class Config {
    companion object {
        const val TIMEOUT = 30
    }
}

val

valは読み取り専用プロパティですが、値が決まるのは実行時でも構いません。

class Config {
    companion object {
        val currentTime = System.currentTimeMillis()
    }
}

この例は実行時に値が決まるため、const valにはできません。

使い分けの目安

  • 完全な固定値なら const val
  • 実行時に決まる読み取り専用値なら val
  • 変更が必要なら var だが、共有状態になるので慎重に使う

よくある使い方2: ファクトリメソッド

companion objectは、インスタンス生成の窓口として非常によく使われます。

class User private constructor(val name: String) {
    companion object {
        fun createGuest(): User {
            return User("Guest")
        }

        fun createAdmin(): User {
            return User("Admin")
        }
    }
}

使い方は次の通りです。

val guest = User.createGuest()
val admin = User.createAdmin()

この書き方にはいくつか利点があります。

  • コンストラクタより意味のある名前を付けられる
  • 生成ルールを1か所にまとめられる
  • 不正な生成を防ぎやすい
  • private constructorと組み合わせて設計しやすい

よくある使い方3: of / from / parse パターン

実務では、意味がわかりやすい名前の生成メソッドを置くことがよくあります。

class Money private constructor(val amount: Int) {
    companion object {
        fun of(amount: Int): Money {
            require(amount >= 0) { "amount must be >= 0" }
            return Money(amount)
        }

        fun from(text: String): Money {
            return Money(text.toInt())
        }
    }
}
val m1 = Money.of(1000)
val m2 = Money.from("2000")

このような書き方にすると、どういう意図でインスタンスを作っているのかが明確になります。

companion objectはオブジェクトとして扱える

companion objectの大きな特徴は、単なる“静的な箱”ではなく、オブジェクトそのものだという点です。

class MyClass {
    companion object
}

この場合、MyClass.Companionという形でそのオブジェクトを参照できます。

val obj = MyClass.Companion
println(obj)

名前付きならさらにわかりやすくなります。

class MyClass {
    companion object Factory
}

val obj = MyClass.Factory

この性質があるため、companion objectはただのstatic代替ではなく、Kotlinらしい柔軟な使い方ができます。

インターフェースを実装できる

companion objectはオブジェクトなので、インターフェースを実装できます。

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

class User(val name: String) {
    companion object : Factory<User> {
        override fun create(): User {
            return User("Guest")
        }
    }
}

この場合、次のように使えます。

val user = User.create()

さらに、Userをファクトリとして渡すこともできます。

fun createSomething(factory: Factory<User>): User {
    return factory.create()
}

val user = createSomething(User)

この書き方ができるのは、Userのcompanion objectがFactory<User>を実装しているからです。

companion objectに拡張関数を定義できる

companion objectには拡張関数を定義することもできます。

class User {
    companion object
}

fun User.Companion.printInfo() {
    println("This is User companion object")
}

呼び出しは次の通りです。

User.printInfo()

この仕組みを使うと、元のクラス定義を変更せずに、クラスに関連する補助機能を追加できます。

companion objectと通常のobjectの違い

Kotlinにはobjectという仕組みもあります。

companion objectと似ていますが、用途は異なります。

通常のobject

object Logger {
    fun log(message: String) {
        println(message)
    }
}

これは独立したシングルトンです。

Logger.log("Hello")

companion object

class User {
    companion object {
        fun createGuest(): User = User()
    }
}

これはUserクラスに属する特別なオブジェクトです。

User.createGuest()

違いの整理

  • object
    独立したシングルトン
  • companion object
    特定のクラスに結びついたシングルトン

companion objectとトップレベル関数の違い

Kotlinでは、クラスの外に直接関数を書くこともできます。

fun hello() {
    println("Hello")
}

これはトップレベル関数です。

一方、同じような処理をcompanion objectに入れることもできます。

class Greeter {
    companion object {
        fun hello() {
            println("Hello")
        }
    }
}

どちらを使うべきか

トップレベル関数が向いているケース

  • 特定のクラスに属さない処理
  • 汎用的なユーティリティ関数
  • シンプルに書きたい処理

companion objectが向いているケース

  • そのクラス専用の生成処理
  • そのクラスに関連する定数
  • クラスの責務として見せたい処理

たとえば、User.createGuest()のような生成処理はcompanion objectが自然です。

一方、文字列整形のような汎用関数はトップレベル関数の方がすっきりすることがあります。

private constructorとの相性がよい

companion objectは、private constructorと組み合わせて使われることがよくあります。

class Secret private constructor(private val code: String) {
    companion object {
        fun create(): Secret {
            return Secret("12345")
        }
    }

    fun show() {
        println(code)
    }
}

この例では、外部から直接Secret()を呼べません。

その代わり、companion objectcreate()を通してのみインスタンスを生成できます。

このようにすると、生成ルールをコントロールしやすくなるため、設計上とても便利です。

Javaとの相互運用

Kotlinのcompanion objectは、Javaから見ると少し見え方が変わります。

たとえば次のコードを見てください。

class User {
    companion object {
        fun greet() {
            println("Hello")
        }
    }
}

Kotlinではこう呼べます。

User.greet()

しかしJavaからは通常、次のように見えます。

User.Companion.greet();

これは、Kotlinのcompanion objectが本当にオブジェクトだからです。

@JvmStatic

Java側でstaticメソッドのように呼びたい場合は、@JvmStaticを使います。

class User {
    companion object {
        @JvmStatic
        fun greet() {
            println("Hello")
        }
    }
}

この場合、Javaからは次のように呼べます。

User.greet();

Javaとの相互運用が必要な場面では、よく使われる注釈です。

@JvmField

プロパティをJavaからフィールドとして直接参照したい場合は、@JvmFieldを使います。

class Config {
    companion object {
        @JvmField
        val VERSION = "1.0"
    }
}

Java側では次のように扱えます。

String v = Config.VERSION;

通常、KotlinのプロパティはJavaから見るとgetter経由になることがありますが、@JvmFieldを付けることでアクセサではなくフィールドとして公開できます。

初期化タイミングについて

ここは少し誤解しやすい部分です。

通常のobject宣言は、最初にアクセスされたときに遅延初期化されます。

一方、companion objectは、対応するクラスがロードされるタイミングで初期化される扱いになります。

class Sample {
    companion object {
        init {
            println("Companion initialized")
        }
    }
}

そのため、companion objectの初期化は、通常のobjectとまったく同じ感覚で考えない方が安全です。

特に、initブロック内に重い処理を書くと、思わぬタイミングでコストが発生する可能性があります。

そのため、companion objectの初期化処理はできるだけ軽く保つのが無難です。

companion objectを使うときの注意点

何でも入れすぎない

companion objectは便利ですが、何でも詰め込むと責務が曖昧になります。

class User {
    companion object {
        fun create() = User()
        fun validateEmail(email: String) = email.contains("@")
        fun formatName(name: String) = name.trim()
        fun exportCsv(users: List<User>) = "..."
    }
}

このように多くの役割を持たせると、保守しにくくなります。

目安

  • 生成処理
  • そのクラス専用の定数
  • そのクラスに密接に関係する処理

このあたりに絞ると、設計がきれいになりやすいです。

可変状態を持たせすぎない

companion objectは1つしか存在しないため、状態を持たせると実質的に共有変数になります。

class Counter {
    companion object {
        var count = 0
    }
}

これは全インスタンスで共有されます。

意図的なら問題ありませんが、安易に使うと次のような問題を招きやすくなります。

  • テストが不安定になる
  • 共有状態のせいでバグが起こる
  • マルチスレッド環境で扱いが難しくなる

staticの代わりとだけ考えない

companion objectは確かにJavaのstaticに近い使い方ができます。
ただし、Kotlinではそれ以外にも次の選択肢があります。

  • トップレベル関数
  • トップレベルプロパティ
  • 通常のobject
  • 拡張関数

そのため、何でもcompanion objectに入れるのではなく、その処理が本当にそのクラスに属するべきかを考えることが大切です。

実務でよくあるパターン

定数置き場

class MainActivity {
    companion object {
        private const val TAG = "MainActivity"
    }
}

ログ用のタグなどでよく見かけます。

生成メソッド

class Product private constructor(val id: Int) {
    companion object {
        fun of(id: Int): Product {
            require(id > 0)
            return Product(id)
        }
    }
}

バリデーション付きの生成処理として使いやすい形です。

ParserやFactoryの実装

interface Parser<T> {
    fun parse(text: String): T
}

class User(val name: String) {
    companion object : Parser<User> {
        override fun parse(text: String): User {
            return User(text)
        }
    }
}
val user = User.parse("Taro")

クラスに関連する共通の変換処理として使えます。

companion objectを使うべきか迷ったときの判断基準

companion objectが向いている

  • そのクラス専用の定数
  • そのクラス専用のインスタンス生成
  • クラスに密接に関係する共通処理
  • Java風のクラスメソッド表現をしたい場合

トップレベル関数が向いている

  • 汎用的なヘルパー関数
  • 特定クラスに属さない処理
  • 小さくシンプルなユーティリティ

別クラスや別objectが向いている

  • 重いロジック
  • 複数クラスにまたがる責務
  • 状態管理を伴う処理
  • テストしやすく分離したい処理

まとめ

Kotlinのcompanion objectは、クラスに関連づいた特別なシングルトンオブジェクトです。

見た目はJavaのstaticに近いものの、本質的にはオブジェクトであることが大きな特徴です。

そのため、次のような用途に向いています。

  • 定数の定義
  • ファクトリメソッド
  • クラス共通の処理
  • Javaとの相互運用
  • インターフェース実装

一方で、何でもcompanion objectに入れると責務が膨らみやすいため、用途は絞った方がよいです。

特に押さえておきたいポイントは次の3つです。

  1. companion objectstaticそのものではなく、特別なobjectである
  2. クラス名から呼べるので、定数や生成処理に向いている
  3. Kotlinではトップレベル関数や通常のobjectもあるため、使い分けが重要

以上、Kotlinのcompanion objectについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次