Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 28 – Java in practice: Android, upgrades and certification

Java on Android: what carries over

How Android builds and runs Java with D8, R8 and ART, why API levels decide which Java methods an app can call, and how to set the JDK in a build.

  • Intermediate
  • 18 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 18 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how Android builds and runs Java code (ART, D8/R8 and desugaring)
  • Identify which features of this track are available at Android's documented Java level
  • Configure the Java toolchain and sourceCompatibility in an Android build

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how Android builds and runs Java code (ART, D8/R8 and desugaring)
  • Identify which features of this track are available at Android's documented Java level
  • Configure the Java toolchain and sourceCompatibility in an Android build

Before you start

On this page

Android apps can be written in Java, but they do not run on a Java Virtual Machine. The build compiles your Java with javac, as it would for a server, and then D8 converts the class files into DEX bytecode for the Android Runtime (ART), rewriting some newer language features so that older phones can run them. R8 does the same job for release builds and also shrinks the code. Most of this track carries over: the language up to Java 17, the version that Android’s documentation pairs with Android 14 (API level 34), and the parts of java.lang, java.util and java.time that the phone’s Android version provides. What stays behind is everything tied to a JVM or a desktop: JavaFX and Swing, servlets, the module system, jlink and JVM flags. One rule replaces the Java version you are used to: on a phone, the API level decides which Java methods exist.

This lesson audits a small library that a shop’s server and its Android app share, checks it twice, once for Java 17 and once for an Android API level, and sets the JDKs of the build. Every command ran on Eclipse Temurin 25.0.4.1 and 17.0.20.1.

How Android builds and runs Java code

An Android build: javac of the toolchain JDK writes class files, D8 or R8 turns them into DEX, and ART runs the DEX on the device.Your .java filesand the shared libraryjavac of the toolchain JDKlanguage: sourceCompatibilityclass files: targetCompatibilityD8, or R8 in release builds.class files to DEX, desugaring,shrinking with R8APK or app bundleclasses.dex insideAndroid Runtime (ART) on the devicecore libraries of its API levelcompiled againstthe compileSdk API.class filesinstalls when the device'sAPI level is minSdk or higher

From Java source to the Android Runtime

Text description of the diagram

The diagram is a column of five boxes joined by arrows, from top to bottom.

  1. Your .java files and the shared library. An arrow labelled "compiled against the compileSdk API" leads to the next box.
  2. javac of the toolchain JDK, where sourceCompatibility sets the language level and targetCompatibility the class-file version. An arrow labelled ".class files" leads on.
  3. D8, or R8 in release builds, which turns the class files into DEX bytecode, desugars newer language features on the way and, with R8, shrinks the code.
  4. The APK or app bundle, with classes.dex inside.
  5. An arrow labelled "installs when the device's API level is minSdk or higher" leads to the last box: the Android Runtime (ART) on the device, which runs the DEX with the core libraries of the device's API level.
  • javac compiles, as on a server. Android’s guide to Java versions says that the toolchain JDK contains the compiler that compiles your Java sources, and that it also runs the unit tests and javadoc. The code compiles against the Android API of compileSdk, not against the JDK’s own library.
  • D8 converts and desugars. Android’s documentation calls the bytecode rewriting desugaring: it happens while D8, or R8, turns class files into DEX code. It lets lambdas, method references, default and static interface methods and try-with-resources run on every API level. Android does not support serialising lambdas.
  • Core library desugaring adds some library classes. With the Android Gradle plugin 4.0 or later, an app can opt in to a separate DEX file that implements missing APIs, among them sequential streams, a subset of java.time, java.util.function and Optional; plugin 7.4 and later add a subset of Java 11 APIs. Each API it covers is listed in Android’s desugaring tables.
  • ART runs the DEX with the phone’s core libraries. Android 15’s notes say that its Java API updates reach devices running Android 12 (API level 31) and higher through Google Play system updates.
ZIP & RAR Extractor An APK is a ZIP file: open one in your browser to see classes.dex, the file D8 wrote.

The API level decides, not the Java version

Android’s guide to Java versions pairs each Android version with the Java version of its core libraries:

Android API level Java version of the core libraries
14 34 17
13 33 11
12 32 11
11 and lower 30 and lower No single version given

That table is a summary, and the API reference is the contract: every method there carries the API level that added it. Android 15 kept going after Java 17 and added sequenced collections from Java 21 at API level 35. These are the levels the reference gives a few methods this track uses:

Method Java Android
List.of 9 API level 30
Stream.takeWhile 9 API level 34
List.copyOf 10 API level 31
String.repeat, strip, isBlank, lines 11 API level 33
String.indent, String.transform 12 API level 34
String.formatted 15 API level 34
Stream.toList 16 API level 34
List.getFirst, getLast, reversed 21 API level 35
String.splitWithDelimiters 21 API level 37
Stream.gather 24 Version 36.1
Thread.ofVirtual, virtual threads 21 Not in Android’s Thread reference

Read across a row and the problem is plain: String.repeat has been in Java since Java 11, yet a phone needs API level 33 for it. Two numbers in the build set the limits. compileSdk is the Android API your code compiles against, so it decides what compiles. minSdk is the oldest Android version the app installs on, so every method the app calls must exist at that level, unless desugaring supplies it. A call above minSdk compiles cleanly, and an older phone that lacks the method fails when the call runs.

Which parts of this track carry over

Part of this track On Android
The language up to Java 17: types, control flow, classes, interfaces, generics, exceptions, lambdas, switch expressions, text blocks, instanceof patterns Yes. javac compiles it and D8 desugars what older devices need; Android 14’s notes list text blocks, instanceof patterns and sealed classes in its Java 17 support
Records and sealed classes Yes. java.lang.Record is in the reference from API level 34, and D8 rewrites both for older devices: one fix in the plugin’s 8.2.0 notes switches to the native forms from minimum API level 34
Language features of Java 21 and later: patterns in switch, record patterns, compact source files Not at Android’s documented Java level; javac --release 17 rejects them
Collections and streams Yes, method by method: check each one’s API level
java.time From API level 26 (LocalDate), or lower with core library desugaring
Threads, executors, CompletableFuture Yes; virtual threads are not in Android’s Thread reference
Unit tests with JUnit Yes: local unit tests run on the toolchain JDK during the build
JavaFX and Swing, servlets and Jakarta EE, the module system, jlink and jpackage, JVM flags, garbage-collector tuning No: Android apps have their own UI toolkit and runtime, and talk to servers through an API

Audit a shared library for Android

The shop’s Basket library is shared by a server on Java 25 and an Android app whose minSdk is 26. It was written for Java 21: Discount.java totals a discount with a switch over record patterns, and Basket.java calls three methods that Java 21 added.

java21/src/com/example/basket/Basket

Basket.java (Java 21) · java21/src/com/example/basket/Basket.java

package com.example.basket;

import java.util.ArrayList;
import java.util.List;

/** A shopping basket that a server and an Android app share, as first written for Java 21. */
public final class Basket {
    /** One line of the basket: a product, how many, and the price of one in cents. */
    public record Line(String product, int quantity, long unitCents) {}

    private final List<Line> lines = new ArrayList<>();

    public void add(String product, int quantity, long unitCents) {
        // An order holds between 1 and 20 of each product.
        lines.add(new Line(product, Math.clamp(quantity, 1, 20), unitCents));
    }

    public Line lastAdded() {
        return lines.getLast();
    }

    public long total(Discount discount) {
        long sum = 0;
        for (Line line : lines) {
            sum += line.quantity() * line.unitCents();
        }
        return Discount.apply(discount, sum);
    }

