What are the synthetic `access$` methods Kotlin generates, why do they appear, and what changed so that they are needed less often?
answer
- access$ = private-member bridge across class files
- Nested/lambda/companion are separate classes
- Synthetic + package-private, shows in reflection
- JDK 11 nestmates (NestHost/NestMembers) remove many
- Higher jvmTarget -> fewer access$
basics
~20 sWhen an inner/nested or companion context needs a private member of an outer class, Kotlin used to generate hidden access$ bridge methods because separate JVM classes can't touch each other's privates. Newer bytecode targets use nestmates instead, so fewer of these appear.
solid answer
~40 sOn the classic JVM access model, a `private` member is only reachable inside its own class file. But Kotlin nested classes, lambdas, `companion object`, and local functions each compile to *separate* classes, so when one of them reads a `private` field or calls a `private` method of an enclosing class, the compiler must synthesize a package-private bridge — historically named `access$getX$cp`, `access$callFoo`, etc. — on the owning class that forwards to the private member. These `access$` methods clutter Java completion and reflection. Since JDK 11 the JVM has the **nestmates** feature (NestHost/NestMembers attributes) letting classes in the same nest directly access each other's privates. With `-jvm-target 11+`, Kotlin emits nestmate attributes and can skip many synthetic accessors, reducing the noise. They're synthetic implementation details — never call them directly.
code
kotlin · 11 linesclass Cache {
private val store = HashMap<String, Int>()
private fun put(k: String, v: Int) { store[k] = v }
// anonymous object = separate class -> needs access$put pre-nestmates
val loader = object : Runnable {
override fun run() { put("warm", 1) }
}
}
// jvmTarget 8 -> Cache.access$put(this, ...) bridge generated
// jvmTarget 11+ -> nestmate direct access, no bridgego deeper
Recognizes access$ as compiler-generated and not real API.
Explains they bridge private members because nested/lambda classes are separate class files.
Connects them to JVM per-class privacy and JDK 11 nestmates / jvmTarget reducing them.
Advises on jvmTarget policy, ABI/perf tradeoffs, and how synthetic accessors interact with reflection-heavy tooling and proguard/R8 across modules.
## The root cause: privacy is per-class-file In classic JVM bytecode, `private` access is checked **per class**: only code physically in the same `.class` file may touch a `private` field or method. Kotlin, however, compiles many logical 'inside' constructs into **separate class files**: - nested/`inner` classes, - `companion object` (the `Companion` nested class), - lambdas / anonymous objects, - local functions, - closures capturing private state. When any of those reads a `private val`/`private fun` of the enclosing class, the JVM would reject the direct access. So the Kotlin compiler **synthesizes a bridge** on the owning class: ```kotlin class Counter { private var n = 0 private fun bump() { n++ } val incrementer = { bump() } // lambda is a separate class } ``` Conceptually the compiler adds to `Counter`: ```java // package-private synthetic bridges static void access$bump(Counter c) { c.bump(); } static int access$getN$p(Counter c) { return c.n; } static void access$setN$p(Counter c, int v) { c.n = v; } ``` The lambda class then calls `Counter.access$bump(this)` instead of the (inaccessible) private method. The `$cp`/`$p` suffixes encode whether it's a companion-property accessor, etc. ## Why it matters at the boundary - These `access$...` methods are **synthetic** and package-private but still show up in **reflection** (`getDeclaredMethods`) and sometimes **Java IDE completion**, looking like accidental API. - They are an implementation detail; calling them directly is unsupported and may break across compiler versions. - They slightly increase method count and can confuse tools that enumerate members (mockers, serializers, coverage). ## What reduced the need: JVM nestmates (JEP 181, JDK 11) JDK 11 added **nestmates**: a class declares a `NestHost` and the host lists `NestMembers`. Members of the same nest may access each other's `private` members **directly**, with no bridge. When you compile Kotlin with `-jvm-target 11` (or higher), the compiler emits these nest attributes for nested/companion/lambda classes, so it can **omit many synthetic accessors**. Targeting Java 8 bytecode keeps the old `access$` bridges. ## Practical takeaways - Seeing `access$...` in a stack trace or reflection dump is normal; it's not your code. - Bumping `jvmTarget` to 11+ cleans much of this up and can marginally improve performance (one fewer indirection). - Never depend on the exact name/shape of an `access$` method. ```kotlin // build.gradle.kts kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } // -> nestmates used, fewer access$ bridges generated ```
- Why can't the lambda just call the private method directly on JVM 8 bytecode?The lambda compiles to its own class file; classic JVM private access is per-class, so direct access is a verification/IllegalAccess error — hence the bridge.
- Does raising jvmTarget to 11 guarantee zero access$ methods?It removes most, but the compiler may still generate accessors in edge cases (e.g., certain inline/cross-module scenarios); nestmates cover same-nest access only.
access$ is a trusted courier: two separate buildings (class files) can't enter each other's private vault, so the owner posts a courier desk that fetches the private item on request.
saying these in an interview costs you the question
- Saying private members are always directly accessible from nested classes on the JVM
- Calling access$ methods a stable public API
- Not knowing nested/lambda/companion are separate class files
- Unaware of JDK 11 nestmates
- Confusing access$ accessors with $default dispatchers