Java 24 Class-File API Tutorial with Examples
Introduction
Much of the Java ecosystem reads and writes bytecode - the compiled
OUTPUT of the above code
The class file version 68 corresponds to Java 24, the release the code was compiled for. Names are returned as their own types, such as
Generating a class
OUTPUT of the above code
Each builder method maps to a bytecode instruction -
Transforming a class
Most real tools do not build classes from scratch but transform existing ones - adding logging, removing methods, renaming things. The API models a transformation as a function from elements to elements. The example removes the
OUTPUT of the above code
.class files the JVM runs. Frameworks generate proxy classes, mocking libraries create test doubles, agents instrument code for profiling, and the JDK itself generates classes for lambdasClick to read: lambdas. For years this was done with third-party libraries, mostly ASM, which the JDK also bundled internally. But the class file format changes with every six-monthly Java release, and a library like ASM can only support a new format after that release ships - so frameworks always lagged behind the newest Java. The Class-File API, previewed in Java 22 and 23 and final in Java 24Click to read tutorial: Overview of New Features in Java 24 (JEP 484), is a standard API in java.lang.classfile which always supports the class file format of the JDK it ships with. This tutorial explains the Class-File API with examples - parsing a class file, generating a class from scratch and running it, and transforming an existing class.
Parsing a class file
ClassFile.of().parse(bytes) returns a ClassModel, an immutable tree of the class's elements - fields, methods, attributes, and the bytecode instructions inside each method -
A class to inspect
//Greeting.java
public class Greeting {
private final String name;
public Greeting(String name) { this.name = name; }
public String hello() { return "Hello, " + name; }
public void debug() { System.out.println("debug: " + name); }
}
Listing the members of a class file
//ParseClassFile.java
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.CodeModel;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;
import java.nio.file.Files;
import java.nio.file.Path;
public class ParseClassFile {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Path.of("classes", "Greeting.class"));
ClassModel model = ClassFile.of().parse(bytes);
System.out.println("Class: " + model.thisClass().asInternalName()
+ " (class file version " + model.majorVersion() + ")");
for (FieldModel f : model.fields()) {
System.out.println(" field " + f.fieldName() + " : " + f.fieldTypeSymbol().displayName());
}
for (MethodModel m : model.methods()) {
int instructions = m.code().map(c -> (int) c.elementStream().filter(e -> e instanceof java.lang.classfile.Instruction).count()).orElse(0);
System.out.println(" method " + m.methodName() + m.methodTypeSymbol().displayDescriptor()
+ " - " + instructions + " instructions");
}
}
}
Class: Greeting (class file version 68) field name : String method <init>(String)void - 6 instructions method hello()String - 4 instructions method debug()void - 6 instructions
Utf8Entry for constant pool strings and ClassDesc and MethodTypeDesc for types - the descriptor types of the JVM Constants APIClick to read: JVM Constants API added in Java 12.
ClassFile.of().build() creates a class file from a description written with builders, down to individual bytecode instructions. The example generates a class with a static greet() method which prints a message, loads it into the running JVM, and calls it -
Generating, loading and running a class
//GenerateClass.java
import static java.lang.constant.ConstantDescs.CD_String;
import static java.lang.constant.ConstantDescs.CD_void;
import java.lang.classfile.ClassFile;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import java.lang.invoke.MethodHandles;
public class GenerateClass {
public static void main(String[] args) throws Throwable {
ClassDesc system = ClassDesc.of("java.lang.System");
ClassDesc printStream = ClassDesc.of("java.io.PrintStream");
byte[] bytes = ClassFile.of().build(ClassDesc.of("GeneratedHello"), clazz -> clazz
.withFlags(ClassFile.ACC_PUBLIC)
.withMethodBody("greet", MethodTypeDesc.of(CD_void), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC,
code -> code
.getstatic(system, "out", printStream) // System.out
.ldc("Hello from a class generated at runtime!") // the message
.invokevirtual(printStream, "println", MethodTypeDesc.of(CD_void, CD_String))
.return_()));
System.out.println("Generated " + bytes.length + " bytes of bytecode");
Class<?> generated = MethodHandles.lookup().defineClass(bytes); // load it
generated.getMethod("greet").invoke(null); // run it
}
}
Generated 299 bytes of bytecode Hello from a class generated at runtime!
getstatic, ldc, invokevirtual, return_. The API computes the details that make hand-written bytecode error-prone, such as the constant pool, the maximum stack size and the stack map frames which the JVM's verifier requires.
debug() method from Greeting -
Removing a method from a class
//TransformClass.java
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.ClassTransform;
import java.lang.classfile.MethodModel;
import java.nio.file.Files;
import java.nio.file.Path;
public class TransformClass {
public static void main(String[] args) throws Exception {
ClassFile cf = ClassFile.of();
ClassModel original = cf.parse(Files.readAllBytes(Path.of("classes", "Greeting.class")));
byte[] transformed = cf.transformClass(original, ClassTransform.dropping(
element -> element instanceof MethodModel m && m.methodName().equalsString("debug")));
ClassModel result = cf.parse(transformed);
System.out.println("Methods before: " + original.methods().stream().map(m -> m.methodName().stringValue()).toList());
System.out.println("Methods after : " + result.methods().stream().map(m -> m.methodName().stringValue()).toList());
}
}
Methods before: [<init>, hello, debug] Methods after : [<init>, hello]
ClassTransform.dropping() drops matching elements and keeps everything else unchanged. Transforms can also replace elements, add new ones, and be composed with andThen(); method and code transforms work the same way at lower levels, down to individual instructions.
Summary
In this tutorial we saw how the Class-File API, final in Java 24, provides a standard way to read, write and transform class files which always keeps up with the JDK's own class file format. We parsed a class file into a ClassModel and listed its fields and methods, generated a class with a method body built from bytecode instructions, loaded and ran it, and removed a method with a ClassTransform. Next, read about the AOT cache for faster startupClick to read tutorial: AOT Cache in Java 24 and 25: Faster Startup.Continue Reading Java 22 to 25 Tutorials
Overview of New Features in Java 24Click to read tutorial: Overview of New Features in Java 24 Java 24 Stream Gatherers Tutorial with ExamplesClick to read tutorial: Java 24 Stream Gatherers Tutorial with Examples AOT Cache in Java 24 and 25: Faster StartupClick to read tutorial: AOT Cache in Java 24 and 25: Faster Startup Java 24: Virtual Threads in synchronized Without PinningClick to read tutorial: Java 24: Virtual Threads in synchronized Without Pinning Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSAClick to read tutorial: Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSA Overview of New Features in Java 22Click to read tutorial: Overview of New Features in Java 22 Overview of New Features in Java 23Click to read tutorial: Overview of New Features in Java 23 Overview of New Features in Java 25Click to read tutorial: Overview of New Features in Java 25 Java 22 Foreign Function & Memory API TutorialClick to read tutorial: Java 22 Foreign Function & Memory API Tutorial
Overview of New Features in Java 24Click to read tutorial: Overview of New Features in Java 24 Java 24 Stream Gatherers Tutorial with ExamplesClick to read tutorial: Java 24 Stream Gatherers Tutorial with Examples AOT Cache in Java 24 and 25: Faster StartupClick to read tutorial: AOT Cache in Java 24 and 25: Faster Startup Java 24: Virtual Threads in synchronized Without PinningClick to read tutorial: Java 24: Virtual Threads in synchronized Without Pinning Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSAClick to read tutorial: Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSA Overview of New Features in Java 22Click to read tutorial: Overview of New Features in Java 22 Overview of New Features in Java 23Click to read tutorial: Overview of New Features in Java 23 Overview of New Features in Java 25Click to read tutorial: Overview of New Features in Java 25 Java 22 Foreign Function & Memory API TutorialClick to read tutorial: Java 22 Foreign Function & Memory API Tutorial