Zygote
Java to Kotlinintermediate

Java, without the JVM

Android chose the Java language and then built its own virtual machine to run it. The decade of litigation that followed is a story about which part of Java is actually the product: the language, the library, or the API.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

Android is a Java platform with an asterisk. It compiles Java source, runs Java bytecode-derived files, and gives you a Java-like standard library — but it never shipped a Java Virtual Machine, and for a decade it did not ship the Java library either.

TL;DR#

  • Android chose the Java language, not the Java Virtual Machine. Dalvik executed DEX files; ART executes DEX files. There has never been a JVM on a phone.
  • The original class library was Apache Harmony, not OpenJDK. Android 7.0 switched the core libraries to OpenJDK-based code.
  • Google v. Oracle (Supreme Court, 2021) held that Google's use of the Java API declarations was fair use. The case was about declaring code, not implementation.
  • Java 8 language features reached old API levels through desugaring, not through a newer runtime.
  • Modern Android supports newer Java language levels, but the library API available depends on minSdk and on desugaring, not on the compiler alone.

Mental model#

Imagine a country that adopts English as its official language but writes its own legal code.

The language — grammar, spelling, idiom — is shared with the English-speaking world. Laws can be written in it, and any English speaker can read them.

The legal code — the actual statutes, the case law, the procedures — is written from scratch. Words that mean one thing in England have a locally defined meaning, and some statutes simply do not exist yet.

The courts — the runtime that executes the law — are also local. They were built to interpret this code, not someone else's.

Android adopted Java the language and wrote its own statutes and courts. The lawsuit was over whether copying the structure of someone else's statute book into a new one is lawful.

Where the analogy breaks#

Countries diverge slowly. Android's legal code was, for its first eight years, largely a reimplementation of someone else's — and the Supreme Court case turned on how much of the structure was copied. The analogy also breaks in the other direction: by 2016 Android had replaced much of its library with actual OpenJDK code, which is the opposite of a country writing its own statutes.

The big picture#

Loading diagram…
Text description of this diagram

A three-lane timeline from 2008 to 2026. The Java lane shows Java 8 language features via desugaring in 2017 and Java 11 and 17 API desugaring in 2021. The Kotlin lane shows Kotlin made first-class at I/O 2017, Kotlin-first at I/O 2019, coroutines as the async default in 2019, Compose announced in 2019, Compose 1.0 in 2021, the K2 compiler in 2024 and KMP first-party Android support in 2024. The platform lane shows Android 1.0 in 2008, Android 4.4 shipping in 2013 with ART as an experimental runtime, Android Studio announced in 2013, Android 7.0 moving to OpenJDK-based libcore in 2016, and the Supreme Court decision in Google v. Oracle in 2021.

The striking thing is how late the Java lane moves relative to the Kotlin lane. Java 8 language features arrived in 2017, the same year Kotlin was announced as first-class. Android's Java story was still being fixed when its Kotlin story began.

Deep dive#

Why not the JVM?#

In 2007 the JVM was a large, licensed piece of software with a memory model and a class library designed for servers. Android needed three things it did not provide:

  • A small runtime that could start an app process quickly on a device with 192 MB of RAM.
  • A register-based bytecode that maps well to ARM, rather than the JVM's stack machine.
  • A library it could ship without a licence from Sun.

Dalvik answered all three. It executed DEX, a register-based format derived from Java class files, with a class library built from Apache Harmony.1

The trade-off was compatibility. Code compiled against the JVM's class library would not necessarily run, because the library was a different implementation with different bugs. "Write once, run anywhere" became "write once, compile for Android, and find out."

The library question: Harmony to OpenJDK#

For years Android's java.lang, java.util and friends were Harmony-derived. This was a reimplementation of the API, and it diverged: some methods were missing, some behaved differently, and some were added only when an Android release shipped.

Android 7.0 changed this. The core libraries were replaced with OpenJDK-based code, which fixed a long list of small incompatibilities and made Android's java.* behave much more like a real JDK.1

That did not mean Android gained the JVM. The runtime was still ART, executing DEX. It only meant the library implementation underneath the API was now the same code the rest of the world used.

The lawsuit, in one paragraph and then more#

Oracle acquired Sun, and with it Java. Oracle sued Google over the declaring code of 37 Java API packages — the class names, method signatures and structure that Android's java.* and javax.* reproduced. Two lower courts disagreed with each other; the Supreme Court decided in 2021 that Google's use was a fair use, reversing the Federal Circuit.2

Why this matters to a working Android developer, even now:

  • It settled that reimplementing an API is not automatically infringement in this context. That is the legal basis on which any compatible reimplementation — including Android's — can exist.
  • It did not decide that APIs are never copyrightable. The Court assumed the declarations were copyrightable and decided the use was fair. That distinction is why the case is cited in every subsequent API-copyright argument.
  • It removed a cloud over the platform. For a decade, a ruling the other way could have forced Android to change its core libraries or pay for them.

Java 8, on API 21#

Suppose you want a lambda in an app with minSdk = 21. ART on API 21 does not understand invokedynamic the way a modern JVM does. The lambda still works, because the compiler and D8 rewrite it into something older.

This is desugaring: the toolchain translates newer language features and, later, a growing set of newer library APIs into forms the target API level supports. It happens at build time; the runtime never knows.

// The source uses java.time, added to Android in API 26.
// With core library desugaring, this runs on API 21 too.
val deadline = java.time.LocalDate.of(2026, 12, 31)
val days = java.time.temporal.ChronoUnit.DAYS.between(
    java.time.LocalDate.now(),
    deadline,
)

Core library desugaring is enabled explicitly in the build:

android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}
 
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}

The version number above is the library's, not the platform's; check the current release before copying it.3

What the toolchain accepts today#

Modern Android Gradle Plugin versions accept newer Java language levels for source and target, and the compiler and D8 handle the translation. Two separate limits apply:

  • Language features are generally available down to low minSdk values, because D8 desugars them.
  • Library APIs are available only at the API level that introduced them, or through core library desugaring for the subset it covers.

That split explains a very common confusion: code that compiles fine and crashes with NoSuchMethodError on an old device. The compiler was satisfied; the runtime was not.

Why the language level and the library level diverge#

The divergence is the single most confusing thing about Java on Android, and it has a clean explanation.

The language level is a compiler setting. The compiler can accept Java 17 syntax and emit bytecode that D8 can rewrite for an old runtime. Nothing about the device constrains what you may write.

The library level is a runtime fact. A method that does not exist in android.jar for your minSdk is not there on the device, no matter what the compiler accepted. Desugaring covers a curated subset — java.time, java.util.function, java.util.stream — and nothing outside it.

So var, records and switch expressions are compile-time concerns; java.time and streams are runtime concerns with a partial bridge. Keeping those two categories separate prevents most NoSuchMethodError surprises.

How it evolved#

2008–2014Dalvik + HarmonyDEX on a register VM; a reimplemented class library
2014–2019ART + OpenJDKART replaces Dalvik; the libraries become OpenJDK-based in 7.0
2017–nowDesugaring + Kotlinlanguage features backported at build time; Kotlin becomes first-class
Runtime changes9 releases
APIVersionWhat changed
11.0
  • Initial AOSP platform: Linux kernel, Dalvik VM, core libraries
82.2 Froyo
  • Trace-based JIT compiler added to Dalvik
92.3 Gingerbread
  • Concurrent garbage collector added to Dalvik (GC pause reduction)
194.4 KitKat
  • Android Runtime (ART) appears as an experimental runtime
215.0 Lollipop
  • ART becomes the only runtime: ahead-of-time compilation at install
236.0 Marshmallow
  • Runtime permission model changes how every app asks for access
247.0 Nougat
  • Hybrid JIT + AOT compilation in ART
3313 Tiramisu
  • The notification permission becomes a runtime permission
3717 Cinnamon Bun
  • Generational garbage collection is added to ART's Concurrent Mark-Compact collector

Under the hood#

  • the core Java libraries: java.lang, java.util, java.io and friends
  • the runtime that actually executes DEX
  • DexFile.javaloading a DEX file, the operation at the centre of the DEX story
  • the DEX file format implementation shared by the runtime and tools

Curiosity corner#

  • Dalvik is named after a village in Iceland, where the ancestor of its creator lived. ART, its successor, is not named after anything.
  • The 64K method limit is a DEX format limit, not an Android limit. One DEX file can reference 65,536 methods; multidex is the workaround.
  • java.time on API 21 is desugared, not emulated by a wrapper. Your code calls real implementations of the same classes, which is why behaviour matches rather than approximates.

Try it#

Which runtime is this device using?

adb shell getprop dalvik.vm.dex2oat-filter adb shell getprop dalvik.vm.heapgrowthlimit

Example output
art
32768
Check the SDK's Java level

javac -version

Example output
javac 17.0.9
See desugared classes in the APK

unzip -l app/build/outputs/apk/debug/app-debug.apk | grep classes

Example output
classes.dex
classes2.dex

Build it#

Set minSdk = 21 and use java.time in a small app. Build it, then remove the core library desugaring dependency and build again. The compile still succeeds; the app crashes on an API 21 device. That failure is the difference between language support and library support, demonstrated in thirty seconds.

Check yourself#

Check yourself

1 / 12

Why did Android choose the Java language but not the JVM?

0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsAndroid Runtime (ART) and Dalviksource.android.comaccessed 2026-10-27↩
  2. specGoogle LLC v. Oracle America, Inc., 593 U.S. (2021)Supreme Court of the United Statesaccessed 2026-10-27↩
  3. docsJava 8+ API desugaring supportdeveloper.android.comaccessed 2026-10-27↩
  4. docsConfigure your builddeveloper.android.comaccessed 2026-10-27↩
  5. wikiAndroid version historyWikipediaaccessed 2026-10-27↩