- Kotlin
Kotlin enum classの使い方 entries・valueOf一覧
Kotlin の enum class(列挙型)とは、取り得る値をあらかじめ決めた定数の集まりを表すクラスです。 サイズ、曜日、状態のように「この中のどれか」と決まっているものを型にでき、決めていない値を書くとコンパイルの段階で気づけます。
この記事では、enum class に最初から付いているプロパティと関数を、戻り値と実行結果つきの一覧表にまとめました。Kotlin 1.9 から使える entries と従来の values() の違い、valueOf に存在しない名前を渡したときの例外の全文、それを null に変える書き方、プロパティ・メソッド・interface 実装・コード値からの逆引きまで、1つずつ動かして示します。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。
enum class の機能早見表
enum class Size { SMALL, MEDIUM, LARGE } に対する結果です。
| 書き方 | 戻り値の型 | 実行結果 | 用途 |
|---|---|---|---|
Size.MEDIUM.name |
String | MEDIUM |
定数名を文字列で得る |
Size.MEDIUM.ordinal |
Int | 1 |
宣言順の番号(0始まり) |
Size.MEDIUM.toString() |
String | MEDIUM |
既定では name と同じ。上書きできる |
Size.entries |
EnumEntries<Size>(読み取り専用の List) |
[SMALL, MEDIUM, LARGE] |
全定数の一覧。毎回同じリスト |
Size.entries.size |
Int | 3 |
定数の個数 |
Size.entries[1] |
Size | MEDIUM |
番号から定数を得る |
Size.entries.getOrNull(5) |
Size? | null |
範囲外なら null |
Size.values() |
Array<Size> |
[SMALL, MEDIUM, LARGE] |
全定数の配列。呼ぶたびに新しい配列 |
Size.valueOf("LARGE") |
Size | LARGE |
名前から定数を得る。無い名前は例外 |
enumValueOf<Size>("SMALL") |
Size | SMALL |
valueOf の型引数版 |
enumEntries<Size>() |
EnumEntries<Size> |
[SMALL, MEDIUM, LARGE] |
entries の型引数版 |
enumValues<Size>() |
Array<Size> |
[SMALL, MEDIUM, LARGE] |
values() の型引数版 |
Size.SMALL.compareTo(Size.LARGE) |
Int | -2 |
ordinal の差 |
Size.SMALL < Size.LARGE |
Boolean | true |
宣言順で大小を比べる |
Size.entries.find { it.name == "XL" } |
Size? | null |
名前から探し、無ければ null |
全定数の一覧は entries、名前からの変換は valueOf(無い名前があり得るなら entries.find)と覚えておくと、ほとんどの用途をまかなえます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin enum classの使い方 entries・valueOf一覧」
まず動かす:基本の機能を1つのファイルで確認する
次のコードを Main.kt という名前で保存し、そのままコンパイルして実行できます。
import kotlin.enums.enumEntries
enum class Size { SMALL, MEDIUM, LARGE }
fun main() {
val s = Size.MEDIUM
println(s.name) // 定数名
println(s.ordinal) // 0 始まりの宣言順
println(s) // toString() は既定で name と同じ
println(Size.entries) // 全定数のリスト
println(Size.entries.size)
println(Size.values().contentToString()) // 全定数の配列
println(Size.valueOf("LARGE")) // 名前から定数
println(enumValueOf<Size>("SMALL")) // 型引数で指定する版
println(enumEntries<Size>()) // 型引数で指定する entries
println(enumValues<Size>().contentToString())
println(Size.SMALL.compareTo(Size.LARGE)) // ordinal の差
println(Size.SMALL < Size.LARGE)
println(Size.MEDIUM == Size.valueOf("MEDIUM"))
println(Size.entries.sortedDescending())
println(Size.entries[1])
println(Size.entries.getOrNull(5))
println(Size.MEDIUM.declaringJavaClass.simpleName)
}
kotlinc Main.kt -include-runtime -d main.jar
java -Dstdout.encoding=UTF-8 -jar main.jar
-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です(このコードの出力は英数字だけですが、以降の例に合わせています)。
MEDIUM
1
MEDIUM
[SMALL, MEDIUM, LARGE]
3
[SMALL, MEDIUM, LARGE]
LARGE
SMALL
[SMALL, MEDIUM, LARGE]
[SMALL, MEDIUM, LARGE]
-2
true
true
[LARGE, MEDIUM, SMALL]
MEDIUM
null
Size
- 定数はカンマ区切りで並べるだけで定義できます。慣例として大文字とアンダースコアで書きます。
- enumEntries は import が必要です(
kotlin.enums.enumEntries)。enumValueOf と enumValues は import なしで使えます。 - 定数どうしは == で比べられます。同じ定数は常に1個なので、valueOf で得たものとも等しくなります。
values() と entries の違い
全定数を得る方法は2つあります。Kotlin 1.9.0 で entries が安定版になり、公式ドキュメントでも entries が推奨されています。values() も引き続き使え、今回の環境ではコンパイル時の警告も出ませんでした。
enum class Size { SMALL, MEDIUM, LARGE }
fun main() {
// values():呼ぶたびに新しい配列
val a = Size.values()
val b = Size.values()
println("values() === values() : ${a === b}")
a[0] = Size.LARGE // 配列なので書き換えられる
println("書き換えた配列 : ${a.contentToString()}")
println("もう一度 values() : ${Size.values().contentToString()}")
// entries:毎回同じ不変リスト
val x = Size.entries
val y = Size.entries
println("entries === entries : ${x === y}")
println("型 : ${x::class.simpleName}")
try {
@Suppress("UNCHECKED_CAST")
(x as MutableList<Size>).add(Size.SMALL)
} catch (e: Exception) {
println("追加しようとすると : ${e::class.simpleName}")
}
}
values() === values() : false
書き換えた配列 : [LARGE, MEDIUM, LARGE]
もう一度 values() : [SMALL, MEDIUM, LARGE]
entries === entries : true
型 : EnumEntriesList
追加しようとすると : ClassCastException
| 比べる点 | values() |
entries |
|---|---|---|
| 戻り値 | Array<Size>(配列) |
EnumEntries<Size>(読み取り専用の List) |
| 呼ぶたびに | 新しい配列を作ってコピーする | 同じリストを返す |
| 中身の書き換え | 受け取った配列は書き換えられる(元の定義には影響しない) | 書き換えられない |
| 使える関数 | 配列の関数 | List の関数(filter・map・find などがそのまま使える) |
| 型引数版 | enumValues<T>() |
enumEntries<T>() |
| 使えるバージョン | 当初から | 1.9.0 で安定版 |
values() が毎回コピーを作るのは、配列が書き換え可能だからです。 呼び出し側が書き換えても元の定義に影響しないよう、新しい配列を渡しています。entries は書き換えられないリストなので、同じものを何度でも返せます。ループの中で何度も呼ぶ場面では、この差が効いてきます。
書き換えは多くの場合、次の置き換えで済みます。
| これまでの書き方 | entries での書き方 |
|---|---|
Size.values() |
Size.entries |
Size.values().size |
Size.entries.size |
Size.values()[i] |
Size.entries[i] |
Size.values().find { … } |
Size.entries.find { … } |
enumValues<T>() |
enumEntries<T>() |
| 配列そのものが必要な場所 | Size.entries.toTypedArray() |
文字列から enum に変換する:valueOf と例外
valueOf は、定数名と完全に一致する文字列を渡すとその定数を返します。存在しない名前を渡すとどうなるかを確かめます。
enum class Size { SMALL, MEDIUM, LARGE }
fun main() {
println(Size.valueOf("XL"))
}
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Size.XL
at java.base/java.lang.Enum.valueOf(Enum.java:293)
at Size.valueOf(Main.kt)
at MainKt.main(Main.kt:4)
at MainKt.main(Main.kt)
IllegalArgumentException が発生し、メッセージは No enum constant Size.XL です。 画面の入力値や外部から届いた文字列のように、無い名前があり得る場面では、例外ではなく null で受け取る書き方が扱いやすくなります。
null に落とす書き方
enum class Size { SMALL, MEDIUM, LARGE }
// どの enum にも使える「見つからなければ null」
inline fun <reified T : Enum<T>> enumValueOrNull(name: String): T? =
enumValues<T>().find { it.name == name }
fun main() {
val input = "XL"
// 1. entries.find:見つからなければ null
println(Size.entries.find { it.name == input })
// 2. firstOrNull も同じ意味
println(Size.entries.firstOrNull { it.name == input })
// 3. runCatching で例外を null に変える
println(runCatching { Size.valueOf(input) }.getOrNull())
// 4. 既定値に落とす
println(Size.entries.find { it.name == input } ?: Size.MEDIUM)
// 5. 大文字小文字や前後の空白をそろえてから探す
println(Size.entries.find { it.name.equals(" large ".trim(), ignoreCase = true) })
// 6. 型引数つきの共通関数
println(enumValueOrNull<Size>("SMALL"))
println(enumValueOrNull<Size>("XL"))
// 7. 小文字のまま valueOf に渡すと例外になる
try {
Size.valueOf("small")
} catch (e: IllegalArgumentException) {
println(e.message)
}
try {
enumValueOf<Size>("XL")
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
null
null
null
MEDIUM
LARGE
SMALL
null
No enum constant Size.small
No enum constant Size.XL
| 書き方 | 無い名前のとき | 向いている場面 |
|---|---|---|
Size.valueOf(s) |
IllegalArgumentException | 必ず存在する名前を渡すとき |
Size.entries.find { it.name == s } |
null | 基本はこれ。条件を自由に書ける |
Size.entries.firstOrNull { it.name == s } |
null | find と同じ意味 |
runCatching { Size.valueOf(s) }.getOrNull() |
null | valueOf をそのまま使いたいとき |
… ?: Size.MEDIUM |
既定値 | 無ければ既定の定数にしたいとき |
it.name.equals(s, ignoreCase = true) |
null | 大文字小文字を区別せずに探すとき |
enumValueOrNull<Size>(s)(自作) |
null | 複数の enum で共通に使いたいとき |
valueOf は大文字小文字も区別します。 "small" を渡すと No enum constant Size.small になります。入力をそのまま渡す場合は、前後の空白を trim で取り、ignoreCase で比べる形にすると安心です。
プロパティ・メソッド・抽象メソッド・interface 実装
enum class はクラスなので、コンストラクタ引数、プロパティ、メソッドを持てます。注文の状態を例に、よく使う機能を1つにまとめます。
interface Labeled {
val label: String
fun display(): String = "[$label]"
}
// プロパティ・メソッド・抽象メソッド・interface 実装をまとめた例
enum class OrderStatus(val code: Int, override val label: String) : Labeled {
RECEIVED(10, "受付") {
override fun next() = PACKING
},
PACKING(20, "梱包中") {
override fun next() = SHIPPED
},
SHIPPED(30, "発送済み") {
override fun next() = DELIVERED
override fun display() = "<<$label>>" // 定数ごとに上書きもできる
},
DELIVERED(40, "配達完了") {
override fun next() = this
}; // メンバーを続けるときはセミコロン
abstract fun next(): OrderStatus // 定数ごとに実装する
val isFinal: Boolean get() = this == DELIVERED // 共通のプロパティ
fun canCancel(): Boolean = this < SHIPPED // 共通のメソッド(宣言順で比較)
companion object {
private val byCode = entries.associateBy { it.code }
fun fromCode(code: Int): OrderStatus? = byCode[code]
}
}
fun main() {
for (s in OrderStatus.entries) {
println("${s.name} code=${s.code} ${s.display()} next=${s.next()} cancel=${s.canCancel()} final=${s.isFinal}")
}
println(OrderStatus.fromCode(20))
println(OrderStatus.fromCode(99))
println(OrderStatus.fromCode(99)?.label ?: "不明なコード")
val labeled: Labeled = OrderStatus.RECEIVED
println(labeled.display())
}
RECEIVED code=10 [受付] next=PACKING cancel=true final=false
PACKING code=20 [梱包中] next=SHIPPED cancel=true final=false
SHIPPED code=30 <<発送済み>> next=DELIVERED cancel=false final=false
DELIVERED code=40 [配達完了] next=DELIVERED cancel=false final=true
PACKING
null
不明なコード
[受付]
| 機能 | 書き方 | この例での箇所 |
|---|---|---|
| プロパティ | enum class X(val code: Int) と書き、定数に A(10) と値を渡す |
code、label |
| 共通のメソッド | 定数の並びの後ろに ; を置いてから fun を書く |
canCancel() |
| 計算で求めるプロパティ | val p: T get() = … |
isFinal |
| 抽象メソッド | abstract fun を宣言し、定数ごとに { override fun … } |
next() |
| interface 実装 | enum class X : Labeled |
label、display() |
| 定数ごとの上書き | 定数の { } の中で override |
SHIPPED の display() |
| 逆引き | companion object に Map を持つ | fromCode() |
押さえておきたい決まり
- 定数の後ろにメンバーを書くときは、最後の定数のあとにセミコロンを置きます。定数だけの enum では不要です。
- 抽象メソッドは全定数で実装が必要です。実装のない定数 B があると
enum entry 'Step.B' does not implement abstract memberというコンパイルエラーになり、定数を足したときの書き忘れに気づけます。 - enum class は interface を実装できますが、クラスは継承できません(Kotlin 公式ドキュメントに記載)。
- 宣言順がそのまま大小関係になります。
this < SHIPPEDは「SHIPPED より前に宣言された定数か」を意味します。順番を入れ替えると結果も変わるので、順番に意味を持たせる場合はコメントで残しておくと親切です。
コード値からの逆引き
データベースや API では、状態を 10・20 のようなコード値でやり取りすることがあります。コードから定数を得るには、companion object に Map を1回だけ作っておきます。
companion object {
private val byCode = entries.associateBy { it.code }
fun fromCode(code: Int): OrderStatus? = byCode[code]
}
associateBy は「コード → 定数」の Map を作ります。戻り値を OrderStatus? にしておくと、無いコードが来たときに null になり、呼び出し側は ?: で既定の動きを書けます(上の出力の「不明なコード」)。
保存や通信には、ordinal ではなく自分で決めたコード値か name を使うのがおすすめです。ordinal は定数の並び順で決まるので、途中に定数を足すと番号がずれます。
when で分岐する:網羅エラーとインスタンス化のエラー
enum を when で分岐すると、全定数を書いたかどうかをコンパイラが確かめてくれます。MEDIUM の分岐を書かずにコンパイルします。
enum class Size { SMALL, MEDIUM, LARGE }
fun price(size: Size): Int = when (size) {
Size.SMALL -> 300
Size.LARGE -> 500
}
fun main() {
println(price(Size.SMALL))
val s = Size()
}
Price.kt:3:30: error: 'when' expression must be exhaustive. Add the 'MEDIUM' branch or an 'else' branch.
fun price(size: Size): Int = when (size) {
^^^^
Price.kt:10:13: error: cannot access 'constructor(): Size': it is private in 'Size'.
val s = Size()
^^^^
Price.kt:10:13: error: enum types cannot be instantiated.
val s = Size()
^^^^^^
- 1つ目:足りない定数の名前(MEDIUM)がエラー文に出ます。その分岐を足せば解決します。else を書いても通りますが、定数を足したときに気づけなくなるので、全定数を並べる書き方がおすすめです。
- 2・3つ目:enum class は自分でインスタンスを作れません。定数として宣言したものだけが存在します。
toString の上書き・集計・EnumMap
最後に、日々の作業でよく使う操作をまとめます。
import java.util.EnumMap
import java.util.EnumSet
enum class Size(val cm: Int) {
SMALL(60), MEDIUM(80), LARGE(100);
override fun toString(): String = "${name.lowercase()}(${cm}cm)"
}
enum class Day { MON, TUE, WED, THU, FRI, SAT, SUN }
fun price(size: Size): Int = when (size) {
Size.SMALL -> 300
Size.MEDIUM -> 400
Size.LARGE -> 500
}
fun main() {
// toString を上書きしても name は変わらない
println(Size.SMALL)
println(Size.SMALL.name)
println(Size.valueOf("SMALL"))
// when(else なしで全部の定数を書く)
println(Size.entries.map { price(it) })
// 集計・絞り込み・並べ替え
println(Size.entries.filter { it.cm >= 80 })
println(Size.entries.maxBy { it.cm })
println(Size.entries.associate { it.name to it.cm })
println(listOf(Size.LARGE, Size.SMALL, Size.MEDIUM).sorted())
// enum をキーにした集計
val orders = listOf(Size.SMALL, Size.LARGE, Size.SMALL, Size.SMALL)
println(orders.groupingBy { it }.eachCount())
val stock = EnumMap<Size, Int>(Size::class.java)
stock[Size.LARGE] = 3
stock[Size.SMALL] = 12
println(stock) // 入れた順でなく宣言順に並ぶ
// 範囲とセット
val weekend = EnumSet.of(Day.SAT, Day.SUN)
println(Day.FRI in weekend)
println(Day.entries.filter { it in Day.TUE..Day.THU })
println(Day.entries[(Day.SUN.ordinal + 1) % Day.entries.size])
}
small(60cm)
SMALL
small(60cm)
[300, 400, 500]
[medium(80cm), large(100cm)]
large(100cm)
{SMALL=60, MEDIUM=80, LARGE=100}
[small(60cm), medium(80cm), large(100cm)]
{small(60cm)=3, large(100cm)=1}
{small(60cm)=12, large(100cm)=3}
false
[TUE, WED, THU]
MON
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| 表示用の文字列を変える | override fun toString() |
name と valueOf は元の定数名のまま |
| 条件で絞る | entries.filter { … } |
List の関数がそのまま使える |
| 最大の定数 | entries.maxBy { it.cm } |
プロパティで比べる |
| 名前と値の Map | entries.associate { it.name to it.cm } |
一覧表示に便利 |
| 宣言順に並べる | list.sorted() |
enum は Comparable |
| 定数ごとに数える | list.groupingBy { it }.eachCount() |
出てきた順の Map になる |
| enum をキーにした Map | EnumMap<Size, Int>(Size::class.java) |
入れた順でなく宣言順に並ぶ |
| 範囲で絞る | it in Day.TUE..Day.THU |
宣言順の範囲 |
| 次の定数(末尾の次は先頭) | entries[(ordinal + 1) % entries.size] |
曜日の繰り上がりなど |
toString を上書きしても name は変わりません。 表示は toString、保存や変換は name、と役割を分けて使えます。
enum class が向いている場面の目安
| 状況 | 選ぶもの |
|---|---|
| 値の種類が固定で、全部が同じ形のデータを持つ | enum class |
| 一覧を出したい、文字列やコードから変換したい | enum class |
| 種類ごとに持つデータの形が違う(成功なら結果、エラーなら理由など) | sealed class / sealed interface |
| 利用者があとから値を増やせるようにしたい | 通常のクラスや interface |
つまずきやすい点の一覧
| 起きること | 表示(今回の環境での文言) | 対応 |
|---|---|---|
| valueOf に無い名前を渡した | IllegalArgumentException: No enum constant Size.XL |
entries.find で null を受け取る |
| 小文字で valueOf を呼んだ | No enum constant Size.small |
ignoreCase = true で比べる |
| when に定数が足りない | 'when' expression must be exhaustive. Add the 'MEDIUM' branch or an 'else' branch. |
表示された定数の分岐を足す |
| 抽象メソッドを実装していない定数がある | enum entry 'Step.B' does not implement abstract member |
その定数に { override fun … } を足す |
Size() と書いた |
enum types cannot be instantiated. |
定数を使う |
| enumEntries が見つからない | (import を忘れている) | import kotlin.enums.enumEntries を足す |
| entries に要素を足そうとした | MutableList へのキャストで ClassCastException | 読み取り専用として使う。変更したいときは toMutableList() でコピーする |
| ordinal を保存していて、定数を足したら番号がずれた | (エラーではない) | コード値のプロパティか name を保存する |
よくある質問
Q. values() と entries はどちらを使えばよいですか? A. entries がおすすめです。values() は呼ぶたびに新しい配列を作りますが、entries は毎回同じ読み取り専用リストを返します。List なので filter や find もそのまま使えます。Kotlin 1.9.0 以降で使えます。
Q. 文字列から enum に、例外なしで変換するには?
A. Size.entries.find { it.name == 文字列 } と書くと、無い名前のときに null が返ります。既定値にしたいときは ?: Size.MEDIUM を続けます。valueOf をそのまま使うなら runCatching { Size.valueOf(文字列) }.getOrNull() でも同じ結果になります。
Q. enum class にメソッドやプロパティを持たせられますか? A. 持たせられます。コンストラクタ引数でプロパティを定義し、定数の並びの最後にセミコロンを置いてからメソッドを書きます。抽象メソッドを宣言して定数ごとに実装を変えることも、interface を実装することもできます。
Q. name と toString() は何が違いますか? A. name は宣言した定数名そのもので、変えられません。toString() は既定で name と同じ文字列を返しますが、上書きできます。valueOf が使うのは name のほうです。
まとめ
- 全定数の一覧は entries。毎回同じ読み取り専用リストが返り、List の関数がそのまま使える
- values() は呼ぶたびに新しい配列を作る。置き換えは
values()をentriesにするだけで済む場面が多い - valueOf は無い名前で IllegalArgumentException(No enum constant …)。大文字小文字も区別する
- null で受けたいときは entries.find。valueOf を使うなら runCatching と getOrNull
- プロパティ・メソッド・抽象メソッド・interface 実装を持てる。メンバーを書くときは定数の最後にセミコロン
- コード値からの逆引きは companion object の Map。保存には ordinal でなくコード値か name
- when は else なしで全定数を書くと、定数を足したときにコンパイラが場所を教えてくれる
参考・出典
- Kotlin 公式ドキュメント「Enum classes」(entries、valueOf、enumEntries、Comparable、interface 実装): https://kotlinlang.org/docs/enum-classes.html
- Kotlin 公式ドキュメント「What's new in Kotlin 1.9.0」(entries の安定版化): https://kotlinlang.org/docs/whatsnew19.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin enum classの使い方 entries・valueOf一覧」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。