    public String receipt() {
        StringBuilder out = new StringBuilder();
        for (Line line : lines) {
            out.append(line.product()).append(' ').repeat('.', 12 - line.product().length());
            out.append(' ').append(line.quantity()).append('\n');
        }
        return out.toString();
    }
}

Discount.java (Java 21) · java21/src/com/example/basket/Discount.java

package com.example.basket;

/** A discount on the whole basket: a fixed amount off, or a percentage off. */
public sealed interface Discount permits Discount.AmountOff, Discount.PercentOff {
    record AmountOff(long cents) implements Discount {}

    record PercentOff(int percent) implements Discount {}

    /** The total after the discount, never below zero. */
    static long apply(Discount discount, long sum) {
        return switch (discount) {
            case AmountOff(long cents) -> Math.max(0, sum - cents);
            case PercentOff(int percent) -> sum - sum * percent / 100;
        };
    }
}

demo/BasketDemo.java

import com.example.basket.Basket;
import com.example.basket.Discount;

/** The basket's checks: the same lines must print on every Java version and on the phone. */
public class BasketDemo {
    public static void main(String[] args) {
        Basket basket = new Basket();
        basket.add("rice 5 kg", 2, 1250);
        basket.add("tea", 30, 399);
        basket.add("lentils", 1, 280);
        System.out.print(basket.receipt());
        System.out.println("last added: " + basket.lastAdded().product());
        System.out.println("total with 10 % off: " + basket.total(new Discount.PercentOff(10)));
        System.out.println("total with 500 off: " + basket.total(new Discount.AmountOff(500)));
    }
}

The first check is javac --release 17, which compiles with the language rules and the API of Java 17. It found problems in two rounds:

The Java 21 basket for Java 17: two rounds of errors, then the port on Java 25 and 17 Python · release_session.py
from session import run

# The Java 21 basket compiled for Java 17 in two rounds (javac reports the language first, then the API calls), the
# Java 17 port, and the port's checks on Java 25 and on Java 17.
run(
    """
javac --release 17 -d round1 java21/src/com/example/basket/*.java
javac --release 17 -d round2 java21/src/com/example/basket/Basket.java java17/src/com/example/basket/Discount.java
javac --release 17 -d out17 java17/src/com/example/basket/*.java demo/BasketDemo.java
java -cp out17 BasketDemo
$JDK_17/bin/java -cp out17 BasketDemo
""",
    fails=(
        "javac --release 17 -d round1 java21/src/com/example/basket/*.java",
        "javac --release 17 -d round2 java21/src/com/example/basket/Basket.java java17/src/com/example/basket/Discount.java",
    ),
)

Output

