- Kotlin
Kotlin companion object|static相当とobject
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期連続の黒字決算。