Java Reflection, part 1: primitives, arrays, classes
Introduction
Reflection is the ability of a program to introspect and modify its structure and behavior at runtime. The Java language supports some reflection features in its Core Reflection API which consists of classes from the?java.lang.reflect?package, as well as the?Class,?Package, and?Module?classes.
The first step in using reflection on a type (primitive type, array type, class or interface) is to obtain a?Class?object and determine the parameters of that type. The next steps may include using the methods of this?Class?object to determine its fields, methods, constructors, member classes and member interfaces, annotations, generic types, and others.
This article is based on the Java 17 implementation in Oracle OpenJDK.
Types
There are two kinds of types in the Java language: primitive types and reference types. Primitive types include types?boolean,?byte,?short,?int,?long,?char,?float,?double, and the pseudo-type?void. Reference types include class types, interface types, array types (and type variables).
There is also a special?null?type, the type of the expression?null, which has no name.
The Class class
The entry point for the Core Reflection API is the?Class?class.
There are three ways to get a?Class?object statically (respectively for objects, reference and primitive types, primitive types) at compile-time and one way to do the same dynamically at runtime (for reference types only).
For an object (class instance or array), you can use the?Object::getClass?method on that object.
Class<? extends Integer> objectClass = Integer.valueOf(1).getClass();
Class<? extends Integer[]> objectArrayClass = new Integer[]{}.getClass();
Class<? extends int[]> primitiveArrayClass = new int[]{}.getClass();
For a reference type (class, interface, or array type) or primitive type, you can use a class literal (an expression consisting of the type name, followed by a “.” and the?class?token).
Class<Integer> classClass = Integer.class;
Class<Integer[]> objectArrayClass = Integer[].class;
Class<int[]> primitiveArrayClass = int[].class;
Class<Integer> primitiveClass = int.class;
For a primitive type, you can use a class constant (a?public static final?field defined in the corresponding wrapper class).
Class<Integer> primitiveClassFromConstant = Integer.TYPE;
Class<Integer> primitiveClassFromLiteral = int.class;
assertSame(primitiveClassFromConstant, primitiveClassFromLiteral);
For a reference type (class, interface, or array type) you can use the static?Class::forName?method to get (and to load, link, and initialize if necessary) a class by its fully qualified name.
Class<?> classClass = Class.forName("java.lang.Integer");
Class<?> objectArrayClass = Class.forName("[Ljava.lang.Integer;");
Class<?> primitiveArrayClass = Class.forName("[I");
Modifiers
In the Java language, different constructs (classes, interfaces, fields, methods, constructors, and parameters) can have modifiers in their declarations to denote their access permissions and properties:
In addition, in the Java VM, there is an additional?interface?modifier, which is considered a pseudo-modifier in the Java language.
The?Class?class contains a method for determining modifiers:
Class<?> clazz = Integer.class;
assertEquals(0b00000000_00010001, clazz.getModifiers());
In addition to the?getModifiers?method, which returns modifiers encoded as bits in an integer, the?Class?class also contains methods for determining the kind of this type:
Class<?> clazz = Integer.class;
assertFalse(clazz.isPrimitive());
The Modifier class
The?Modifier?class is a utility class with static methods for decoding the Java language modifiers for classes, interfaces, fields, methods, constructors, and parameters.
This class declares the methods to check for the presence of individual bits in the specified modifiers parameter:
Class<?> clazz = Integer.class;
int modifiers = clazz.getModifiers();
assertTrue(Modifier.isPublic(modifiers));
assertTrue(Modifier.isFinal(modifiers));
assertEquals("public final", Modifier.toString(modifiers));
Also, this class declares the methods to get all possible modifiers that can be applied to classes, interfaces, fields, methods, constructors and parameters:
assertEquals(0b00001100_00011111, Modifier.classModifiers());
assertEquals("public protected private abstract static final strictfp", Modifier.toString(Modifier.classModifiers()));
Primitives
The primitive types are the?boolean?type, the numeric types, and the pseudo-type?void. The numeric types include the integral types?byte,?short,?int,?long, and?char?and the floating-point types?float?and?double.
The?Class?class contains a method for determining primitives:
assertTrue(int.class.isPrimitive());
assertTrue(Integer.TYPE.isPrimitive());
Arrays
Arrays are objects that can contain several variables (array components) that are referenced by non-negative integer index values. The array components may themselves be arrays. The array components of the lowest nested array that are not themselves arrays are called array elements.
For example, for an array of type?int[][][], the component type is?int[][], and the element type is?int.
Every array has an associated?Class?object, shared with all other arrays with the same component type. Although an array type is not a class, the?Class?object of every array acts as if its direct superclass is the?Object?class and the direct superinterfaces are the?Cloneable?and?Serializable?interfaces.
The class?Class?contains methods for determining arrays:
Class<?> clazz = Integer[].class;
assertTrue(clazz.isArray());
assertEquals(Integer.class, clazz.componentType());
assertEquals(Integer[][].class, clazz.arrayType());
The Array class
The?Array?class is a utility class with static methods for dynamically creating new arrays and getting and setting array components.
This class declares the following methods:
(*) The?Array?class also declares similar methods for getting and setting components for?boolean,?byte,?short,?int,?long,?char,?float,?double?arrays.
Object array = Array.newInstance(int.class, 1);
assertTrue(array.getClass().isArray());
assertEquals(1, Array.getLength(array));
assertEquals(0, Array.getInt(array, 0));
Array.setInt(array, 0, 2);
assertEquals(2, Array.getInt(array, 0));
Inheritance
The?Class?class has three groups of methods for analyzing type inheritance.
The first subgroup of these methods is for determining the direct supertypes (that is, which class and interfaces are mentioned in the?extends?and?implements?clauses of the class or interface declaration):
Class<?> clazz = ArrayList.class;
assertEquals(AbstractList.class, clazz.getSuperclass());
assertArrayEquals(
new Class[]{List.class, RandomAccess.class, Cloneable.class, Serializable.class},
clazz.getInterfaces()
);
The second subgroup of these methods is for determining whether one type can be cast to another type (that is, whether one type is the same type or a subtype of another type):
The?isInstance?method is the runtime equivalent of the compile-time?instanceof?operator.
assertTrue(Number.class.isInstance(Integer.valueOf(1)));
assertTrue(Number.class.isAssignableFrom(Integer.class));
The third subgroup of these methods is for casting one type to another type:
领英推荐
The?cast?method is the runtime equivalent of the compile-time?cast operator.
Number number = Integer.valueOf(1);
Integer integer = Integer.class.cast(number);
assertEquals(1, integer);
Number number = Integer.valueOf(1);
Class<? extends Number> numberClass = number.getClass();
Class<? extends Integer> integerClass = number.getClass().asSubclass(Integer.class);
Special classes and interfaces
The?Class?class has methods for analyzing special kinds of types: types of different nesting (top-level and nested classes and interfaces), restricted types (enums, records, sealed classes and interfaces), and types with special implementation (synthetic and hidden classes and interfaces).
Nested classes and interfaces
There are different kinds of classes depending on which enclosing context they are declared in.
Top-level classes are classes declared directly in compilation units.
Nested classes are classes declared within the body of another class or interface declaration. Nested classes may be static or non-static member classes, local classes, or anonymous classes. Some kinds of nested classes that are not explicitly or implicitly?static?(and therefore can refer to instances of the enclosing classes) are inner classes.
The?Class?class has four groups of methods for analyzing nested classes and interfaces.
Kinds of nested classes
A member class is a class whose declaration is directly enclosed in the body of another class or interface declaration. A local class is a class declared within a block. An anonymous class is a class without a name, also declared within a block, that is implicitly declared by a?class instance creation expression?or?by an enum constant with a class body.
The first subgroup of these methods is for determining the kind of nested classes:
Public vs. declared members
A class inherits?public,?protected,?package access?(both?static?and non-static) but not?private?member classes and member interfaces from its superclass and superinterfaces.
The second subgroup of these methods is for determining member classes and member interfaces:
Enclosing vs. declaring context
Only a member class has a declaring class - the top-level class or interface of which it is a member. A local class or an anonymous class is not a member of any class or interface and therefore does not have a declaring class.
The third subgroup of these methods is for determining enclosing context (method, constructor, or class) for local and anonymous classes and declaring context (only class) for member classes:
Nest-Based Access Control
Nest-based access control (since Java 11) is a new implementation of nested classes that allows them to access other private members without the Java compiler having to insert?synthetic?accessibility-broadening bridge methods.
Nest-based access control replaced synthetic bridge methods used to access?private?members between a top-level class and its nested classes.
A top-level class, plus all classes nested within it, are forming a nest. All classes in a nest allow mutual access to their?private?members. Every class belongs to exactly one nest. A top-level class that forms the nest is described as a nest host. Two classes in a nest are described as nestmates.
The fourth subgroup of these methods is for determining nests:
Enum classes
Enum classes (since Java 5.0) are restricted kinds of classes that define a fixed set of named class instances (enum constants).
If an enum constant is declared with a class body, the class of that enum constant is an anonymous class and not the enum class where this enum constant is declared.
The?Class?class declares the methods for determining enum classes:
enum SomeEnum {
INSTANCE
}
Class<SomeEnum> clazz = SomeEnum.class;
assertTrue(clazz.isEnum());
assertEquals(Enum.class, clazz.getSuperclass());
Synthetic classes and interfaces
Synthetic classes and interfaces (since Java 1.3) are generated by the Java compiler and are not explicitly or implicitly declared.
A construct emitted by a Java compiler is marked as?synthetic?if it does not correspond to a construct declared explicitly or implicitly in the source code (unless the emitted construct is a?class initialization method).
The?Class?class declares the method for determining synthetic classes:
Class<? extends Runnable> clazz = ((Runnable) () -> {}).getClass();
assertTrue(clazz.isSynthetic());
System.out.println(clazz.getSimpleName()); // SyntheticClassTest$$Lambda$363/0x0000000800d327b8
Hidden classes and interfaces
Hidden classes and interfaces (since Java 15) are a new implementation of runtime generated classes that are not visible to other classes either at compile-time or runtime by class loaders (for example, by the?Class::forName?method). A hidden class is created by invoking the?java.lang.invoke.Lookup::defineHiddenClass?method with the?class?file as the byte array argument.
Hidden classes have replaced the anonymous classes created by the?sun.misc.Unsafe::defineAnonymousClass?method that was deprecated in Java 15 and removed in Java 17.
The?Class?class declares the method for determining hidden classes:
byte[] classFile = getClassFileAsBytes(SomeClass.class);
Class<?> clazz = MethodHandles.lookup()
.defineHiddenClass(classFile, true, ClassOption.NESTMATE)
.lookupClass();
assertTrue(clazz.isHidden());
System.out.println(clazz.getSimpleName()); // SomeClass/0x0000000800d34000
Record classes
Record classes (previewed in Java 14 and released in Java 16) are restricted kinds of classes that define an immutable aggregate of values. From record components in the record header, the Java compiler created the following implicit constructs:
The?Class?class declares the methods for determining record classes:
record SomeRecord(int i) {
}
Class<?> clazz = SomeRecord.class;
assertTrue(clazz.isRecord());
assertTrue(Modifier.isFinal(clazz.getModifiers()));
The?RecordComponent?class declares the methods for determining record components:
RecordComponent[] components = clazz.getRecordComponents();
assertEquals(1, components.length);
RecordComponent component = components[0];
assertEquals(clazz, component.getDeclaringRecord());
assertEquals("i", component.getName());
assertEquals(int.class, component.getType());
assertEquals("public int demo.SomeRecord.i()", component.getAccessor().toString());
Sealed classes and interfaces
Sealed classes and interfaces (previewed in Java 15 and released in Java 17) restrict which other classes or interfaces may extend or implement them. A sealed class has the?sealed?modifier to its declaration and the?permits?clause that specifies the classes that may extend the sealed class. Permitted subclasses must directly extend the sealed class and must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:
The?Class?class declares the methods for determining sealed classes and interfaces:
sealed class Polygon permits Triangle, Quadrangle, Pentagon {}
final class Triangle extends Polygon {}
sealed class Quadrangle extends Polygon permits Parallelogram, Trapezoid, Kite {}
non-sealed class Pentagon extends Polygon {}
Class<?> clazz = Polygon.class;
assertTrue(clazz.isSealed());
assertEquals(
Set.of(Triangle.class, Quadrangle.class, Pentagon.class),
toSet(clazz.getPermittedSubclasses())
);
Conclusion
The?Class?class is the entry point for the Core Reflection API. Once you obtain a?Class?object for a type, you can determine many of the properties of this type: the kind of type (primitive type, array type, class or interface), modifiers, specification and implementation details.
Structures in the source code do not always have a one-to-one correspondence with structures in the generated?class?file. In some cases, reflection reveals that the generated?class?file has?implicit?constructs (absent in the source code but mandated by the Java Language Specification) or?synthetic?constructs (generated by a Java compiler in a way that is not specified by this specification).
Complete code examples are available in the?GitHub repository.