# openjdk version "25.0.4.1" LTS
$ javac --release 17 -d round1 java21/src/com/example/basket/*.java
java21/src/com/example/basket/Discount.java:12: error: patterns in switch statements are not supported in -source 17
            case AmountOff(long cents) -> Math.max(0, sum - cents);
                 ^
  (use -source 21 or higher to enable patterns in switch statements)
java21/src/com/example/basket/Discount.java:12: error: deconstruction patterns are not supported in -source 17
            case AmountOff(long cents) -> Math.max(0, sum - cents);
                          ^
  (use -source 21 or higher to enable deconstruction patterns)
2 errors
[exit status 1]
$ javac --release 17 -d round2 java21/src/com/example/basket/Basket.java java17/src/com/example/basket/Discount.java
java21/src/com/example/basket/Basket.java:15: error: cannot find symbol
        lines.add(new Line(product, Math.clamp(quantity, 1, 20), unitCents));
                                        ^
  symbol:   method clamp(int,int,int)
  location: class Math
java21/src/com/example/basket/Basket.java:19: error: cannot find symbol
        return lines.getLast();
                    ^
  symbol:   method getLast()
  location: variable lines of type List<Line>
java21/src/com/example/basket/Basket.java:33: error: cannot find symbol
            out.append(line.product()).append(' ').repeat('.', 12 - line.product().length());
                                                  ^
  symbol:   method repeat(char,int)
  location: class StringBuilder
3 errors
[exit status 1]
$ javac --release 17 -d out17 java17/src/com/example/basket/*.java demo/BasketDemo.java
$ java -cp out17 BasketDemo
rice 5 kg ... 2
tea ......... 20
lentils ..... 1
last added: lentils
total with 10 % off: 9684
total with 500 off: 10260
$ $JDK_17/bin/java -cp out17 BasketDemo
rice 5 kg ... 2
tea ......... 20
lentils ..... 1
last added: lentils
total with 10 % off: 9684
total with 500 off: 10260

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 release_session.py

  • javac reports the language before the API. The first command stopped at the two pattern errors in Discount.java and said nothing about Basket.java. With Discount.java rewritten, the second command reported the three calls that Java 17 lacks: Math.clamp, List.getLast and StringBuilder.repeat. Compile again after every fix until nothing is left.
  • The Java 17 port printed the same lines on Java 25 and Java 17. The switch became two instanceof patterns, Math.clamp became Math.max and Math.min, and getLast() became get(size() - 1) with an explicit check, so an empty basket still throws NoSuchElementException, as getLast() did.

The second check asks the Android question. ApiLevels.java reads the compiled classes with the JDK’s Class-File API and reports calls to methods that Android added after a given API level, from a small table of levels taken from Android’s reference:

ApiLevels.java Java
// Lists the calls in compiled classes to methods that Android added after a given API level.
//
//   java ApiLevels.java <minSdk> <folder of .class files>...
//
// A compact source file (Java 25): it reads class files with the Class-File API (java.lang.classfile). The table holds
// a few methods with the API level that Android's API reference gives each; it is a teaching aid for this lesson, not
// a replacement for the API checks of Android lint.

static final Map<String, String> ADDED = Map.ofEntries(
        Map.entry("java/util/List.of", "30"),
        Map.entry("java/util/List.copyOf", "31"),
        Map.entry("java/lang/String.repeat", "33"),
        Map.entry("java/lang/String.strip", "33"),
        Map.entry("java/lang/String.isBlank", "33"),
        Map.entry("java/lang/String.lines", "33"),
        Map.entry("java/lang/String.indent", "34"),
        Map.entry("java/lang/String.formatted", "34"),
        Map.entry("java/lang/String.transform", "34"),
        Map.entry("java/util/stream/Stream.toList", "34"),
        Map.entry("java/util/stream/Stream.takeWhile", "34"),
        Map.entry("java/util/List.getFirst", "35"),
        Map.entry("java/util/List.getLast", "35"),
        Map.entry("java/util/List.removeFirst", "35"),
        Map.entry("java/util/List.removeLast", "35"),
        Map.entry("java/util/List.reversed", "35"),
        Map.entry("java/util/stream/Stream.gather", "36.1"),
        Map.entry("java/lang/String.splitWithDelimiters", "37"));

void main(String[] args) throws IOException {
    double minSdk = Double.parseDouble(args[0]);
    Set<String> findings = new TreeSet<>();
    for (int i = 1; i < args.length; i++) {
        List<Path> classes;
        try (Stream<Path> files = Files.walk(Path.of(args[i]))) {
            classes = files.filter(p -> p.toString().endsWith(".class")).sorted().toList();
        }
        for (Path file : classes) {
            ClassModel model = ClassFile.of().parse(Files.readAllBytes(file));
            String owner = model.thisClass().asInternalName().replace('/', '.');
            for (MethodModel method : model.methods()) {
                String caller = owner + "." + method.methodName().stringValue();
                method.code().ifPresent(code -> code.forEach(element -> {
                    if (element instanceof InvokeInstruction call) {
                        String target = call.owner().asInternalName() + "." + call.name().stringValue();
                        String level = ADDED.get(target);
                        if (level != null && Double.parseDouble(level) > minSdk) {
                            findings.add(caller + " calls " + target.replace('/', '.') + ": added in API level " + level);
                        }
                    }
                }));
            }
        }
    }
    findings.forEach(IO::println);
    IO.println(findings.isEmpty() ? "no calls above API level " + args[0] : findings.size() + " call(s) above API level " + args[0]);
}
The Java 17 port against API level 26, and the version that keeps to it Python · apilevels_session.py
from session import run

# The Java 17 port checked against Android API level 26 with ApiLevels.java, then the version that keeps to API level
# 26, checked the same way and run on Java 17.
run(
    """
javac --release 17 -d out17 java17/src/com/example/basket/*.java demo/BasketDemo.java
java ApiLevels.java 26 out17
javac --release 17 -d out26 api26/src/com/example/basket/*.java demo/BasketDemo.java
java ApiLevels.java 26 out26
$JDK_17/bin/java -cp out26 BasketDemo
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac --release 17 -d out17 java17/src/com/example/basket/*.java demo/BasketDemo.java
$ java ApiLevels.java 26 out17
com.example.basket.Basket.receipt calls java.lang.String.repeat: added in API level 33
1 call(s) above API level 26
$ javac --release 17 -d out26 api26/src/com/example/basket/*.java demo/BasketDemo.java
$ java ApiLevels.java 26 out26
no calls above API level 26
$ $JDK_17/bin/java -cp out26 BasketDemo
rice 5 kg ... 2
tea ......... 20
lentils ..... 1
last added: lentils
total with 10 % off: 9684
total with 500 off: 10260

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 apilevels_session.py

The port compiled for Java 17, and it still called String.repeat, which needs API level 33: a phone between API level 26 and 32 is not guaranteed to have it, and the first receipt would fail there. The api26 version appends the dots in a loop, and the check came back empty. The two checks answer different questions: --release 17 asks whether the code is Java 17, and the API levels ask whether the phone has each method. On a real project, Android lint’s API checks do the second job for the app’s own code; a shared library deserves the same check before it ships.

Set the JDKs in the build

Three settings decide which Java an Android build uses, and Android’s guide warns that they serve different purposes:

Setting What it decides Where it is set
The JDK that runs Gradle The Java that Gradle and the Android Gradle plugin run on; plugin 9.4 needs JDK 17 or later Android Studio’s Gradle JDK setting; in a terminal, JAVA_HOME or java on PATH
The Java toolchain The javac that compiles the Java sources, and the JDK that runs unit tests and javadoc java { toolchain { languageVersion = … } }
sourceCompatibility and targetCompatibility The language level of the Java sources, and the class-file version javac writes android { compileOptions { … } }

Without a toolchain, the JDK that runs Gradle compiles the code, so a laptop and a build server with different JDKs can build different apps; Android’s guide recommends always setting the toolchain. If the source and target settings are left out, they follow the toolchain. This is the app module of the shop:

android-app/build.gradle.kts Kotlin
// app/build.gradle.kts of the shop's Android app module. Shown, not built here: building it needs the Android SDK.
plugins {
    id("com.android.application")
}

android {
    namespace = "com.example.grocer"
    // The Android API the code compiles against.
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.grocer"
        // The oldest Android version the app installs on: API level 26 is Android 8.0.
        minSdk = 26
        targetSdk = 36
    }

    compileOptions {
        // The Java language level of the sources, and the class-file version javac writes.
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

// The JDK whose javac compiles the Java sources, and which runs the unit tests and javadoc.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

dependencies {
    // The shared basket library, a plain Java module with the same toolchain.
    implementation(project(":basket"))
}

Building that module needs the Android SDK, but the shared library is a plain Java module with the same toolchain, and Gradle builds it anywhere. Here Gradle ran on JDK 25 and the toolchain was JDK 17:

toolchain/build.gradle

toolchain/build.gradle.kts

// The basket as a plain Java library module, with the toolchain that the Android app's build uses too.
plugins {
    `java-library`
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

// Which version of the basket to compile: -Pbasket=java21 (the default) or -Pbasket=api26.
sourceSets {
    main {
        java.setSrcDirs(listOf("../" + providers.gradleProperty("basket").getOrElse("java21") + "/src"))
    }
}

toolchain/gradle.properties

# Gradle looks for toolchain JDKs only in the folder that the JDK_17 environment variable names, so the build
# compiles with the same JDK on every computer that sets it. Without these lines it also searches the usual places.
org.gradle.java.installations.fromEnv=JDK_17
org.gradle.java.installations.auto-detect=false
org.gradle.java.installations.auto-download=false
Gradle on JDK 25 with the Java 17 toolchain: the Java 21 basket Python · toolchain_session.py
from session import run

# Gradle runs on JDK 25 and compiles with the Java 17 toolchain: the Java 21 basket fails with JDK 17's own javac.
run(
    """
gradle -q compileJava
""",
    fails=("gradle -q compileJava",),
    folder="toolchain",
    gradle=True,
)

Output

# openjdk version "25.0.4.1" LTS · Gradle 9.7.1
$ gradle -q compileJava
java21/src/com/example/basket/Discount.java:12: error: '.class' expected
            case AmountOff(long cents) -> Math.max(0, sum - cents);
                                ^
java21/src/com/example/basket/Discount.java:12: error: ';' expected
            case AmountOff(long cents) -> Math.max(0, sum - cents);
                                     ^
java21/src/com/example/basket/Discount.java:13: error: '.class' expected
            case PercentOff(int percent) -> sum - sum * percent / 100;
                                ^
java21/src/com/example/basket/Discount.java:13: error: ';' expected
            case PercentOff(int percent) -> sum - sum * percent / 100;
                                       ^
java21/src/com/example/basket/Discount.java:13: error: not a statement
            case PercentOff(int percent) -> sum - sum * percent / 100;
                                                ^
5 errors

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Compilation failed; see the compiler output below.
  java21/src/com/example/basket/Discount.java:12: error: '.class' expected
              case AmountOff(long cents) -> Math.max(0, sum - cents);
                                  ^
  5 errors

* Try:
> Check your code and dependencies to fix the compilation error(s)
> Run with --scan to get full insights from a Build Scan (powered by Develocity).

BUILD FAILED in …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 toolchain_session.py

The errors prove which compiler ran. JDK 25’s javac with --release 17 named the problem, “patterns in switch statements are not supported in -source 17”; JDK 17’s own javac, older than record patterns, only saw broken syntax, “‘.class’ expected”. The api26 sources compile, into class files of version 61, the number the JVM specification gives Java 17:

The same build with the API level 26 basket Python · toolchain26_session.py
from session import run

# The same build with the API level 26 version of the basket: it compiles, into class files of version 61 (Java 17).
run(
    """
gradle -q compileJava -Pbasket=api26
javap -v build/classes/java/main/com/example/basket/Basket.class | grep "major version"
""",
    folder="toolchain",
    gradle=True,
)

Output

# openjdk version "25.0.4.1" LTS · Gradle 9.7.1
$ gradle -q compileJava -Pbasket=api26
$ javap -v build/classes/java/main/com/example/basket/Basket.class | grep "major version"
  major version: 61

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 toolchain26_session.py

Practise: port the delivery slots

Slots.java lists a day’s delivery slots for the shop’s app. It was written for Java 21 and uses methods that the app’s minSdk of 26 does not have:

practice/src/com/example/slots/Slots

practice/src/com/example/slots/Slots.java

package com.example.slots;

import java.util.List;
import java.util.stream.IntStream;

/** Delivery slots for the shop's app, as first written for Java 21. Port it to Java 17 and API level 26. */
public final class Slots {
    private Slots() {
    }

