companion object は、クラスの中に1つだけ置ける「そのクラス専用のオブジェクト」で、Java の static メンバーに相当するものを書く場所です。 Kotlin には static というキーワードがないため、インスタンスを作らずに呼びたい関数や、クラス全体で共有したい値は、companion object・object 宣言・トップレベルのいずれかに書きます。

この記事では、Java の static フィールド / static メソッド / 定数 / ファクトリを Kotlin でどう書くかを対応表にまとめ、object 宣言・companion object・トップレベルの3つを「インスタンス数・初期化される時点・Java からの呼び方」で比較します。init のログで初期化の順番を確かめ、Java 側から呼んだときに Companion. が必要になる場合とならない場合も、実際にコンパイルして示します。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc-jvm)/ OpenJDK 21.0.10(javac・javap も同じ版)。掲載したコード・エラー文・出力はすべてこの環境で実行したものです。

Java の static と Kotlin の書き方の早見表

Java での書き方 Kotlin での書き方 Kotlin から Java から
static final String PREFIX = "P-";(定数) companion object の中に const val PREFIX = "P-" Product.PREFIX Product.PREFIX
static final Product EMPTY = ...;(オブジェクトの定数) companion object の中に @JvmField val EMPTY = ... Product.EMPTY Product.EMPTY
static int created;(共有する変数) companion object の中に var created = 0 Product.created Product.Companion.getCreated()
static Product of(int n)(static メソッド・ファクトリ) companion object の中に @JvmStatic fun of(n: Int) Product.of(12) Product.of(12)
同上(アノテーションなし) companion object の中に fun parse(text: String) Product.parse("P-1") Product.Companion.parse("P-1")
static { ... }(static 初期化ブロック) companion object { init { ... } } ― ―
static メソッドだけのユーティリティクラス トップレベル関数 fun shout(text: String) shout("ok") HelpersKt.shout("ok")
シングルトン object Stock { ... } Stock.total() Stock.INSTANCE.total()

Kotlin から呼ぶときは、どれも「クラス名.名前」または名前だけで書けます。 違いが出るのは Java から呼ぶときで、@JvmStatic・@JvmField・const のどれも付けないメンバーは Companion を経由します。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin companion object|static相当とobject」

この記事のコードの動かし方

Kotlin だけのコードは、次の2行で実行できます。

kotlinc sample.kt -include-runtime -d sample.jar
java -jar sample.jar

Kotlin と Java を組み合わせるコードは、Kotlin を先にコンパイルし、その出力先と Kotlin の標準ライブラリ(kotlinc の lib フォルダにある kotlin-stdlib.jar)をクラスパスに入れて javac・java を実行しました。

kotlinc Product.kt TextUtils.kt Helpers.kt -d out
javac -cp out:kotlinc/lib/kotlin-stdlib.jar -d out Main.java
java -cp out:kotlinc/lib/kotlin-stdlib.jar Main

日本語の出力が ? になるときは、java に -Dstdout.encoding=UTF-8 を付けます。

companion object の基本の書き方

クラスの中に companion object { } を書き、その中に関数やプロパティを置くと、クラス名.名前 で呼べるようになります。 よく使うのは、定数・共有する値・ファクトリ(インスタンスを作る関数)の3つです。

class User private constructor(val id: Int, val name: String) {
    companion object {
        const val MAX_NAME_LENGTH = 20          // 定数
        private var nextId = 1                  // クラス全体で共有する値

        fun of(name: String): User {            // ファクトリ
            require(name.length <= MAX_NAME_LENGTH) { "名前は $MAX_NAME_LENGTH 文字までです" }
            return User(nextId++, name)         // private なコンストラクタを呼べる
        }

        fun createdCount(): Int = nextId - 1
    }

    override fun toString() = "User(id=$id, name=$name)"
}

fun main() {
    val a = User.of("sato")                     // クラス名.関数名 で呼べる
    val b = User.of("suzuki")
    println(a)
    println(b)
    println(User.createdCount())
    println(User.MAX_NAME_LENGTH)
    println(User.Companion.createdCount())      // 省略しない書き方
    val companion = User                        // companion object は値として扱える
    println(companion.of("tanaka"))
}

実行結果

User(id=1, name=sato)
User(id=2, name=suzuki)
2
20
2
User(id=3, name=tanaka)

押さえておきたい点は4つです。

  • User.of("sato") は User.Companion.of("sato") の省略形です。 名前を付けない companion object は、Companion という名前になります。
  • companion object は、外側のクラスの private なコンストラクタやメンバーに触れられます。 そのため「コンストラクタを private にして、作る入口を of だけにする」というファクトリの形が書けます。
  • nextId は全インスタンスで共有されます。 3つ作ると id が 1・2・3 と進んでいます。
  • companion object は実体のあるオブジェクトです。 val companion = User のように変数に入れられます。Java の static メンバーとの大きな違いで、後述のインターフェース実装につながります。

書き方を間違えたときのエラー

つまずきやすい書き方を1つのファイルに集めてコンパイルしました。

class Screen
fun main() {
    val user = User("sato")
}
class User private constructor(val name: String) {
    val nickname = "x"
    companion object {
        fun show() = println(nickname)
    }
    companion object Second {}
    const val LIMIT = 3
}
object Registry(val size: Int) {}

コンパイル結果

b2.kt:3:16: error: cannot access 'constructor(name: String): User': it is private in 'User'.
    val user = User("sato")
               ^^^^
b2.kt:8:30: error: unresolved reference 'nickname'.
        fun show() = println(nickname)
                             ^^^^^^^^
b2.kt:10:5: error: only one companion object is allowed per class.
    companion object Second {}
    ^^^^^^^^^
b2.kt:11:5: error: const 'val' is only allowed on top level, in named objects, in companion objects or companion blocks.
    const val LIMIT = 3
    ^^^^^
b2.kt:13:16: error: objects cannot have constructors.
object Registry(val size: Int) {}
               ^^^^^^^^^^^^^^^
エラー文 原因 直し方
cannot access 'constructor(...)': it is private in 'User'. private なコンストラクタを外から呼んだ companion object のファクトリ関数を呼ぶ
unresolved reference 'nickname'. companion object からインスタンスのプロパティを直接読んだ 引数でインスタンスを受け取り user.nickname と書く
only one companion object is allowed per class. 1つのクラスに companion object を2つ書いた 1つにまとめる
const 'val' is only allowed on top level, in named objects, in companion objects or companion blocks. クラス本体に直接 const val を書いた companion object の中かトップレベルに移す
objects cannot have constructors. object 宣言にコンストラクタ引数を付けた 引数が必要ならクラスにする。初期値は object の中で決める

2つ目のエラーは、companion object の関数にはインスタンス(this)がないことを表しています。Java で static メソッドからインスタンスのフィールドを直接読めないのと同じ考え方です。

Java の static との対応を実際に呼んで確かめる

ここからは Kotlin で書いたクラスを Java から呼び、アノテーションの有無で呼び方がどう変わるかを見ます。

// Product.kt
class Product private constructor(val code: String) {
    companion object {
        const val PREFIX = "P-"                           // 定数
        @JvmField val EMPTY = Product("P-0000")           // static フィールドとして公開
        val plain = Product("P-9999")                     // 何も付けないプロパティ
        var created = 0                                   // 何も付けない var

        @JvmStatic
        fun of(number: Int): Product {                    // static メソッドとして公開
            created++
            return Product(PREFIX + number.toString().padStart(4, '0'))
        }

        fun parse(text: String): Product = Product(text)  // 何も付けない関数
    }

    override fun toString() = "Product($code)"
}

object Stock {
    const val UNIT = "個"
    private val counts = mutableMapOf<String, Int>()

    @JvmStatic
    fun add(code: String, n: Int) { counts[code] = (counts[code] ?: 0) + n }

    fun total(): Int = counts.values.sum()
}
// TextUtils.kt
@file:JvmName("TextUtils")

const val SEPARATOR = ","
val defaultLabel = "none"

fun joinCodes(codes: List<String>): String = codes.joinToString(SEPARATOR)
// Helpers.kt
fun shout(text: String): String = text.uppercase() + "!"

Java 側のコードです。

// Main.java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 定数(const val)
        System.out.println(Product.PREFIX);
        // @JvmField を付けたプロパティ
        System.out.println(Product.EMPTY);
        // 何も付けないプロパティは Companion の getter 経由
        System.out.println(Product.Companion.getPlain());
        // @JvmStatic を付けた関数は Companion なしで呼べる
        System.out.println(Product.of(12));
        // Companion 経由でも呼べる
        System.out.println(Product.Companion.of(13));
        // 何も付けない関数は Companion が必要
        System.out.println(Product.Companion.parse("P-0500"));
        System.out.println(Product.Companion.getCreated());

        // object 宣言
        Stock.add("P-0012", 5);              // @JvmStatic あり
        Stock.INSTANCE.add("P-0013", 2);     // INSTANCE 経由でも呼べる
        System.out.println(Stock.INSTANCE.total() + Stock.UNIT);

        // トップレベル関数
        System.out.println(TextUtils.joinCodes(List.of("a", "b")));   // @file:JvmName あり
        System.out.println(TextUtils.SEPARATOR + " / " + TextUtils.getDefaultLabel());
        System.out.println(HelpersKt.shout("ok"));                    // ファイル名 + Kt
    }
}

実行結果

P-
Product(P-0000)
Product(P-9999)
Product(P-0012)
Product(P-0013)
Product(P-0500)
2
7個
a,b
, / none
OK!

Companion を省いたときの Java 側のエラー

アノテーションを付けていないメンバーを、Java の static と同じ書き方で呼んでみます。

public class Bad {
    public static void main(String[] args) {
        System.out.println(Product.parse("P-0500"));
        System.out.println(Product.plain);
        System.out.println(Stock.total());
        Product p = new Product("P-1");
    }
}

javac の結果

Bad.java:3: error: cannot find symbol
        System.out.println(Product.parse("P-0500"));
                                  ^
  symbol:   method parse(String)
  location: class Product
Bad.java:4: error: plain has private access in Product
        System.out.println(Product.plain);
                                  ^
Bad.java:5: error: non-static method total() cannot be referenced from a static context
        System.out.println(Stock.total());
                                ^
Bad.java:6: error: Product(String) has private access in Product
        Product p = new Product("P-1");
                    ^
4 errors
Java での書き方 結果 通る書き方
Product.parse("P-0500") cannot find symbol Product.Companion.parse("P-0500")、または Kotlin 側に @JvmStatic
Product.plain private access Product.Companion.getPlain()、または Kotlin 側に @JvmField
Stock.total() non-static method Stock.INSTANCE.total()、または Kotlin 側に @JvmStatic
new Product("P-1") private access Product.of(1)

javap で生成されたクラスを見る

なぜこの違いになるのかは、コンパイル後のクラスを javap -p -constants で見ると分かります(抜粋)。

public final class Product {
  public static final Product$Companion Companion;
  private final java.lang.String code;
  public static final java.lang.String PREFIX = "P-";
  public static final Product EMPTY;
  private static final Product plain;
  private static int created;
  private Product(java.lang.String);
  public static final Product of(int);
  ...
}

public final class Product$Companion {
  public final Product getPlain();
  public final int getCreated();
  public final void setCreated(int);
  public final Product of(int);
  public final Product parse(java.lang.String);
  ...
}

public final class Stock {
  public static final Stock INSTANCE;
  private Stock();
  public static final void add(java.lang.String, int);
  public final int total();
  ...
}

public final class TextUtils {
  public static final java.lang.String SEPARATOR = ",";
  private static final java.lang.String defaultLabel;
  public static final java.lang.String getDefaultLabel();
  public static final java.lang.String joinCodes(java.util.List<java.lang.String>);
  ...
}
Kotlin での書き方 生成されるもの Java からの呼び方
companion の const val PREFIX Product に public static final フィールド Product.PREFIX
companion の @JvmField val EMPTY Product に public static final フィールド Product.EMPTY
companion の val plain(何も付けない) Product に private static フィールド + Companion に getter Product.Companion.getPlain()
companion の var created private static フィールド + Companion に getter / setter Product.Companion.getCreated()
companion の @JvmStatic fun of Product に static メソッド + Companion にも同名メソッド Product.of(12) と Product.Companion.of(13) の両方
companion の fun parse Companion のインスタンスメソッドだけ Product.Companion.parse(...)
object の @JvmStatic fun add Stock に static メソッド Stock.add(...) と Stock.INSTANCE.add(...) の両方
object の fun total Stock のインスタンスメソッド Stock.INSTANCE.total()
トップレベルの fun shout(Helpers.kt) HelpersKt クラスの static メソッド HelpersKt.shout("ok")
トップレベル + @file:JvmName("TextUtils") TextUtils クラスの static メソッド TextUtils.joinCodes(...)

companion object の正体は、外側のクラスが static フィールド Companion に持っている1つのインスタンスです。プロパティの値そのものは外側のクラスの static フィールドに置かれ、何も付けなければ Companion の getter 経由で読む形になります。

@JvmStatic・@JvmField・const の使い分け

付けるもの 対象 効果 向いている場面
const プリミティブ型と String の val コンパイル時定数になり、Java からも static フィールドとして読める 数値や文字列の固定値
@JvmField val / var のプロパティ getter を作らず、フィールドをそのまま公開する オブジェクトの定数を Java から クラス名.名前 で読ませたい
@JvmStatic companion object / object の関数 外側のクラスに static メソッドを追加で生成する Java から クラス名.関数名() で呼ばせたい
何も付けない ― Kotlin からは同じ書き方で呼べる Kotlin だけで使うコード

Kotlin だけで完結するコードなら、@JvmStatic や @JvmField は付けなくても呼び方は変わりません。 Java のコードから呼ばれる入口にだけ付ける、と決めておくと整理しやすくなります。公式ドキュメントの Java 連携のページでも、静的フィールドとして公開する方法として @JvmField・lateinit・const の3つ、静的メソッドとして @JvmStatic が紹介されています。

object 宣言:1つだけのインスタンス(シングルトン)

object 名前 { } と書くと、クラスの定義とただ1つのインスタンスの作成を同時に行えます。 companion object がクラスに付属するのに対し、object 宣言は単独で存在します。

object Counter {
    var count = 0
    fun increment(): Int = ++count
}
data object Empty
object Plain

interface Shape { fun area(): Double }
object UnitSquare : Shape { override fun area() = 1.0 }

fun main() {
    Counter.increment()
    Counter.increment()
    val a = Counter
    val b = Counter
    println(a === b)            // いつも同じ1つ
    println(Counter.count)
    println(Empty)              // data object は名前がそのまま出る
    println(Plain.toString().substringBefore("@"))
    println(UnitSquare.area())

    val listener = object : Shape {     // object 式(その場で作る無名オブジェクト)
        override fun area() = 2.5
    }
    println(listener.area())
}

実行結果

true
2
Empty
Plain
1.0
2.5

object というキーワードは3つの場面で使われます。

書き方 呼び名 何ができるか
object Counter { } object 宣言 名前のある、ただ1つのインスタンスを定義する
companion object { } コンパニオンオブジェクト クラスの中に1つだけ置ける、そのクラスに付属するオブジェクト
object : Shape { } object 式 その場で使う無名のオブジェクトを作る

data object Empty のように data を付けると、println したときに Empty と名前がそのまま出ます。付けない Plain は、名前のあとに @ と16進数の値が続く表示でした(上のコードでは @ より前だけを出力しています)。

object 宣言・companion object・トップレベルの比較

比べる点 object 宣言 companion object トップレベル
書く場所 ファイルの直下 クラスの中(1クラスに1つ) ファイルの直下
インスタンス数 1つ(INSTANCE に入る) 外側のクラスごとに1つ(Companion に入る) 0(インスタンスは作られず、static メソッドになる)
初期化される時点 最初にアクセスしたとき 外側のクラスが読み込まれたとき(最初にそのクラスを使ったとき) そのファイルの関数やプロパティを最初に使ったとき
Kotlin からの呼び方 Stock.total() Product.of(12) shout("ok")
Java からの呼び方 Stock.INSTANCE.total() Product.Companion.parse(...) HelpersKt.shout("ok")
Java から短く呼ぶには @JvmStatic で Stock.add(...) @JvmStatic で Product.of(12) @file:JvmName でクラス名を変える
クラスの private に触れるか 触れない 外側のクラスの private に触れる 触れない
インターフェースの実装 できる できる ―
向いている用途 状態を1か所で持つ、設定、レジストリ ファクトリ、そのクラスに関する定数 どのクラスにも属さない関数や定数

「初期化される時点」は、公式ドキュメントに「object 宣言の初期化はスレッド安全で、最初のアクセス時に行われる」「companion object は対応するクラスが読み込まれたときに初期化され、Java の static 初期化子と同じ意味になる」と書かれています。次の節でログを出して確かめます。

init のログで初期化の順番を見る

val topValue: String = "top".also { println("  [トップレベル] topValue を初期化") }

object Registry {
    init { println("  [object] Registry の init") }
    val items = mutableListOf<String>()
    fun add(item: String) { items.add(item) }
}

class Report(val title: String) {
    init { println("  [インスタンス] Report($title) の init") }

    companion object {
        init { println("  [companion] Report.Companion の init") }
        const val FORMAT = "csv"
        val createdAt = "2026-10-03".also { println("  [companion] createdAt を初期化") }
        fun describe() = "Report format=$FORMAT"
    }
}

fun main() {
    println("1. main 開始")
    println("2. const val を読む: ${Report.FORMAT}")
    println("3. Report を1つ作る")
    Report("A")
    println("4. Report をもう1つ作る")
    Report("B")
    println("5. companion の関数を呼ぶ: ${Report.describe()}")
    println("6. Registry を初めて使う")
    Registry.add("x")
    println("7. Registry をもう一度使う")
    Registry.add("y")
    println("8. items = ${Registry.items} / topValue = $topValue")
}

実行結果

  [トップレベル] topValue を初期化
1. main 開始
2. const val を読む: csv
3. Report を1つ作る
  [companion] Report.Companion の init
  [companion] createdAt を初期化
  [インスタンス] Report(A) の init
4. Report をもう1つ作る
  [インスタンス] Report(B) の init
5. companion の関数を呼ぶ: Report format=csv
6. Registry を初めて使う
  [object] Registry の init
7. Registry をもう一度使う
8. items = [x, y] / topValue = top

この出力から分かることを順に整理します。

番号 起きたこと 分かること
main の前 topValue の初期化が main より先に動いた main と同じファイルのトップレベルのプロパティは、そのファイルのクラスが読み込まれた時点で初期化される
2 const val を読んでも companion の init は動かない const val は値が使う側に埋め込まれるので、クラスの読み込みが起きない
3 1つ目のインスタンスを作るときに companion の init が先に動く companion object の初期化は、インスタンスの init より前
4 2つ目では companion の init は動かない companion object の初期化は1回だけ
6 Registry の init は、初めて使った行で動く object 宣言は最初のアクセスまで初期化されない
7 2回目は動かない object 宣言の初期化も1回だけ

別ファイルのトップレベルと、クラス参照だけの場合

トップレベルのプロパティを別のファイル(Settings.kt)に分けると、そのファイルの関数を最初に呼んだときに初期化されました。

// Settings.kt
val startedLabel: String = "started".also { println("  [トップレベル] Settings.kt の startedLabel を初期化") }
val version: Int = 3.also { println("  [トップレベル] Settings.kt の version を初期化") }
fun appName() = "stock-tool"
// i2.kt
fun main() {
    println("1. main 開始")
    println("2. Settings.kt の関数を呼ぶ: ${appName()}")
    println("3. もう一度: $startedLabel")
    println("4. 別の型に触れる")
    val k = Holder::class.simpleName
    println("5. クラス参照だけ: $k")
    println("6. companion の関数を呼ぶ")
    Holder.touch()
    println("7. object 式はその場で初期化")
    val temp = object { init { println("  [object 式] init") } ; val x = 1 }
    println("8. ${temp.x}")
}
class Holder {
    companion object {
        init { println("  [companion] Holder.Companion の init") }
        fun touch() = println("  touch")
    }
}

実行結果(kotlinc i2.kt Settings.kt -include-runtime -d i2.jar のあと java -cp i2.jar I2Kt)

1. main 開始
  [トップレベル] Settings.kt の startedLabel を初期化
  [トップレベル] Settings.kt の version を初期化
2. Settings.kt の関数を呼ぶ: stock-tool
3. もう一度: started
4. 別の型に触れる
5. クラス参照だけ: Holder
6. companion の関数を呼ぶ
  [companion] Holder.Companion の init
  touch
7. object 式はその場で初期化
  [object 式] init
8. 1
  • appName() を呼んだだけで、同じファイルの startedLabel と version がまとめて初期化されました。 トップレベルのプロパティは、ファイル単位で初期化されると考えると分かりやすくなります。
  • Holder::class.simpleName でクラス名を得ただけでは、companion の init は動きませんでした。 companion の関数を呼んだ時点で初めて動いています。
  • object 式は、書いた行を通った時点で初期化されます。

複数スレッドから同時に使ったとき

object 宣言を4本のスレッドから同時に読み、init が何回動くかを確かめました。

import kotlin.concurrent.thread

object Config {
    init {
        println("  [object] init を実行 (${Thread.currentThread().name})")
        Thread.sleep(50)
    }
    val name = "stock-tool"
}

fun main() {
    val threads = (1..4).map { i -> thread(name = "worker-$i") { Config.name.length } }
    threads.forEach { it.join() }
    println("4本のスレッドから読み終わりました")
}

実行結果(worker のあとの番号は実行のたびに変わるため N と表記しています)

  [object] init を実行 (worker-N)
4本のスレッドから読み終わりました

init が動いたのは1回だけで、公式ドキュメントの「初期化はスレッド安全」という説明どおりの結果でした。なお、初期化のあとに var を複数スレッドから書き換える場合の同期は、別途用意します。

companion object の応用:インターフェース実装・名前付き・拡張関数

companion object は実体のあるオブジェクトなので、Java の static メンバーにはできない書き方ができます。

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

// 1. companion object にインターフェースを実装する
class Point(val x: Int, val y: Int) {
    companion object : Parser<Point> {
        override fun parse(text: String): Point {
            val (x, y) = text.split(",").map { it.trim().toInt() }
            return Point(x, y)
        }
    }
    override fun toString() = "Point($x, $y)"
}

// 2. 名前付きの companion object
class Color(val hex: String) {
    companion object Palette {
        val RED = Color("#ff0000")
        fun gray(level: Int) = Color("#" + level.toString(16).padStart(2, '0').repeat(3))
    }
    override fun toString() = "Color($hex)"
}

