
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 14, 2025
The Java Class-File API was introduced in JEP-484 as part of Java 24. It aims to create an interface that allows class file processing without relying on the legacy JDK’s internal copy implementation of the ASM library.
In this tutorial, we’ll look at how to build class files from scratch and how to transform a class file into another using the Class-File API.
The Class-File has three core elements to generate and transform features that we’ll see later:
Let’s explore how these three components connect through practical examples in the following sections.
In this section, we’ll see how to generate a class file using the MethodBuilder and CodeBuilder classes.
To illustrate class generation, let’s look at a simple code snippet that calculates an employee’s salary based on their function and base salary:
public double calculateAnnualBonus(double baseSalary, String role) {
if (role.equals("sales")) {
return baseSalary * 0.35;
}
if (role.equals("engineer")) {
return baseSalary * 0.25;
}
return baseSalary * 0.15;
}
To generate a method with the same functionality as calculateAnnualBonus(), we can use the MethodBuilder and CodeBuilder classes. Hence, let’s first define the generate() method with a Consumer<MethodBuilder> that will be used to construct methods:
public static void generate() throws IOException {
Consumer<MethodBuilder> calculateAnnualBonusBuilder = methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label notSales = codeBuilder.newLabel();
Label notEngineer = codeBuilder.newLabel();
ClassDesc stringClass = ClassDesc.of("java.lang.String");
// ...
});
}
First, we define two Label objects that will be used later for jumping between conditional statements. Additionally, we’ve defined a ClassDesc constant that represents the String class file for later use.
Then, we can add the first part of our logic inside the calculateAnnualBonusBuilder‘s lambda expression:
codeBuilder.aload(3)
.ldc("sales")
.invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
.ifeq(notSales)
.dload(1)
.ldc(0.35)
.dmul()
.dreturn()
Let’s look at each line of the above logic in detail:
That first part covers the first if statement of the method we want to generate. Hence, to generate the second if statement, we can add more method calls to our codeBuilder(), after the dreturn() call:
.labelBinding(notSales)
.aload(3)
.ldc("engineer")
.invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
.ifeq(notEngineer)
.dload(1)
.ldc(0.25)
.dmul()
.dreturn()
The labelBinding(notSales) runs if the ifeq(notSales) expression returns false. The other operations are similar to those we previously covered to handle the first if statement.
Finally, we can add the last part to cover the default value return:
.labelBinding(notEngineer)
.dload(1)
.ldc(0.15)
.dmul()
.dreturn();
The same thing occurs with labeling branches, but now for the notEngineer label. That last part runs if ifeq(notEngineer) returns false.
Finally, to wrap up our generate() method, we need to define the ClassFile object and write it to a .class file:
var classBuilder = ClassFile.of()
.build(ClassDesc.of("EmployeeSalaryCalculator"),
cb - > cb.withMethod("calculateAnnualBonus", MethodTypeDesc.of(CD_double, CD_double, CD_String),
AccessFlag.PUBLIC.mask(),
calculateAnnualBonusBuilder));
Files.write(Path.of("EmployeeSalaryCalculator.class"), classBuilder);
We’ve used ClassFile.of().build() to instantiate a class file builder, and passed two arguments to it. The first is the class name wrapped inside the ClassDesc.of() call. The second is a ClassBuilder consumer that generates the class with the desired methods. For that, we used withMethod() passing the method name, the method signature, the access flag, and the method code builder defined previously.
Noticeably, we defined the method signature as MethodTypeDesc.of(CD_double, CD_double, CD_String), which means that the method generated returns a double, defined by the first parameter, and receives a double and a String parameter.
Then, we write the byte array stored in the classBuilder variable to a file using the Files writers.
Now, let’s say we want to copy all the contents of a class file into another. We can do that by using different transformations:
public static void transform() throws IOException {
var basePath = Files.readAllBytes(Path.of("EmployeeSalaryCalculator.class"));
CodeTransform codeTransform = ClassFileBuilder::accept;
MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform);
ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform);
ClassFile classFile = ClassFile.of();
byte[] transformedClass = classFile.transformClass(classFile.parse(basePath), classTransform);
Files.write(Path.of("TransformedEmployeeSalaryCalculator.class"), transformedClass);
}
In the example above, we first read the class file we created in the previous section, EmployeeSalaryCalculator.
Then, we define a CodeTransform that accepts all CodeElements defined in the original class. Moreover, we create a MethodTransform using the codeTransform and a ClassTransform using the methodTransform. Such a composition makes it easy to generalize and reuse transformers for different purposes.
More customized code and method transforms could be defined using more explicit lambda expressions. For instance, we could define a custom MethodTransform using a lambda expression that only accepts methods with specific names:
MethodTransform methodTransform = (methodBuilder, methodElement) - > {
if (methodElement.header().name().stringValue().equals("calculateAnnualBonus")) {
methodBuilder.withCode(codeBuilder - > {
for (var codeElement: methodElement.code()) {
codeBuilder.accept(codeElement);
}
});
}
};
In the case above, we first check if the method name is equal to the literal calculateAnnualBonus, using the header() and name() methods. If so, we use the methodBuilder to create a method with the exact instructions from the original class’ methodElement.
In this article, we looked at the details of creating classes from scratch and copying content from one class to another using the Class File API.
We examined examples of how to utilize various builders, transformers, and elements to create and transform classes at runtime.
As always, the source code is available over on GitHub.