    /** The day's slots, one an hour from firstHour to lastHour, such as "09:00-10:00". */
    public static List<String> forDay(int firstHour, int lastHour) {
        return IntStream.range(firstHour, lastHour)
                .mapToObj(h -> "%02d:00-%02d:00".formatted(h, h + 1))
                .toList();
    }

    /** The day's last slot. */
    public static String lastSlot(List<String> slots) {
        return slots.getLast();
    }

    /** The slots a customer can book: the shop blanks out the ones that are full. */
    public static List<String> open(List<String> slots) {
        return slots.stream().filter(s -> !s.isBlank()).toList();
    }
}

practice/SlotsDemo.java

import com.example.slots.Slots;
import java.util.ArrayList;
import java.util.List;

/** The checks of the exercise: your port must print exactly these lines, on Java 17 and on Java 25. */
public class SlotsDemo {
    public static void main(String[] args) {
        List<String> day = new ArrayList<>(Slots.forDay(9, 13));
        System.out.println("slots: " + day);
        System.out.println("last: " + Slots.lastSlot(day));
        day.set(1, "  ");
        System.out.println("open: " + Slots.open(day));
    }
}
The starting point: no Java 17 build, and five calls above API level 26 Python · practice_session.py
from session import run

# The exercise's starting point: Slots.java does not compile for Java 17; compiled for Java 21, ApiLevels.java finds
# four calls above API level 26, and SlotsDemo prints the lines that the port must keep.
run(
    """
javac --release 17 -d out17 practice/src/com/example/slots/Slots.java practice/SlotsDemo.java
javac --release 21 -d out21 practice/src/com/example/slots/Slots.java practice/SlotsDemo.java
java ApiLevels.java 26 out21
java -cp out21 SlotsDemo
""",
    fails=("javac --release 17 -d out17 practice/src/com/example/slots/Slots.java practice/SlotsDemo.java",),
)

