Class-File API in Java
A .class file is like a sealed exam paper: the student wrote it in Java, the compiler printed it in a format only the JVM reads, and most people never open it. Some tools need to open it, though. Frameworks add code to your classes, test tools measure which lines ran, and compilers for other languages write class files from scratch. For years they all used outside libraries such as ASM. The Class-File API puts that ability inside the JDK. In this lesson, you will learn what the Class-File API is, how to read a class file, and how to build a small class while the program runs.
What Is the Class-File API?
The Class-File API in Java is a standard library in the java.lang.classfile package that is used to parse, generate and transform class files. It became final in Java 24.
- Parse: turn the bytes of a
.classfile into a tree of objects you can walk through: fields, methods, instructions. - Generate: describe a class in Java code and get back the bytes of a valid
.classfile. - Transform: read a class, keep most of it, and change a few parts (for example, drop a method or add logging).
Why Was It Added?
The class file format changes with almost every Java release. Outside libraries such as ASM must catch up each time, and tools that ship an older copy break on new class files. The JDK itself used several private copies of these libraries. A standard API that always matches the running JDK solves both problems: it understands every class file version the JDK understands, on the day the JDK is released.
Note that most application code never needs this API. It is aimed at framework, agent, compiler and tool authors. Knowing it exists helps you understand how frameworks change your classes behind the scenes.
The Main Types
| Type | Description |
|---|---|
ClassFile | It is the entry point. ClassFile.of() gives an object with parse(bytes), build(...) and transformClass(...). |
ClassModel | It is used to represent a parsed class: its name, superclass, interfaces, fields and methods. |
FieldModel, MethodModel | They describe one field or one method inside a ClassModel. |
CodeModel | It holds the bytecode instructions of a method. |
ClassBuilder, CodeBuilder | They are used to describe a new class or a method body step by step. |
ClassDesc, MethodTypeDesc | They describe types by name (in java.lang.constant), such as ClassDesc.of("java.lang.String"). |
Reading a Class File
Let's see an example that reads the class file of a small record and prints what is inside it. The program loads its own nested class's bytes as a resource, so it needs no extra files.
import java.io.InputStream;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;
public class Read {
record Fee(String student, int amount) {
int withLateFine() { return amount + 100; }
}
public static void main(String[] args) throws Exception {
byte[] bytes;
String file = "Read$Fee.class";
try (InputStream in = Read.class.getResourceAsStream(file)) {
bytes = in.readAllBytes();
}
ClassModel model = ClassFile.of().parse(bytes);
System.out.println("Class: " + model.thisClass().asInternalName());
System.out.println("Super: "
+ model.superclass().orElseThrow().asInternalName());
for (FieldModel f : model.fields()) {
System.out.println("Field: " + f.fieldName() + " " + f.fieldType());
}
for (MethodModel m : model.methods()) {
System.out.println("Method: " + m.methodName() + m.methodType());
}
}
}Class: Read$Fee
Super: java/lang/Record
Field: student Ljava/lang/String;
Field: amount I
Method: <init>(Ljava/lang/String;I)V
Method: withLateFine()I
Method: toString()Ljava/lang/String;
Method: hashCode()I
Method: equals(Ljava/lang/Object;)Z
Method: student()Ljava/lang/String;
Method: amount()IExplanation:
ClassFile.of().parse(bytes)turns the raw bytes into aClassModel.- Names use the internal form: slashes instead of dots (
java/lang/Record). - Types use descriptors:
Iisint,Ljava/lang/String;isString,Visvoid, and(Ljava/lang/String;I)Vmeans "takes a String and an int, returns nothing". - The output shows what the compiler wrote for a record: the constructor
<init>, your own methodwithLateFine, and the generatedtoString,hashCode,equalsand accessor methods.
Building a Class at Runtime
The API can also generate a class from nothing. Let's see an example that builds a class named Greeter with one static method, loads it, and calls the method.
import static java.lang.classfile.ClassFile.ACC_PUBLIC;
import static java.lang.classfile.ClassFile.ACC_STATIC;
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 Gen {
public static void main(String[] args) throws Throwable {
ClassDesc greeter = ClassDesc.of("Greeter");
ClassDesc system = ClassDesc.of("java.lang.System");
ClassDesc printStream = ClassDesc.of("java.io.PrintStream");
MethodTypeDesc println = MethodTypeDesc.of(CD_void, CD_String);
byte[] bytes = ClassFile.of().build(greeter, cls -> cls
.withFlags(ACC_PUBLIC)
.withMethodBody("hello", MethodTypeDesc.of(CD_void),
ACC_PUBLIC | ACC_STATIC, code -> code
.getstatic(system, "out", printStream)
.ldc("Hello from a class built at runtime")
.invokevirtual(printStream, "println", println)
.return_()));
System.out.println("Generated " + bytes.length + " bytes");
Class<?> loaded = MethodHandles.lookup().defineClass(bytes);
loaded.getMethod("hello").invoke(null);
}
}Generated 287 bytes
Hello from a class built at runtimeExplanation:
build(greeter, cls -> ...)describes the class through aClassBuilder.withMethodBodyadds a method namedhellothat ispublic static void, with its instructions written through aCodeBuilder.- The four instructions do exactly what
System.out.println("...")compiles to: get the static fieldout, push the text, callprintln, and return. MethodHandles.lookup().defineClass(bytes)loads the new class into the same package asGen, and reflection calls its method.
Transforming a Class
The third job is to change an existing class. ClassFile.of().transformClass(model, transform) copies every element of the class through your transform, which can keep, drop or replace each one. For example, this transform drops every method whose name starts with debug:
byte[] cleaned = ClassFile.of().transformClass(model, (builder, element) -> {
if (element instanceof MethodModel m
&& m.methodName().stringValue().startsWith("debug")) {
return; // leave it out
}
builder.with(element); // keep everything else
});Frameworks use the same idea to add timing code to methods, or to add an interface to a class while it loads.
In Practice
| Who uses it | What for |
|---|---|
| The JDK itself | javac, jlink, lambdas and other internals are moving to it from private copies of ASM |
| Java agents | Changing classes as they load, for monitoring and profiling |
| Frameworks | Generating proxy classes and adding behavior to your classes |
| Language compilers | Writing class files for languages that run on the JVM |
If you write an agent or a framework today, prefer the Class-File API over an outside library: it never falls behind the JDK you run on.
Common Mistakes
Mixing dotted and slashed names. ClassDesc.of takes the normal dotted name (java.lang.String); asInternalName() returns slashes. Don't build descriptors by hand from strings.
Forgetting return_(). Every method body must end with a return instruction, even a void one. The method name has an underscore because return is a Java keyword.
Expecting it on older JDKs. The final API needs Java 24 or newer. Tools that must support Java 17 still use ASM.
Practice
- Easy: Change
Read.javato also print the interfaces of the class withmodel.interfaces(). Then makeFeeimplementComparable<Fee>and run it again. - Harder: Change
Gen.javaso thathelloprints two lines. Then remove thegetstaticcall on purpose and see the error you get.
Key Takeaways
Was this tutorial helpful?