How do Kotlin's visibility modifiers map onto JVM bytecode access flags, and why does that matter?
answer
- JVM has no internal; Kotlin has no package-private
- internal → public + mangled name ($module)
- private can become package-private via synthetic accessors
- Enforced at compile time, not by the JVM
- Not a security boundary against Java/reflection
basics
~20 sKotlin enforces visibility at compile time, then compiles to JVM access flags: public stays public, protected stays protected, but private members are sometimes public in bytecode, and internal becomes public with a mangled name. So Java can sometimes reach what Kotlin hides.
solid answer
~50 sThe JVM has only `public`, `protected`, package-private, and `private` access flags — it has **no `internal`** concept. Kotlin enforces its richer visibility model at **compile time**, then lowers to JVM flags as best it can. `public` → `public`; `protected` → `protected`. **`internal`** → `public` bytecode but **name-mangled** (e.g., `foo$module`) so Java can technically call it but is discouraged. **`private`** members usually map to JVM `private`, but a `private` member accessed by a same-file inline function, lambda, or nested class may be widened to package-private/`public` with a **synthetic accessor** (Kotlin used to generate `access$` bridges; modern Kotlin can also relax the flag). Top-level `private` declarations live in a `FileKt` class as `private`/package-private. The practical consequences: Java interop can bypass Kotlin's encapsulation, reflection sees the relaxed flags, and library authors should not rely on `internal`/`private` as a *security* boundary against JVM-level callers — only as a Kotlin-language contract.
code
kotlin · 10 lines// Kotlin
class Account(internal val number: String) {
private val pin: String = "0000"
inline fun describe(block: (String) -> Unit) = block("acct")
}
// What the JVM roughly sees:
// public getNumber$mymodule() <- internal, mangled
// private final pin <- private (may relax if captured by inline/lambda)
// Java code can call getNumber$mymodule(); Kotlin code in another module cannot call number.go deeper
Knows visibility is enforced by the Kotlin compiler.
Knows the JVM lacks internal and that internal compiles to public bytecode.
Explains name mangling for internal and that private can be relaxed for inline/lambda capture.
Reasons about Java-interop leakage, reflection/serialization implications, binary compatibility of mangled names, and why visibility is a language contract, not a security control.
## The mismatch: Kotlin's model vs the JVM's model Kotlin has four visibilities: `public`, `internal`, `protected`, `private`. The **JVM** has four *different* ones: `public`, `protected`, **package-private** (no keyword), `private`. They don't line up — most notably the JVM has **no `internal`** and Kotlin has **no package-private**. So Kotlin **enforces visibility in the compiler** and then emits the closest JVM **access flags**. ## The mapping | Kotlin | JVM bytecode | Notes | |---|---|---| | `public` | `public` | direct | | `protected` | `protected` | direct (but Java's protected adds package access, so Java callers see slightly more) | | `internal` | `public` + **mangled name** | e.g. `transfer$app`; Kotlin enforces at compile time only | | `private` member | usually `private` | may be **relaxed** when captured by inline functions, lambdas, or nested classes in the same file | | top-level `private` | lives in `XxxKt` class, `private`/package-private | file-scoped | ## Why `private` sometimes isn't `private` in bytecode Kotlin features like **inline functions**, **lambdas**, **local/nested classes**, and **companion objects** may need to read a `private` member from generated bytecode in a *different* synthetic class. Historically the compiler emitted **synthetic accessor methods** (named `access$...`) or widened the field's flag to package-private so the generated class could reach it. So a Kotlin `private` field can appear as package-private/`public` to the JVM, and reflection (`java.lang.reflect`) will report the relaxed flag. ```kotlin class Wallet(private var cents: Long) { // The inline lambda below may force 'cents' to a synthetic accessor fun spend(amount: Long) = run { cents -= amount } } ``` ## Why `internal` is mangled, not hidden Because the JVM can't express 'visible within this module', the compiler keeps `internal` **`public`** so other files in the module can link to it, but **mangles the name** (appends `$moduleName`) to make accidental Java calls unlikely and to avoid signature clashes. Hence: **Kotlin enforces `internal` only at compile time**; Java/JVM tooling can still reach it. ## Why this matters 1. **Java interop leaks encapsulation.** Java code or reflection can reach `internal` (mangled) and sometimes `private` (relaxed) members. Never treat Kotlin visibility as a **security** boundary at the JVM level. 2. **Reflection / serialization frameworks** observe the bytecode flags, not the Kotlin ones — relevant for libraries that scan fields. 3. **API stability**: mangled `internal` names embed the module name, so renaming a module changes the symbol — a binary-compatibility consideration for published JARs. 4. **`@JvmName` / `@JvmStatic`** and friends interact with how names surface to Java, which compounds with mangling. ## Recall - JVM has package-private, **no** internal; Kotlin has internal, **no** package-private. - internal → public + name mangling, compile-time enforced only. - private can be relaxed (synthetic `access$` / flag widening) when inline/lambda/nested code needs it. - Visibility is a language contract, **not** a JVM-level security barrier.
- Can Java code read a Kotlin internal property?Yes, by using the mangled accessor name (e.g., getX$module). Kotlin only enforces internal at compile time; the bytecode is public.
- Why might a Kotlin private field show as non-private to reflection?Inline functions, lambdas, or nested classes in the same file may need access from generated classes, so the compiler relaxes the flag or adds a synthetic access$ method.
- Is Kotlin visibility a reliable security control?No. It's a compile-time language contract; Java interop and reflection can bypass it, so don't rely on private/internal to hide secrets at runtime.
Kotlin visibility is a company's internal org chart (compile-time rules); the JVM only has building keycards (access flags) that don't capture the chart, so some doors are technically unlocked to Java.
saying these in an interview costs you the question
- Claiming the JVM has a native internal access flag
- Asserting Kotlin private is always JVM-private with no exceptions
- Treating internal/private as a runtime security boundary
- Not knowing internal names are mangled
- Thinking reflection sees Kotlin-level visibility rather than bytecode flags