Output

# openjdk version "25.0.4.1" LTS
$ javac --release 17 -d out17 practice/src/com/example/slots/Slots.java practice/SlotsDemo.java
practice/src/com/example/slots/Slots.java:20: error: cannot find symbol
        return slots.getLast();
                    ^
  symbol:   method getLast()
  location: variable slots of type List<String>
1 error
[exit status 1]
$ javac --release 21 -d out21 practice/src/com/example/slots/Slots.java practice/SlotsDemo.java
$ java ApiLevels.java 26 out21
com.example.slots.Slots.forDay calls java.util.stream.Stream.toList: added in API level 34
com.example.slots.Slots.lambda$forDay$0 calls java.lang.String.formatted: added in API level 34
com.example.slots.Slots.lambda$open$0 calls java.lang.String.isBlank: added in API level 33
com.example.slots.Slots.lastSlot calls java.util.List.getLast: added in API level 35
com.example.slots.Slots.open calls java.util.stream.Stream.toList: added in API level 34
5 call(s) above API level 26
$ java -cp out21 SlotsDemo
slots: [09:00-10:00, 10:00-11:00, 11:00-12:00, 12:00-13:00]
last: 12:00-13:00
open: [09:00-10:00, 11:00-12:00, 12:00-13:00]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

Change only Slots.java so that these checks pass:

  1. javac --release 17 -d out practice/src/com/example/slots/Slots.java practice/SlotsDemo.java compiles.
  2. java ApiLevels.java 26 out prints no calls above API level 26.
  3. java -cp out SlotsDemo prints the three lines recorded above, on Java 25 and with $JDK_17/bin/java.