// 3. 中身が空の companion object に、あとから拡張関数を足す
class Task(val title: String) {
    companion object {}
    override fun toString() = "Task($title)"
}
fun Task.Companion.fromLine(line: String): Task = Task(line.substringAfter(":").trim())
val Task.Companion.DEFAULT_TITLE: String get() = "無題"

// インターフェース型の引数に、クラス名をそのまま渡せる
fun <T> parseAll(parser: Parser<T>, lines: List<String>): List<T> = lines.map { parser.parse(it) }

fun main() {
    println(Point.parse("3, 4"))
    println(parseAll(Point, listOf("1,2", "5,6")))

    println(Color.RED)
    println(Color.gray(128))
    println(Color.Palette.gray(255))        // 名前を付けたので Palette で参照できる

    println(Task.fromLine("todo: 資料を作る"))
    println(Task.DEFAULT_TITLE)
    println(Task.Companion::class.simpleName + " / " + Color.Palette::class.simpleName)
}

実行結果

Point(3, 4)
[Point(1, 2), Point(5, 6)]
Color(#ff0000)
Color(#808080)
Color(#ffffff)
Task(資料を作る)
無題
Companion / Palette
書き方 できること 例
companion object : Parser<Point> クラス名そのものを、インターフェース型の値として渡せる parseAll(Point, lines)
companion object Palette Companion の代わりに付けた名前で参照できる Color.Palette.gray(255)
fun Task.Companion.fromLine(...) クラスの外から「クラス名.関数名」で呼べる関数を足せる Task.fromLine("todo: ...")

1つ目の例では、parseAll(Point, ...) のようにクラス名を引数として渡しています。 これは Point の companion object が Parser<Point> を実装したオブジェクトだからで、「文字列から作れる型」を共通の形で扱えるようになります。

拡張関数を足すときの条件

拡張関数を足すには、対象のクラスに companion object が宣言されている必要があります。宣言がないクラスや、名前を付けた companion object に対して Companion と書くと、次のエラーになります。

class Memo(val text: String)
fun Memo.Companion.empty() = Memo("")
fun main() { println(Color.Companion.RED) }
class Color { companion object Palette { val RED = "r" } }
c2.kt:2:10: error: unresolved reference 'Companion'.
fun Memo.Companion.empty() = Memo("")
         ^^^^^^^^^
c2.kt:3:28: error: unresolved reference 'Companion'.
fun main() { println(Color.Companion.RED) }
                           ^^^^^^^^^
状況 直し方
クラスに companion object がない クラスに companion object {} を1行足す
companion object に名前を付けている Color.Palette.RED のように付けた名前で参照する(Color.RED はどちらでも使える)

名前付き companion を Java から呼ぶ

名前を付けると、Java から見える static フィールドの名前も変わります。

// Color.kt
class Color(val hex: String) {
    companion object Palette {
        @JvmStatic fun gray(level: Int) = Color("#" + level.toString(16).padStart(2, '0').repeat(3))
        fun named(name: String) = Color(if (name == "red") "#ff0000" else "#000000")
    }
    override fun toString() = "Color($hex)"
}
// NamedMain.java
public class NamedMain {
    public static void main(String[] args) {
        System.out.println(Color.gray(128));             // @JvmStatic あり
        System.out.println(Color.Palette.named("red"));  // Companion ではなく付けた名前で呼ぶ
    }
}
Color(#808080)
Color(#ff0000)

javap で見ると、Color クラスには public static final Color$Palette Palette; というフィールドができていました。

どこに書くかの判断表

// トップレベル:どのクラスにも属さない関数と定数
const val TAX_LABEL = "税込"
fun formatPrice(price: Int): String = "%,d円(%s)".format(price, TAX_LABEL)

// companion object:クラスの private な部分に触れられる
class Coupon private constructor(private val secret: String) {
    companion object {
        fun issue(): Coupon = Coupon("C-" + (1000..9999).first())
        fun reveal(coupon: Coupon): String = coupon.secret     // private なプロパティを読める
    }
}

// object:状態を1か所で持つ
object Cart {
    private val items = mutableListOf<Int>()
    fun add(price: Int) { items.add(price) }
    fun total() = items.sum()
}

fun main() {
    println(formatPrice(1280))
    println(Coupon.reveal(Coupon.issue()))
    Cart.add(1280); Cart.add(320)
    println(formatPrice(Cart.total()))
}
1,280円(税込)
C-1000
1,600円(税込)
書きたいもの 置き場所 理由
どのクラスにも属さない便利関数 トップレベル関数 クラス名を付けずに呼べる
アプリ全体で使う数値・文字列の定数 トップレベルの const val どこからでも名前だけで参照できる
そのクラスに意味が結びつく定数 companion object の const val User.MAX_NAME_LENGTH のようにクラス名で意味が伝わる
インスタンスを作る入口(ファクトリ) companion object の関数 private なコンストラクタを呼べる
クラスの private な中身を使う補助関数 companion object の関数 外側のクラスの private に触れられる
アプリに1つだけあればよい状態や設定 object 宣言 インスタンスが1つだけになる
Java から static として呼ばせたい関数 companion object + @JvmStatic クラス名.関数名() で呼べる
その場かぎりのインターフェース実装 object 式 名前を付けずに書ける

迷ったら「まずトップレベル関数、クラスの private に触れたいときやファクトリは companion object、状態を1つにまとめたいときは object 宣言」の順で考えると決めやすくなります。

よくある質問

Q. Kotlin に static はないのですか? A. static というキーワードはありません。代わりに companion object・object 宣言・トップレベルの関数やプロパティを使います。Java から static として見せたいときは @JvmStatic・@JvmField・const を付けます。

Q. companion object と object 宣言はどうちがいますか? A. companion object はクラスの中に1つだけ置け、外側のクラスの private なメンバーに触れられ、クラス名.名前 で呼べます。object 宣言は単独で存在するただ1つのインスタンスです。初期化の時点もちがい、companion object は外側のクラスが読み込まれたとき、object 宣言は最初にアクセスしたときです。

Q. Java から呼ぶと Companion と書かなければならないのはなぜですか? A. 何も付けない関数は、Companion という static フィールドに入ったインスタンスのメソッドとして生成されるためです。Kotlin 側に @JvmStatic を付けると、外側のクラスにも static メソッドが作られ、Product.of(12) と書けるようになります。

Q. companion object の init はいつ動きますか? A. 今回の確認では、最初のインスタンスを作るとき、または companion object の関数を最初に呼んだときに1回だけ動きました。const val を読むだけ、クラス名::class を書くだけでは動きませんでした。

まとめ

  • companion object はクラスに1つだけ置ける、そのクラス専用のオブジェクト。 クラス名.名前 で呼べ、外側の private にも触れられる
  • Java の static に相当する書き方は、companion object・object 宣言・トップレベルの3つ
  • Java から呼ぶとき、何も付けないメンバーは Companion. や INSTANCE. が必要。 @JvmStatic・@JvmField・const を付けると省ける
  • インスタンス数は object 宣言が1つ、companion object がクラスごとに1つ、トップレベルは0
  • 初期化は、object 宣言が最初のアクセス時、companion object がクラスの読み込み時、トップレベルはそのファイルを最初に使ったとき
  • companion object はインターフェースを実装でき、名前を付けられ、拡張関数も足せる
  • 迷ったらトップレベル関数から考え、ファクトリは companion object、1つだけの状態は object 宣言

参考

  • Kotlin 公式ドキュメント Object declarations and expressions(object 宣言、companion object、初期化の時点): https://kotlinlang.org/docs/object-declarations.html
  • Kotlin 公式ドキュメント Calling Kotlin from Java(@JvmStatic、@JvmField、@file:JvmName): https://kotlinlang.org/docs/java-to-kotlin-interop.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin companion object|static相当とobject」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?