Kotlin Enum Classes and the Companion JVM Code
What the compiler adds to enum classes
Enum classes in Kotlin use a compact and readable syntax, but the compiled output is much more detailed. The compiler generates extra fields, synthetic methods, and bytecode that closely matches how Java handles enums. Today we go over how Kotlin enum classes are built, how the values()
and valueOf()
methods work, and how they appear when accessed through reflection. The focus is on how the pieces are assembled by the compiler to make the final result behave the way it does.
How Enum Classes Are Compiled in Kotlin
The compiler fills in the rest by generating fields, methods, and bytecode that follow the JVM enum model. Each enum maps directly to what the JVM expects, following the same structure used in Java. Kotlin enum classes end up producing final classes with static fields, synthetic helper methods, and a hidden array that backs most of the enum behaviors you get out of the box. All of this gets baked into the compiled bytecode. You’re not required to write any of it manually, but it’s there once the class is built.
Internal Representation
Every Kotlin enum class is compiled into a final class that extends java.lang.Enum
. Each value declared in the enum gets compiled as a public static final
field. The enum itself isn't abstract and can't be subclassed. Here's a basic example to work from:
The compiler takes this and produces a class with three constant fields, a private constructor, and a few utility methods. The Kotlin code gives no indication of the extra fields and logic, but they’re always added during compilation.
All enum constants are backed by a static array. That array is hidden and used internally by the values()
method that Kotlin automatically provides. Each constant is an object that's created by calling the private constructor for the enum, passing in a name and ordinal index. These are the same two arguments you’d get from Enum
in Java.
On the bytecode level, the class ends up shaped like this:
The $VALUES
field holds the constants in declaration order. It gets initialized statically when the enum class is loaded.
Constructor Behavior
Enum constructors in Kotlin can have parameters, just like any other class. Each constant is initialized with a call to the constructor, even if no arguments are shown in the enum declaration. This is required by the JVM. You can also include custom properties and even methods in the enum class body.
Each constant here passes a different string to the constructor. That string is stored in the extension
property, which becomes a final field. Kotlin compiles this into a constructor that looks roughly like this in bytecode:
The name
and ordinal
values come from the JVM’s enum system. Kotlin doesn’t handle them directly. They’re passed in automatically when each constant is created. Kotlin’s constructor parameters follow those built-in values.
Even when you don’t declare parameters in your Kotlin enum, the constructor still exists and takes the same two built-in arguments behind the scenes. You can also define functions on an enum class, and each constant will still follow the same static initialization flow. Kotlin doesn’t change that structure. It just lets you write a lot less of it manually.
Decompilation View
If you decompile a Kotlin enum class using javap
or a bytecode viewer in IntelliJ, you’ll see the structure unfold into something much closer to Java. This is helpful if you want to understand what the compiler builds without stepping through the raw bytecode.
Let’s say you have this enum:
After decompilation, the bytecode looks almost identical to:
That’s the structure Kotlin relies on. The clone()
method call in values()
makes a copy of the $VALUES
array to prevent any changes to it from outside code. All enum constants are created with their name and position in the declaration list. That ordinal value is zero-based and follows the order the constants appear in the file.
This layout is what allows valueOf()
to work, what gives the enum its ordinal
and name
properties, and what backs iteration when you call values()
in Kotlin code. There’s nothing Kotlin-specific in the final compiled output beyond the source annotations and possibly some metadata. At runtime, the enum behaves just like any other JVM enum. Kotlin leans into that design and builds the rest through the compiler’s front-end rules. All of it ends up inside the class, even if you never see it written.
How values() and valueOf() Are Generated and Used
Enum classes in Kotlin give you values()
and valueOf()
automatically. You don’t have to write either one, and the compiler blocks any attempt to override or redefine them. These methods aren’t special in the syntax, but they are part of what makes enum classes work the way they do in Kotlin and on the JVM. Both are added during compilation, and they follow the pattern expected by Java bytecode.
The values()
method returns all constants declared in the enum. The valueOf()
method returns a specific constant based on a string name. Both are treated as static methods and are tightly bound to how enums are built in compiled output.
Compiler-Generated Methods
Every enum class gets a values()
method that returns an array copy of the constants in declaration order. This method is synthetic. It’s not visible in your source code and doesn’t need to be declared. It always uses a cloned copy of a private backing array to avoid letting external code change the contents.
Here’s a basic Kotlin enum to work with:
You can call Mode.values()
in regular code, and it returns a snapshot of the constants. That array isn’t shared. Each call returns a new copy backed by the same source array inside the class.
The valueOf()
method works a little differently. It takes a string and returns the enum constant with a matching name. The name is case-sensitive, and it must exactly match how the constant appears in the enum. Otherwise, it throws an IllegalArgumentException
.
The string must match one of the declared constants. If it doesn’t, the exception will stop the program unless caught. There’s no fallback or default behavior built into valueOf()
.
Both methods are added to every enum class, and the compiler marks them as static. These match the method signatures the JVM expects for enums. That keeps enum classes usable from Java and allows Kotlin’s code model to stay compatible with older libraries and tooling.
Bytecode Overview
If you decompile an enum class, both values()
and valueOf()
will appear in the bytecode even though they were never typed out in the Kotlin file. They follow the same structure across all enum classes. Kotlin adds them consistently so that enums always expose these behaviors without extra work. In bytecode, values()
returns a copy of a private array field. That field holds the enum constants and is created during static initialization. Kotlin uses the same approach as Java, with a hidden $VALUES
array storing the constants.
The clone()
call is important because arrays are mutable. Without that, changes to the returned array could affect the internal state of the enum, which isn’t allowed. Returning a new copy avoids that problem.
The valueOf()
method in bytecode is a thin wrapper around the JVM’s built-in lookup. It uses Enum.valueOf()
to perform the actual match.
This relies on the class name being fixed and available at runtime. If you rename the enum or change the string, the method has to reflect that change, or it won’t match. There’s no case-insensitive variant provided out of the box, and you can’t override valueOf()
in a Kotlin enum even if you wanted to.
In newer versions of Kotlin (stable since 1.9 and introduced experimentally in 1.8), the compiler also includes a static field called ENTRIES
. This field supports the Enum.entries
property, which gives a read-only list of the constants. It exists alongside the existing $VALUES
array and does not change how values()
or valueOf()
work. The older behavior is still in place, and both versions produce the same constant values in the same order.
Reflection Access Patterns
Reflection can reach enum constants in Kotlin, either through Kotlin’s reflection tools or Java interop. Both work, and both see the enum as a compiled class with static constants.
Using Kotlin reflection:
This includes methods, constructors, and internal properties, not just the enum constants. If you only want the constants, it’s usually cleaner to switch to Java’s class model.
Using Java reflection:
This returns the declared constants as an array. It follows the same order used by the values()
method. These objects can then be used like any other enum instance, including accessing properties, calling methods, or comparing them.
Reflection can also get the value by name without calling valueOf()
directly. The lookup still depends on the exact name string, and if no constant matches, the expression just returns null
when you use firstOrNull
, so no exception is thrown.
This provides a way to do safe lookups without catching exceptions. It’s one of the few ways to bypass valueOf()
without risking a crash on a bad input.
Both reflection and compiler-generated methods behave predictably because Kotlin builds its enum support directly into the compilation process. The method names, static array, and field layout are always present in the final bytecode and follow a structure the JVM understands.
Conclusion
Kotlin enum classes are built to line up with what the JVM already expects. The compiler fills in the structure by generating static fields, synthetic methods, and internal arrays that match the Java enum model. Everything from constant creation to how values()
and valueOf()
behave is handled during compilation. The result is a predictable class with bytecode that runs cleanly on any JVM and works with reflection, tooling, and Java code without extra effort.