A first hint: the getLast() error is the only one --release 17 reports; the four other calls are Java 17 methods that Android added later, so String.format, a loop or Collectors.toList() can replace them.

Hex Viewer & File Inspector Open a classes.dex file: its first eight bytes are the DEX magic, dex, a line feed, a three-digit format version and a zero byte.

Kotlin first, Java supported

Android’s own page on its “Kotlin-first” approach says that new Jetpack libraries, samples and documentation are designed with Kotlin users in mind, while Android keeps supporting its APIs from Java. Its comparison table marks the platform SDK, Android Studio, lint and AndroidX as supported for Java, and Jetpack Compose, Android’s newer UI toolkit, as not available from Java. So a Java developer can build and maintain Android apps, shared libraries are a natural fit, and new screens are increasingly written in Kotlin. What you learn in this track carries into Kotlin too, because Kotlin runs on the same build and the same runtime: the collections, the concurrency rules and the cost of each choice stay the same.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 A shared library compiles with javac --release 17 and its tests pass on Java 17. Can an Android app with minSdk 26 call every method the library calls?

    Choose one answer.

    Show the answer to question 1

    Answer: Not necessarily: Android adds Java methods per API level, and String.repeat, a Java 11 method, needs API level 33

    --release 17 proves the code is Java 17. Whether a phone has a method is a separate question, answered by the API level that Android's reference gives the method: in the lesson, ApiLevels.java found String.repeat, added in API level 33, in a library that compiled for Java 17.

  2. Question 2 of 5 In an Android build, which JDK compiles the Java sources?

    Choose one answer.

    Show the answer to question 2

    Answer: The Java toolchain JDK set with java { toolchain { languageVersion } }

    The toolchain JDK's javac compiles the Java sources and runs the unit tests and javadoc. In the lesson, Gradle ran on JDK 25 while JDK 17's own javac reported the errors. Without a toolchain, the JDK that runs Gradle is used.

  3. Question 3 of 5 Which class-file major version did the Java 17 toolchain write for Basket.class?

    Type a number.

    Show the answer to question 3

    Answer: 61

    javap printed "major version: 61", the number the JVM specification gives Java 17 class files. D8 then turns those class files into DEX for the device.

  4. Question 4 of 5 What does D8 do in an Android build?

    Choose one answer.

    Show the answer to question 4

    Answer: It turns class files into DEX bytecode for the Android Runtime and rewrites some newer language features for older devices

    Android's documentation describes the bytecode changes called desugaring as part of the D8/R8 step that turns class files into DEX code. javac still compiles the Java source.

  5. Question 5 of 5 Which of these parts of the Java track do not carry over to an Android app?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • Servlets deployed on Tomcat
    • Runtime images built with jlink
    • JavaFX windows and Swing forms

    JavaFX, Swing, servlets and jlink belong to desktops, servers and the JVM. Lambdas, method references and the core language carry over: javac compiles them and D8 desugars what older devices need.

