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

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

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