Class-File API in Java

Naveen AnamaneniUpdated · 7 min read

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 .class file 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 .class file.
  • 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

TypeDescription
ClassFileIt is the entry point. ClassFile.of() gives an object with parse(bytes), build(...) and transformClass(...).
ClassModelIt is used to represent a parsed class: its name, superclass, interfaces, fields and methods.
FieldModel, MethodModelThey describe one field or one method inside a ClassModel.
CodeModelIt holds the bytecode instructions of a method.
ClassBuilder, CodeBuilderThey are used to describe a new class or a method body step by step.
ClassDesc, MethodTypeDescThey 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.

Read.java▶ Try it
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());
        }
    }
}
Output
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()I

Explanation:

  1. ClassFile.of().parse(bytes) turns the raw bytes into a ClassModel.
  2. Names use the internal form: slashes instead of dots (java/lang/Record).
  3. Types use descriptors: I is int, Ljava/lang/String; is String, V is void, and (Ljava/lang/String;I)V means "takes a String and an int, returns nothing".
  4. The output shows what the compiler wrote for a record: the constructor <init>, your own method withLateFine, and the generated toString, hashCode, equals and 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.

Gen.java▶ Try it
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);
    }
}
Output
Generated 287 bytes
Hello from a class built at runtime

Explanation:

  1. build(greeter, cls -> ...) describes the class through a ClassBuilder.
  2. withMethodBody adds a method named hello that is public static void, with its instructions written through a CodeBuilder.
  3. The four instructions do exactly what System.out.println("...") compiles to: get the static field out, push the text, call println, and return.
  4. MethodHandles.lookup().defineClass(bytes) loads the new class into the same package as Gen, 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:

Transform.java
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 itWhat for
The JDK itselfjavac, jlink, lambdas and other internals are moving to it from private copies of ASM
Java agentsChanging classes as they load, for monitoring and profiling
FrameworksGenerating proxy classes and adding behavior to your classes
Language compilersWriting 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

  1. Easy: Change Read.java to also print the interfaces of the class with model.interfaces(). Then make Fee implement Comparable<Fee> and run it again.
  2. Harder: Change Gen.java so that hello prints two lines. Then remove the getstatic call on purpose and see the error you get.

Quick check: Class-File API

4 questions
1Which package holds the Class-File API?
2What does ClassFile.of().parse(bytes) return?
3What does the descriptor (I)V mean?
4Who is the API mainly for?

Key Takeaways

Was this tutorial helpful?

Lesson 522 of 578