- Kotlin
Kotlin sealed classの使い方 enumとの違いとwhen網羅
Kotlin の sealed class(シールドクラス)とは、「この型を継承できるのは、ここに書いたものだけ」とコンパイラに伝えるクラスです。 サブクラスの種類がコンパイル時に確定するので、when で全種類を分岐すると else なしで書け、種類を増やしたときに直す場所をコンパイラが教えてくれます。
この記事では、enum class・sealed class・sealed interface の違いを1枚の表にまとめたうえで、分岐を1つ消したときのエラー文、else を書いたときの挙動、object と data object の表示の違いを実際にコンパイル・実行して示します。後半では Loading / Success / Error の状態管理を、画面を使わない Kotlin だけのコードで組み立てます。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。
enum・sealed class・sealed interface の早見表
| 比べる点 | enum class | sealed class | sealed interface |
|---|---|---|---|
| 表すもの | 決まった個数の「値」 | 決まった種類の「型」 | 決まった種類の「型」 |
| インスタンスの数 | 定数1つにつき1個で固定 | 型ごとに何個でも作れる(object なら1個) | 型ごとに何個でも作れる(object なら1個) |
| 持てるデータ | 全定数が同じプロパティ構成 | サブクラスごとに別々のプロパティ | 実装ごとに別々のプロパティ |
| 親が状態を持てるか | 持てる(コンストラクタ引数) | 持てる(コンストラクタ引数・プロパティ) | 持てない(抽象プロパティの宣言は可) |
| 継承 | クラスは継承できない。interface は実装できる | サブクラスは親クラスを1つだけ継承 | 1つの型が複数の sealed interface を実装できる |
| enum が子になれるか | ― | なれない | なれる |
全種類の一覧 entries |
ある | ない | ない |
name / ordinal / valueOf |
ある | ない | ない |
| when の網羅チェック | 効く(定数を全部書く) | 効く(サブクラスを全部書く) | 効く(実装を全部書く) |
| サブクラスを置ける場所 | ―(定数は本体の中だけ) | 同じパッケージ・同じモジュール | 同じパッケージ・同じモジュール |
| 向いている例 | サイズ、曜日、権限の種類 | 図形、木構造のノード | 画面の状態、処理結果、イベント |
選び方の目安は「種類ごとに持たせたいデータが違うか」です。 全部同じ形なら enum class、種類によって違うなら sealed を選びます。sealed class と sealed interface の使い分けは後半の判断表にまとめました。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin sealed classの使い方 enumとの違いとwhen網羅」
まず動かす:3つを同じファイルで比べる
次のコードを Main.kt という名前で保存し、そのままコンパイルして実行できます。
enum class Level { LOW, HIGH }
sealed class Shape {
data class Circle(val r: Double) : Shape()
data class Rect(val w: Double, val h: Double) : Shape()
data object Empty : Shape()
}
sealed interface Event
data class Clicked(val x: Int, val y: Int) : Event
data class Typed(val text: String) : Event
object Closed : Event
data object Opened : Event
fun area(s: Shape): Double = when (s) {
is Shape.Circle -> Math.PI * s.r * s.r
is Shape.Rect -> s.w * s.h
Shape.Empty -> 0.0
}
fun main() {
// enum:決まった個数のインスタンスが1つずつ
println(Level.entries)
println(Level.valueOf("LOW") === Level.LOW)
// sealed class:同じ型のインスタンスをいくつでも作れ、型ごとに持つデータが違う
val shapes = listOf(Shape.Circle(1.0), Shape.Circle(2.0), Shape.Rect(2.0, 3.0), Shape.Empty)
for (s in shapes) println("$s -> ${"%.2f".format(area(s))}")
// object と data object の toString
println(Closed.toString().substringBefore('@') + "@(ハッシュ値)")
println(Opened)
println(Shape.Empty)
}
kotlinc Main.kt -include-runtime -d main.jar
java -Dstdout.encoding=UTF-8 -jar main.jar
-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です。
[LOW, HIGH]
true
Circle(r=1.0) -> 3.14
Circle(r=2.0) -> 12.57
Rect(w=2.0, h=3.0) -> 6.00
Empty -> 0.00
Closed@(ハッシュ値)
Opened
Empty
読み取れることは3つあります。
- enum の定数は1個ずつ。
valueOf("LOW")で取り出したものとLevel.LOWは同じインスタンス(===が true)です。 - sealed class のサブクラスは何個でも作れる。
Circle(1.0)とCircle(2.0)は別の値を持つ別のインスタンスで、RectはCircleと違うプロパティを持っています。 - area 関数の when に else がない。Shape のサブクラスは Circle・Rect・Empty の3つだけだとコンパイラが知っているためです。
サブクラスの書き方は3通り
| 書き方 | 使う場面 | 例 |
|---|---|---|
data class |
値を持つ種類。equals・toString・copy が自動で付く | data class Circle(val r: Double) : Shape() |
data object |
値を持たない種類(1個だけあればよい) | data object Empty : Shape() |
class |
中身で等しさを比べない種類、状態を変えていく種類 | class Session(var count: Int) : Shape() |
sealed class を継承するときは : Shape() とかっこを付けてコンストラクタを呼びます。sealed interface を実装するときは : Event とかっこなしで書きます。
サブクラスは、Shape のように親の中に入れ子で書いても、Event のように外に並べて書いてもかまいません。入れ子にすると Shape.Circle と親の名前つきで呼ぶことになり、どの階層の型かが名前から読み取れます。
object と data object:toString の違い
先ほどの出力の最後の3行が、object と data object の違いです。
| 宣言 | println の表示 | equals |
|---|---|---|
object Closed : Event |
Closed@ に続けてハッシュ値(実行のたびに変わる) |
参照が同じかどうか |
data object Opened : Event |
Opened |
同じ型なら等しい |
値を持たない種類は data object で書くと、ログに出したときに名前だけが表示されます。 data class と並べたときの見た目もそろいます。data object は Kotlin 1.9.0 で安定版になりました。Kotlin 公式ドキュメントによると、data object には toString・equals・hashCode が生成され、copy と componentN は生成されません(1個しかないものを複製したり分解したりする必要がないためです)。
サブクラスを置ける場所:同じパッケージ・同じモジュール
sealed class / sealed interface の直接のサブクラスは、親と同じパッケージ、かつ同じモジュールに置きます。ファイルは分かれていてもかまいません。
model/Shape.kt:
package shop.model
sealed interface Notice
sealed class Shape
class Square : Shape()
model/Hexagon.kt(別ファイル・同じパッケージ):
package shop.model
// 別のファイルでも、同じパッケージ・同じモジュールなら継承できる
class Hexagon : Shape()
fun main() {
val shapes: List<Shape> = listOf(Square(), Hexagon())
println(shapes.map { it::class.simpleName })
}
kotlinc model/Shape.kt model/Hexagon.kt -include-runtime -d main.jar
java -jar main.jar
[Square, Hexagon]
別ファイルの Hexagon も Shape を継承できました。次に、別のパッケージから継承してみます。
other/Other.kt:
package shop.other
import shop.model.Notice
import shop.model.Shape
class Triangle : Shape()
object Mail : Notice
fun main() {}
other/Other.kt:6:18: error: a class can only extend a sealed class or interface declared in the same package.
class Triangle : Shape()
^^^^^
other/Other.kt:7:15: error: a class can only extend a sealed class or interface declared in the same package.
object Mail : Notice
^^^^^^
別パッケージからの継承はコンパイルエラーになります。 この決まりがあるので、ライブラリの利用者が勝手に種類を増やすことはなく、when の網羅チェックが成り立ちます。
なお、sealed なのは「直接の」サブクラスだけです。たとえば Shape を継承した open class Polygon : Shape() を用意すれば、Polygon のサブクラスは別パッケージにも書けます。when では is Polygon の分岐がそれらをまとめて受け取ります。
sealed class そのものは new できない
sealed class は抽象クラスで、コンストラクタは既定で protected です(private にもできます。public と internal は指定できません)。直接インスタンスを作ろうとすると次のようになります。
sealed class Shape(val name: String) {
class Circle : Shape("circle")
}
fun main() {
val s = Shape("x")
}
NewShape.kt:5:13: error: cannot access 'constructor(name: String): Shape': it is protected in 'Shape'.
val s = Shape("x")
^^^^^
インスタンスを作るのは、必ずサブクラスの側です。
sealed に entries はない
enum の entries に当たる「全種類の一覧」は、sealed にはありません。
sealed interface UiState
data object Loading : UiState
data class Error(val message: String) : UiState
fun main() {
println(UiState.entries)
}
Entries.kt:5:21: error: unresolved reference 'entries'.
println(UiState.entries)
^^^^^^^
Error("…") のように値を持つ種類はインスタンスが無数にあり得るので、一覧にできないためです。選択肢をプルダウンに並べたい、文字列から変換したい、といった用途が中心なら enum class が合っています。
when の網羅:分岐を1つ消すとどうなるか
sealed の一番の利点は、when に書き忘れがあるとコンパイルが通らないことです。Loading・Success・Error の3状態のうち、Error の分岐を書かずにコンパイルします。
sealed interface UiState
data object Loading : UiState
data class Success(val items: List<String>) : UiState
data class Error(val message: String) : UiState
fun render(state: UiState): String = when (state) {
Loading -> "読み込み中…"
is Success -> "${state.items.size} 件を表示"
}
fun main() {
println(render(Loading))
}
UiState.kt:6:38: error: 'when' expression must be exhaustive. Add the 'is Error' branch or an 'else' branch.
fun render(state: UiState): String = when (state) {
^^^^
エラー文に 足りない分岐の名前(is Error) が出ます。その分岐を足せば解決します。
値を返さない「文」として書いた when でも、sealed と enum では同じチェックが働きます。
sealed interface UiState
data object Loading : UiState
data class Success(val items: List<String>) : UiState
data class Error(val message: String) : UiState
fun log(state: UiState) {
when (state) {
Loading -> println("loading")
}
}
enum class Level { LOW, MIDDLE, HIGH }
fun label(l: Level): String = when (l) {
Level.LOW -> "低"
Level.HIGH -> "高"
}
fun main() { log(Loading) }
Missing.kt:7:5: error: 'when' expression must be exhaustive. Add the 'is Error', 'is Success' branches or an 'else' branch.
when (state) {
^^^^
Missing.kt:12:31: error: 'when' expression must be exhaustive. Add the 'MIDDLE' branch or an 'else' branch.
fun label(l: Level): String = when (l) {
^^^^
| 足りない分岐 | エラー文に出る書き方 |
|---|---|
| data class / class | 'is Error' |
| data object / object | 'Empty'(is なし) |
| enum の定数 | 'MIDDLE' |
| 複数 | 'is Error', 'is Success' branches と並ぶ |
分岐の書き方:is を付けるもの、付けないもの
| サブクラスの種類 | 分岐の書き方 | 右側で使えるもの |
|---|---|---|
| data class / class | is Success -> |
state.items のように、その型のプロパティ(スマートキャスト) |
| data object / object | Loading ->(is Loading -> でも可) |
値を持たないので名前だけ |
| ネストした sealed の親 | is UiState.Error -> |
配下の種類をまとめて受け取る |
else を書くと、追加漏れに気づけなくなる
when に else を書いてもコンパイルは通ります。ただし、あとから種類を足したときの動きが変わります。Empty という状態を追加した例で比べます。
sealed interface UiState
data object Loading : UiState
data class Success(val items: List<String>) : UiState
data class Error(val message: String) : UiState
data object Empty : UiState // あとから追加した状態
// else あり:Empty を足してもコンパイルが通る
fun renderWithElse(state: UiState): String = when (state) {
Loading -> "読み込み中…"
is Success -> "${state.items.size} 件を表示"
else -> "エラー"
}
fun main() {
println(renderWithElse(Error("接続できません")))
println(renderWithElse(Empty))
}
エラー
エラー
Empty(0件)を渡したのに「エラー」と表示されました。 コンパイルは通り、警告も出ません。else が「まだ存在しない種類」まで受け取ってしまうためです。
同じ追加を、else なしの when に対して行うとこうなります。
sealed interface UiState
data object Loading : UiState
data class Success(val items: List<String>) : UiState
data class Error(val message: String) : UiState
data object Empty : UiState // あとから追加した状態
fun render(state: UiState): String = when (state) {
Loading -> "読み込み中…"
is Success -> "${state.items.size} 件を表示"
is Error -> "エラー: ${state.message}"
}
fun main() {
println(render(Empty))
}
AddEmpty.kt:7:38: error: 'when' expression must be exhaustive. Add the 'Empty' branch or an 'else' branch.
fun render(state: UiState): String = when (state) {
^^^^
| 書き方 | 種類を足したとき | 向いている場面 |
|---|---|---|
| else なしで全種類を書く | 直す場所がコンパイルエラーで全部わかる | sealed・enum を分岐するとき(基本はこちら) |
| else を書く | そのまま通り、else の処理が動く | 「残りは全部同じ扱い」と決めている場合 |
sealed を when で分岐するときは、else を書かずに全種類を並べるのが、sealed の利点をそのまま受け取る書き方です。
状態管理の実例:Loading / Success / Error を Kotlin だけで組む
「読み込み中・成功・エラー」は、sealed がよく合う題材です。3つの状態は同時には起こらず、状態ごとに持つデータが違うからです(成功なら結果、エラーなら理由)。
次のコードは、画面の代わりに状態を文字列へ変換して表示します。Error の中をさらに sealed interface で分け、ネストした階層にしています。
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
// Error の中をさらに sealed で分ける(ネストした階層)
sealed interface Error : UiState<Nothing> {
data class Network(val code: Int) : Error
data class Validation(val field: String, val reason: String) : Error
data object Unknown : Error
}
}
data class Article(val id: Int, val title: String)
// 画面の代わりに、状態を文字列にして表示する
fun render(state: UiState<List<Article>>): String = when (state) {
UiState.Loading -> "読み込み中…"
is UiState.Success -> state.data.joinToString(prefix = "記事: ") { it.title }
is UiState.Error.Network -> "通信エラー(コード ${state.code})。再試行してください"
is UiState.Error.Validation -> "${state.field} を見直してください:${state.reason}"
UiState.Error.Unknown -> "原因を特定できないエラーです"
}
// Error をまとめて扱いたい場所では、親の型で1行にできる
fun canRetry(state: UiState<*>): Boolean = when (state) {
UiState.Loading -> false
is UiState.Success -> false
is UiState.Error -> state is UiState.Error.Network
}
// 状態を持つクラス。状態が変わるたびに登録された関数を呼ぶ
class ArticleStore(private val loader: (String) -> List<Article>) {
var state: UiState<List<Article>> = UiState.Loading
private set
private val listeners = mutableListOf<(UiState<List<Article>>) -> Unit>()
fun observe(listener: (UiState<List<Article>>) -> Unit) {
listeners += listener
listener(state)
}
fun load(keyword: String) {
update(UiState.Loading)
val next = try {
if (keyword.isBlank()) UiState.Error.Validation("keyword", "1文字以上を入力")
else UiState.Success(loader(keyword))
} catch (e: java.io.IOException) {
UiState.Error.Network(503)
} catch (e: Exception) {
UiState.Error.Unknown
}
update(next)
}
private fun update(next: UiState<List<Article>>) {
state = next
listeners.forEach { it(next) }
}
}
fun main() {
val store = ArticleStore { keyword ->
when (keyword) {
"offline" -> throw java.io.IOException("timeout")
"bug" -> throw IllegalStateException("unexpected")
else -> listOf(Article(1, "$keyword 入門"), Article(2, "$keyword 応用"))
}
}
store.observe { println("[${it::class.simpleName}] ${render(it)} 再試行=${canRetry(it)}") }
for (k in listOf("Kotlin", "", "offline", "bug")) {
println("--- load(\"$k\")")
store.load(k)
}
}
[Loading] 読み込み中… 再試行=false
--- load("Kotlin")
[Loading] 読み込み中… 再試行=false
[Success] 記事: Kotlin 入門, Kotlin 応用 再試行=false
--- load("")
[Loading] 読み込み中… 再試行=false
[Validation] keyword を見直してください:1文字以上を入力 再試行=false
--- load("offline")
[Loading] 読み込み中… 再試行=false
[Network] 通信エラー(コード 503)。再試行してください 再試行=true
--- load("bug")
[Loading] 読み込み中… 再試行=false
[Unknown] 原因を特定できないエラーです 再試行=false
このコードのポイント
| 箇所 | 書き方 | ねらい |
|---|---|---|
UiState<out T> |
型引数に out を付ける | UiState<Nothing> の Loading や Error を、どの UiState<T> の変数にも入れられる |
Loading : UiState<Nothing> |
値を持たない状態は data object + Nothing | 1個のインスタンスを全部の型で共用できる |
sealed interface Error : UiState<Nothing> |
sealed の中に sealed を入れる | エラーの種類を細かく分けつつ、まとめても扱える |
| render の when | 末端の5種類を全部書く | 種類ごとに表示を変える。else なし |
| canRetry の when | is UiState.Error の1行で受ける |
エラーをまとめて扱う。配下に種類が増えても直さなくてよい |
var state + private set |
外からは読むだけ | 状態を変えられるのは ArticleStore の中だけになる |
ネストした階層では、when の分岐を「細かく書く」か「親でまとめる」かを場所ごとに選べます。 render のように表示を出し分けたい場所では末端まで書き、canRetry のようにエラーかどうかだけ知りたい場所では親の型で受けます。どちらの書き方でも網羅チェックは働きます。
Boolean と null 許容型で持つ場合との比較
同じ状態を isLoading: Boolean、data: List<Article>?、errorMessage: String? の3つのプロパティで表すこともできます。その場合、取り得る組み合わせは次のようになります。
| 表し方 | 取り得る組み合わせ | 意味のある組み合わせ |
|---|---|---|
| Boolean 1つ+null 許容型2つ | 2 × 2 × 2 = 8通り | 3通り |
| sealed interface | 3通り(細分化して5通り) | すべて |
「読み込み中なのにエラー文も入っている」といった組み合わせは、sealed ならそもそも作れません。状態を受け取る側は、あり得ない組み合わせへの備えを書かずに済みます。
sealed class と sealed interface の使い分け
sealed interface Result
sealed interface Retryable
// 1つの型が複数の sealed interface を実装できる
data class Timeout(val ms: Long) : Result, Retryable
data class Done(val value: Int) : Result
// enum も sealed interface を実装できる
enum class Fatal : Result { DISK_FULL, NO_PERMISSION }
fun describe(r: Result): String = when (r) {
is Timeout -> "${r.ms}ms で時間切れ"
is Done -> "結果 ${r.value}"
Fatal.DISK_FULL -> "空き容量がありません"
Fatal.NO_PERMISSION -> "権限がありません"
}
sealed class Node {
abstract val label: String
data class Leaf(override val label: String) : Node()
data class Branch(override val label: String, val children: List<Node>) : Node()
fun depth(): Int = when (this) {
is Leaf -> 1
is Branch -> 1 + (children.maxOfOrNull { it.depth() } ?: 0)
}
}
data object Idle
object PlainIdle
fun main() {
listOf(Timeout(3000), Done(42), Fatal.DISK_FULL, Fatal.NO_PERMISSION).forEach { println(describe(it)) }
val tree = Node.Branch("root", listOf(Node.Leaf("a"), Node.Branch("b", listOf(Node.Leaf("c")))))
println("depth=${tree.depth()}")
println(Timeout(1) == Timeout(1))
println(Idle)
println(PlainIdle.toString().startsWith("PlainIdle@"))
}
3000ms で時間切れ
結果 42
空き容量がありません
権限がありません
depth=3
true
Idle
true
このコードには、sealed interface でしかできないことが2つ、sealed class が向いていることが1つ入っています。
- Timeout は Result と Retryable の両方を実装している。sealed interface は1つの型にいくつでも付けられます。
- enum class の Fatal が Result を実装している。enum は他のクラスを継承できませんが、sealed interface なら実装できます。when では
Fatal.DISK_FULLのように定数まで含めて網羅チェックされます。 - Node は共通のメソッド depth() を親に持っている。親に実装やコンストラクタ引数を置きたいときは sealed class が向いています。
判断表
| 状況 | 選ぶもの |
|---|---|
| 種類が固定で、全部が同じ形のデータ | enum class |
| 一覧を取りたい、名前の文字列から変換したい | enum class |
| 種類ごとに持つデータが違う | sealed interface(まずこちら) |
| 1つの型を複数の分類に入れたい | sealed interface |
| enum や、すでに別のクラスを継承している型も仲間に入れたい | sealed interface |
| 親に共通のプロパティの実体・コンストラクタ引数・初期化処理を置きたい | sealed class |
| 利用者が自由に種類を増やせるようにしたい | 通常の interface / open class |
迷ったら sealed interface から始め、親に状態を持たせたくなったら sealed class にする、という順で考えると選びやすくなります。
つまずきやすい点とエラー文の一覧
| 起きること | 表示(今回の環境での文言) | 対応 |
|---|---|---|
| when に種類が足りない | 'when' expression must be exhaustive. Add the 'is Error' branch or an 'else' branch. |
表示された分岐を足す |
| 別パッケージから継承した | a class can only extend a sealed class or interface declared in the same package. |
親と同じパッケージに移す |
| sealed class を直接インスタンス化した | cannot access 'constructor(...)': it is protected in 'Shape'. |
サブクラスのインスタンスを作る |
| sealed に entries と書いた | unresolved reference 'entries'. |
一覧が必要なら enum class にする |
object の表示が Closed@… になる |
(エラーではない) | data object にする |
| 種類を足したのに既存の処理がそのまま動く | (エラーではない) | when の else を外して全種類を書く |
書くときのチェックリスト
- 値を持たない種類は data object にしたか
- 値を持つ種類は data class にして、プロパティは val にしたか
- when は else なしで全種類を書いたか
- 「まとめて扱う」単位があるなら、中間に sealed の親を置いたか
- サブクラスは親と同じパッケージに置いたか
よくある質問
Q. sealed class のサブクラスは、同じファイルに書く必要がありますか? A. 同じファイルでなくてもかまいません。直接のサブクラスは「同じパッケージ・同じモジュール」にあれば、別ファイルに書けます。この記事でも、別ファイルの Hexagon が Shape を継承できること、別パッケージからはコンパイルエラーになることを確かめました。
Q. sealed class と enum class はどう違いますか? A. enum class は決まった個数の「値」で、全定数が同じプロパティ構成です。sealed は決まった種類の「型」で、種類ごとに別のプロパティを持て、同じ種類のインスタンスを何個でも作れます。一覧(entries)や名前からの変換(valueOf)があるのは enum class だけです。
Q. when に else を書いてもよいですか? A. コンパイルは通ります。ただし、種類を足したときに else がそれを受け取ってしまい、直す場所をコンパイラが教えてくれなくなります。sealed や enum を分岐するときは、else を書かずに全種類を並べるのがおすすめです。
Q. data object と object はどちらを使えばよいですか?
A. sealed の種類として使うなら data object が便利です。println やログで Loading のように名前だけが表示され、data class と見た目がそろいます。
まとめ
- sealed は「種類が決まった型」。サブクラスの種類がコンパイル時に確定するので、when を else なしで書ける
- enum は同じ形の値、sealed は種類ごとに違うデータ。entries・valueOf があるのは enum だけ
- 分岐が足りないと 'when' expression must be exhaustive と表示され、足りない分岐の名前が出る
- else を書くと、種類を足しても通ってしまう。sealed の when は全種類を並べる
- 値を持たない種類は data object にすると、表示が名前だけになる
- サブクラスは同じパッケージ・同じモジュールに置く。ファイルは分けてよい
- 状態管理ではネストした sealed が役立つ。細かく分岐する場所と、親でまとめる場所を選べる
参考・出典
- Kotlin 公式ドキュメント「Sealed classes and interfaces」(サブクラスを置ける場所、コンストラクタの可視性、enum との関係): https://kotlinlang.org/docs/sealed-classes.html
- Kotlin 公式ドキュメント「Object declarations and expressions」(data object に生成される関数): https://kotlinlang.org/docs/object-declarations.html
- Kotlin 公式ドキュメント「What's new in Kotlin 1.9.0」(data object の安定版化): https://kotlinlang.org/docs/whatsnew19.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin sealed classの使い方 enumとの違いとwhen網羅」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。