1. Overview

Java 15 introduced a whole lot of features. In this article, we’ll discuss one of the new features called Hidden Classes under JEP-371. This feature is introduced as an alternative to Unsafe API, which isn’t recommended outside JDK.

The hidden classes feature is helpful for anyone who works with dynamic bytecode or JVM languages.

2. What Is a Hidden Class?

Dynamically generated classes provide efficiency and flexibility for low-latency applications. They’re only needed for a limited time. Retaining them for the lifetime of statically generated classes increases memory footprint. Existing solutions for this situation, such as per-class loaders, are cumbersome and inefficient.

Since Java 15, hidden classes have become a standard way to generate dynamic classes.

Hidden classes are classes that cannot be used directly by the bytecode or other classes. Even though it’s mentioned as a class, it should be understood to mean either a hidden class or interface. It can also be defined as a member of the access control nest and can be unloaded independently of other classes.

3. Properties of Hidden Classes

Let’s take a look at the properties of these dynamically generated classes:

  • Non-discoverable – a hidden class is not discoverable by the JVM during bytecode linkage, nor by programs making explicit use of class loaders. The reflective methods Class::forName, ClassLoader::findLoadedClass, and Lookup::findClass will not find them.
  • We can’t use the hidden class as a superclass, field type, return type, or parameter type.
  • Code in the hidden class can use it directly, without relying on the class object.
  • final fields declared in hidden classes are not modifiable regardless of their accessible flags.
  • It extends the access control nest with non-discoverable classes.
  • It may be unloaded even though its notional defining class loader is still reachable.
  • Stack traces don’t show the methods or names of hidden classes by default, however, tweaking JVM options can show them.

4. Creating Hidden Classes

The hidden class isn’t created by any class loader. It has the same defining class loader, runtime package, and protection domain of the lookup class.

First, let’s create a Lookup object:

MethodHandles.Lookup lookup = MethodHandles.lookup();

The Lookup::defineHiddenClass method creates the hidden class. This method accepts an array of bytes.

For simplicity, we’ll define a simple class with the name HiddenClass that has a method to convert a given string to uppercase:

public class HiddenClass {
    public String convertToUpperCase(String s) {
        return s.toUpperCase();
    }
}

Let’s get the path of the class and load it into the input stream. After that, we’ll convert this class into bytes using IOUtils.toByteArray():

Class<?> clazz = HiddenClass.class;
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader()
    .getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray();

Lastly, we pass these constructed bytes into Lookup::defineHiddenClass:

Class<?> hiddenClass = lookup.defineHiddenClass(IOUtils.toByteArray(stream),
  true, ClassOption.NESTMATE).lookupClass();

The second boolean argument true initializes the class. The third argument ClassOption.NESTMATE specifies that the created hidden class will be added as a nestmate to the lookup class so that it has access to the private members of all classes and interfaces in the same nest.

Suppose we want to bind the hidden class strongly with its class loader, ClassOption.STRONG. This means that the hidden class can only be unloaded if its defining loader is not reachable.

5. Using Hidden Classes

Hidden classes are used by frameworks that generate classes at runtime and use them indirectly via reflection.

In the previous section, we looked at creating a hidden class. In this section, we’ll see how to use it and create an instance.

Since casting the classes obtained from Lookup.defineHiddenClass is not possible with any other class object, we use Object to store the hidden class instance. If we wish to cast the hidden class, we can define an interface and create a hidden class that implements the interface:

Object hiddenClassObject = hiddenClass.getConstructor().newInstance();

Now, let’s get the method from the hidden class. After getting the method, we’ll invoke it as any other standard method:

Method method = hiddenClassObject.getClass()
    .getDeclaredMethod("convertToUpperCase", String.class);

Now, we can verify a few properties of a hidden class by invoking some of its methods:

The method isHidden() will return true for this class:

Assertions.assertEquals(true, hiddenClass.isHidden());

Also, since there’s no actual name for a hidden class, its canonical name will be null:

Assertions.assertEquals(null, hiddenClass.getCanonicalName());

The hidden class will have the same defining loader as the class that does the lookup. Since the lookup happens in the same class, the following assertion will be successful:

Assertions.assertEquals(this.getClass()
    .getClassLoader(), hiddenClass.getClassLoader());

If we try to access the hidden class through any methods, they’ll throw ClassNotFoundException. This is obvious, as the hidden class name is sufficiently unusual and unqualified to be visible to other classes. Let’s check a couple of assertions to prove that the hidden class is not discoverable:

Assertions.assertThrows(ClassNotFoundException.class, () -> Class.forName(hiddenClass.getName()));

Note that the only way other classes can use a hidden class is via its Class object.

6. Anonymous Class vs. Hidden Class

We created a hidden class in previous sections and played with some of its properties. Now, let’s elaborate on the differences between anonymous classes – inner classes without explicit names – and hidden classes:

  • Anonymous class has a dynamically generated name with a $ in-between, while a hidden class derived from com.baeldung.reflection.hiddenclass.HiddenClass would be com.baeldung.reflection.hiddenclass.HiddenClass/1234.
  • Anonymous class is instantiated using Unsafe::defineAnonymousClass, which is deprecated, whereas Lookup::defineHiddenClass instantiates a hidden class*.*
  • Hidden classes don’t support constant-pool patching. It helps define anonymous classes with their constant pool entries already resolved to concrete values.
  • Unlike a hidden class, an anonymous class can access protected members of a host class even though it’s in a different package and not a subclass.
  • An anonymous class can enclose other classes to access its members, but a hidden class cannot enclose other classes.

Although the hidden class isn’t a replacement for an anonymous class, they are replacing some of the usages of anonymous classes in the JDK. From Java 15, lambda expressions use hidden classes.

7. Conclusion

In this article, we discussed a new language feature called Hidden Classes in detail. As always, the code is available over on GitHub.