Interview questions

Fresher · What is desugaring in an Android build, and which tool does it?

Desugaring is the rewriting of Java bytecode so that features newer than a device’s runtime still work on it. In an Android build, D8 does it while it turns class files into DEX bytecode, and R8 does it in release builds: lambdas, method references and default interface methods become ordinary classes and calls that every API level can run. Library APIs are a separate case. Core library desugaring, which an app switches on in its build, adds implementations of a documented subset of java.time, streams and other APIs as an extra DEX file. Anything outside those tables needs the API level that Android’s reference gives it.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A Java library is shared by a Java 21 service and an Android app with minSdk 26. It compiles with --release 17 and its tests pass, yet the app fails on an Android 11 phone when it prints a receipt. What went wrong, and what do you add to the library's build so that it cannot happen again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    An Android project builds in Android Studio, but on the CI server the same commit fails with "Android Gradle plugin requires Java 17 to run. You are currently using Java 11", although the module sets its Java toolchain to 17. Explain the JDKs involved and how you make every machine build the same app.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Android compiles Java with javac, converts class files to DEX with D8 or R8, and runs them on ART, not on a JVM.
  • Android’s documentation pairs Java 17 with Android 14, API level 34; Java 21 language features are beyond that level.
  • On a phone, the API level decides which Java methods exist: String.repeat is Java 11 but needs API level 33.
  • Check shared code twice: javac --release 17 for the Java level, then each method’s API level against minSdk.
  • Three settings choose the Java of a build: the JDK that runs Gradle, the toolchain, and the source and target compatibility.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.