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.
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
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.
- Your .java files and the shared library. An arrow labelled "compiled against the compileSdk API" leads to the next box.
- javac of the toolchain JDK, where sourceCompatibility sets the language level and targetCompatibility the class-file version. An arrow labelled ".class files" leads on.
- 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.
- The APK or app bundle, with classes.dex inside.
- 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.functionandOptional; 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.
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.
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:
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.javaand said nothing aboutBasket.java. WithDiscount.javarewritten, the second command reported the three calls that Java 17 lacks:Math.clamp,List.getLastandStringBuilder.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
instanceofpatterns,Math.clampbecameMath.maxandMath.min, andgetLast()becameget(size() - 1)with an explicit check, so an empty basket still throwsNoSuchElementException, asgetLast()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:
// 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]);
} 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:
// 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.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 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:
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.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));
}
} 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:
javac --release 17 -d out practice/src/com/example/slots/Slots.java practice/SlotsDemo.javacompiles.java ApiLevels.java 26 outprintsno calls above API level 26.java -cp out SlotsDemoprints 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.
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.
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.
-
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.
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.repeatis Java 11 but needs API level 33. - Check shared code twice:
javac --release 17for the Java level, then each method’s API level againstminSdk. - Three settings choose the Java of a build: the JDK that runs Gradle, the toolchain, and the source and target compatibility.
References
- Java versions in Android builds (Android Developers)
- Use Java 8 language features and APIs (desugaring) (Android Developers)
- Java 11+ APIs available through desugaring (Android Developers)
- Android 14 features and APIs: OpenJDK 17 updates (Android Developers)
- Android 15 features and APIs: OpenJDK 17 updates (Android Developers)
- Android API reference: java.lang.String (Android Developers)
- Android API reference: java.util.List (Android Developers)
- Android API reference: java.util.stream.Stream (Android Developers)
- Android API reference: java.lang.Thread (Android Developers)
- Android API reference: java.time.LocalDate (Android Developers)
- Android Gradle plugin release notes: compatibility (Android Developers)
- Android Gradle plugin 8.2.0 release notes: fixed issues (Android Developers)
- Android's Kotlin-first approach (Android Developers)
- The javac Command (Java SE 25): --release (Oracle)
- Java SE 25 API: package java.lang.classfile (Oracle)
- Gradle User Manual: Toolchains for JVM projects (Gradle)
- Dalvik executable format: DEX_FILE_MAGIC (Android Open Source Project)
- JEP 441: Pattern Matching for switch (OpenJDK)
- JEP 440: Record Patterns (OpenJDK)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress