skip to content

Is `SomeClass::class.java.kotlin === SomeClass::class` guaranteed? What identity guarantees do the .java/.kotlin conversions give?

level: middleimportance: nice to knowfreq 20%

answer

  1. Compare KClass with ==, not ===
  2. java.lang.Class is canonical per classloader (=== holds)
  3. Round-trip KClass->Class->KClass is == equal
  4. Class identity = (name, classloader)
  5. .java/.kotlin are cheap; member reflection needs kotlin-reflect

basics

~10 s

Round-tripping a class through .java and back to .kotlin gives you an equal KClass, and comparing with == works. The conversions are cheap lookups, not new copies of your data.

solid answer

~40 s

`KClass` instances compare correctly by `==` (equals/hashCode are well-defined and based on the underlying type), so `SomeClass::class == SomeClass::class.java.kotlin` is reliably true. Reference identity (`===`) of the *KClass wrapper* is **not** something to rely on — Kotlin may hand out different `KClassImpl` wrapper instances for the same class, so prefer `==`. On the Java side, `java.lang.Class` objects *are* canonical per (class, classloader): `SomeClass::class.java === SomeClass::class.java` holds because a classloader returns the same Class object. The `.java`/`.kotlin` properties are lightweight — `.java` reads an already-resolved Class handle, and `.kotlin` wraps an existing Class — neither performs heavy parsing (that only happens when you touch members, which needs kotlin-reflect).

code

kotlin · 6 lines
kotlin
val k1 = String::class
val k2 = k1.java.kotlin
println(k1 == k2)   // true  (equals)
// k1 === k2 is not guaranteed

println(String::class.java === String::class.java) // true (canonical Class)

go deeper

for a junior

Knows round-tripping a class and comparing with == gives a match and conversions are cheap.

for a middle

Distinguishes == from === for KClass and knows java.lang.Class is canonical per classloader.

for a senior

Explains classloader-scoped identity and why frameworks key maps on Class, plus the lazy/kotlin-reflect cost boundary.

for a principal

Connects classloader identity to hot-reload/app-server ClassCastException pitfalls and chooses stable keys deliberately.

## What round-tripping means Starting from a Kotlin class, you can go `KClass -> Class -> KClass`: ```kotlin val k1 = String::class val k2 = k1.java.kotlin k1 == k2 // true (use equals) k1 === k2 // not guaranteed (wrapper identity) ``` ## Equality vs reference identity - **`==` (equals):** `KClass` defines a sensible `equals`/`hashCode` keyed on the underlying JVM type, so two KClass objects describing the same class are **equal**. Always compare KClasses with `==`. - **`===` (reference identity):** Kotlin does **not** promise to return the *same* `KClass` wrapper object every time. The reflection layer can create fresh wrapper instances, so `===` on KClasses is fragile — don't depend on it. ## The Java side is canonical `java.lang.Class` instances are **canonical per classloader**: a given classloader loads a class once and always returns that same `Class` object. Therefore: ```kotlin String::class.java === String::class.java // true ``` This is why frameworks safely use `Class` objects as map keys. If you need identity-based lookups, the `Class` (not the KClass wrapper) is the stable key. ## Classloader caveat "Same class" is really "same (binary name, defining classloader)". The same `.class` loaded by two different classloaders produces two **unequal** `Class` objects (and two unequal KClasses). This is the classic source of `ClassCastException` in app servers / hot-reload setups. ## Cost of the conversions Both properties are cheap: - `.java` returns the JVM Class handle the runtime already has — no parsing. - `.kotlin` constructs/looks up a thin KClass wrapper around an existing Class. The expensive part of Kotlin reflection — reading members, supertypes, annotations from the `@Metadata` — happens lazily and **requires the kotlin-reflect artifact**. Plain `::class`, `.java`, and `.kotlin` are stdlib-only. ## Takeaway Round-trip safely with `==`; treat the underlying `java.lang.Class` as the identity-stable key; remember classloaders define class identity.

  • Should you use == or === to compare two KClass values?
    Use ==. KClass defines equals/hashCode by the underlying type; === wrapper identity isn't guaranteed.
  • When can two Class objects for the 'same' class be unequal?
    When loaded by different classloaders — class identity is (binary name, defining classloader), a common cause of ClassCastException.

Two photocopies of the same ID read identical (==), but they aren't the literal same sheet (===); the government's master record (java.lang.Class) is the one canonical original.

saying these in an interview costs you the question

  • Relying on === to compare KClass instances
  • Assuming the same .class always yields one Class regardless of classloader
  • Thinking .kotlin re-parses metadata (it doesn't; member access does)
  • Believing round-trip breaks equality

context