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期連続の黒字決算。

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

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