Reflection Manifesto

Maurizio Cimadamore, June 2016, version 0.3

Reflection is a very common feature of modern programming languages - as best stated in Wikipedia:

In object oriented programming languages such as Java, reflection allows inspection of classes, interfaces, fields and methods at runtime without knowing the names of the interfaces, fields, methods at compile time. It also allows instantiation of new objects and invocation of methods.

In Java, reflection is exposed to the programmer in a very simple way: the top type Object defines a method - namely getClass - which returns a reflective view of the object's class - an instance of type Class. In addition, the language syntax offers some basic reflection capability, in the form of class literals such as String.class - which allow the programmer to obtain the Class object associated with a given (statically known) type.

Once the programmer has an hold on a Class instance, he is then free to perform various tasks, such as (but not limited to):

In other words, programmers have full access to advanced meta-programming facilities, with relatively little effort on their part.

Changelog

0.3

0.2

Notation

As we shall see in the remainder of this document, the terms 'class' and 'type' are becoming increasingly overloaded. To avoid confusion, this section gives a brief overview on how these terms are used in the context of this document.

First, we use the term 'class' to denote a class declaration in the source code - such as:

class Bar {
   void doSomething() { ..}
}
 
class Foo extends Bar { ..}

A class declaraton has certain (static) properties - for instance, a class has a name (Foo) and a superclass (Bar) and so forth.

Types are divided in two categories: source types and runtime types. The term source type is used to denote an expression in a source program which describes the set of operations available in a given value. For instance, variable declarations must come with a type to make sure that the variable is used accordingly:

Bar bar = new Foo();
bar.doSomething();

In the above, the variable bar is given the type Bar - which allows the compiler to prove statically that bar has indeed a method named doSomething. In a nominal language - such as Java - a class name (as spelled in the declaration) is also a type - i.e. it can be used as a type expression.

The term runtime type is used to denote the runtime information associated with a live object in the JVM; it is important to notice that the source type of an expression doesn't always correspond to the runtime type obtained after the expression has been evaluated: in the example above, foo has a source type Bar, while the runtime type associated with the live objects pointed to by the variable is Foo. In Java, the runtime type of an expression is generally a subtype of the source type associated with the same expression (this is indeed a property of all sound languages featuring subtyping).

Note that, even though the same name Foo is used - with a slight abuse of notation - to denote both the source type and the runtime type associated with the declaration of a class named Foo, source types and runtime types should be considered as different entities with different domains and lifecyecles; source types are generally available statically, at compilation time (in fact the compiler needs them to prove properties about the correctness of a program) - runtime types, on the other hand, belong to the runtime environment and are created during the execution of a program (for instance, the JVM verifier needs runtime types to prove properties about the well-formedness of a classfile). An extreme example of this important distinction is given below:

ClassLoader cl1 = new URLClassLoader(...);
ClassLoader cl2 = new URLClassLoader(...);
 
Class<Foo> c1 = (Class<Foo>)Class.forName("Foo", cl1);
Class<Foo> c2 = (Class<Foo>)Class.forName("Foo", cl2);
 
System.out.println(c1 == c2); //prints false! 

The above program is creating two runtime types for the same source type Foo - this is possible because in the runtime environment any given type is uniquely represented by a name and the classloader used to load that type. Since the runtime types above have been created with different loaders, they are treated as distinct runtime types by the JVM, even though the source language cannot distinguish between the two - in fact, the comparison c1 == c2 is accepted by the static compiler, as there's just one source type Foo.

Source types can be mapped onto runtime types; since the runtime type system is usually less expressive than the source type system, such mapping can be lossy, where multiple source types are mapped onto a single runtime type; an extreme example of such a lossy translation is, as we shall see, the so called erasure generic translation scheme added in Java 5; another more benign example is numerics, with the JVM handling only a subset (namely int, float, long, double) of all the numeric types available in the source language.

Core reflection: an evolutionary tale

Core reflection has been designed in a world where the following two strong assumptions on source and runtime types held:

This conspiracy between source types, runtime types and declarations gave users the illusion that there was very little (or nothing at all) to distinguish elements in such domains (at least in a context where a single classloader was used - see discussion in the above section); in other words, it was as if the JVM could operate directly on the entities expressed in the source program. Over the course of years, as new features were added to the Java language, these assumption started to tremble.

Inner classes (added in Java 1.1) were perhaps the first language feature which required significant desugaring from the static compiler; such desugaring includes:

In other words, it was now possible to have differences between a source code artifact and a classfile artifact. In certain cases such differences were small - such as an extra ACC_SYNTEHTIC method available in the classfile; in other cases differences were more significant: the resulting classfile output had in fact no notion of the lexical scoping relationship between classes, and such information was only recorded inside certain classfile attributes (such as InnerClasses and EnclosingMethod). Additionally, all constructors of member inner classes had to be touched in order to accept an extra parameter carrying the desired enclosing instance. While none of the above assumptions on source vs. runtime types has been broken yet, the mapping between source types and runtime types had suddenly gotten more convoluted - with nested source types mapping into non-nested runtime types. What to do about reflection then? Does Class expose a flat, VM-friendly view of the world (i.e. is it a runtime type)? Or does it preserve the original lexical scoping information, recovered by the classfile attributes (i.e. is it a declaration)? As the latter option was chosen - Class effectively started to exhibit both declaration features (i.e. lexical scoping) and runtime type features (i.e. each Class has a ClassLoader). For instance, the name of the runtime type Foo$Bar will be presented in a source-friendly way as Foo.Bar; while this attempt at bridging the gap between source and runtime types is not new - binary names such as java/lang/Object have historically been presented as java.lang.Object - inner class reflection support take this bridging activity to a whole new level.

Eventually, both type assumptions were brought down with the addition of generics in Java 5. Now, a single declaration could be generic in one or more type parameters; this means that a generic class such as List<X> can suddenly be mapped into an infinite set of types: List, List<String>, List<Integer>, etc. Moreover, since the static compiler used a translation technique called erasure, such source types were all mapped into the same runtime type - that of List. In other words, the set of source types was bigger than the set of runtime types. Of course, small cracks started to appear; for instance, the type of a superclass, field, or method could now contained type-arguments; such type information was reified in a special classfile attribute called Signature (so that the static compiler could reconstruct it). So, a new question for core reflection was: should reflective entities expose source types (available from the Signature attribute) or runtime types (hence closer to a descriptor-based view of the world)?

No perfect answer existed, given the ambiguous nature of Class. For clients viewing Class as a declaration it made perfect sense to have the source type information readily available. On the other hand, for clients viewing Class as a runtime type it made sense to stick to a descriptor-based view, as that was closer to the JVM. To reconcile these opposing forces, core reflection eventually ended up with a parallel hierarchy called java.lang.reflect.Type and a set of additional methods on all reflective classes to expose source types (in addition to runtime types, as usual). The inherent ambiguous role of the Class abstraction became manifest as Class was retrofitted to implement the new Type interface. So Class was now effectively playing three roles: declaration, runtime type and (more subtly) source type (since Class was used to model raw types in this new hierarchy).

This fact had nefarious consequences in JDK 8 when type annotations were added, and eventually a new parallel hierarchy of reflective interfaces was added - namely java.lang.reflect.AnnotatedType - meaning reflective entities now exposed two different flavors of source types: generic types as available in the Signature attribute (as of JDK 5), as well as annotated generic types (this view was obtained by combining the information available in the Signature attribute with that available in the RuntimeVisibleTypeAnnotations attribute). The extra hierarchy was necessary because Class was both a declaration and a source type (through the newly acquired Type interface). If methods for retrieving (type) annotations were added on the top Type interface, Class would have inherited them too - and this would have led to endless confusion between declaration annotations and type annotations.

As the basic source vs. runtime types assumptions were progressively broken, the maintenance cost of core reflection went up considerably - as users expected core reflection to hide away most of the complexity of the generated code and give them back a model that was more or less resemblant of what was available in the source. This meant having a story for exposing accessibility of inner classes, supporting bridge methods, and much more. Frequently, as changes occur in the Java language, updates are required for core reflection to function properly. In certain cases, it was even necessary for the static compiler to emit special artifact whose goal was only to support reflective features (as accessibility bridges).

Even worse, as the impedance mismatch between language and core reflection grew, it has become increasingly difficult for users to simulate some of the more advanced type-system features such as type substution, subtyping etc. Reasoning about these properties in the core reflection world often requires non-trivial mental gymnastic, as users have to re-implement a type-system from scratch.

And now, with Valhalla, we are on the verge of another evolutionary leap; with type-specialization, the function that maps source types into runtime types has become more convoluted, as certain types are no longer erased. This means that source types like List<int> now have a runtime type counterpart (expressed in a constant pool entry of the kind ParameterizedType[List, I]). This raises new questions - should Class be extended to model these new types? What do these new runtime types have in common with the old erased runtime type List ? What should Object.getClass() return when invoked on a specialized receiver? Should class literals be enhanced to cover these new cases? As we shall see later in this document, some of these questions are hard, if not impossible to answer within the current core reflection framework.

Core reflection: a closer look

At a very high level, there are three basic layers provided by reflection:

In the declaration layer, we find those operations that are meant to expose properties of a declaration associated with e.g. a given Class object, such as the class name, the class flags and so forth. Since a declaration is made up of types (e.g. a class declaration mentions a supertype, a method declaration must provide the method signature, etc.), it is natural for the this layer to also include functionalities for accessing types mentioned in the corresponding declaration. As noted before, since core reflection exposes both runtime types and source types, there is a proliferation of accessor methods and duplication. For instance, the parameter types of a method must be accessible in its erased form (getParameterTypes), in its generic form (getGenericParameterTypes) and in its annotated form (getAnnotatedParameterTypes). Member lookup is another important category of the declaration layer, as it gives access to member types, methods, fields and constructors enclosed in a given declaration. Again we note some duplication here, as the same reflective entities can be looked up in subtly different ways; for instance, getDeclaredMethods returns all non-synthetic methods available in a class, while getMethods returns all public methods (including inherited ones).

In the type layer we find operations to manipulate runtime types, such as performing subtyping and comparison tests, in additon to operations which reveal dynamic properties about the runtime type which are simply not available at compile-time (getClassLoader).

Finally, we find the operational layer where we find operations which allow users to programmatically access fields, invoke methods, construct new instances. Such operations tend to follow the JVM semantics quite closely: for instance there is little distinction between static and instance members - an instance method is just a method which takes one more argument (the receiver). It is important to notice how the features provided by this layer are essentially equivalent to those provided by more modern abstractions such as MethodHandle and VarHandle (we will return on this point later in this document).

One might think that cramming multiple orthogonal abstractions into the same entity would yield to issues; in the case of core reflection, it's the opposite - the fact that the three layers are embodied by all the main reflective classes (i.e. Class, Method, Field and Constructor) leads to a very simple programming model, and strikes a great balance between complexity and usability. Look at the example below:

public class ReflectionTest {
 
    String getClassName(Object o) {
        return o.getClass().getSimpleName();
    }
 
    String getPackageName(Object o) {
        return o.getClass().getPackage().getName();
    }
 
    boolean isStrictFP(Object o) {
        return Modifier.isStrict(o.getClass().getModifiers());
    }
 
    String superClassName(Object o) {
        return o.getClass().getSuperclass().getName();
    }
 
    void printMethodNames(Object o) {
        Stream.of(o.getClass().getDeclaredMethods())
                .forEach(-> System.out.println(e.getName()));
    }
 
    void printFieldNames(Object o) {
        Stream.of(o.getClass().getDeclaredFields())
                .forEach(-> System.out.println(e.getName()));
    }
 
    Object createObjectUsingStringConstructor(Object o) throws Throwable {
        Constructor<?> c = o.getClass().getDeclaredConstructor(String.class);
        return c.newInstance("Hello!");
    }
 
    void resetField(Object o) throws Throwable {
        Field f = o.getClass().getDeclaredField("someField");
        Object val = f.get(o);
        f.set(o, val);
 
    }
 
    void printStaticTypeArgsOfMethodReturn(Object o) throws Throwable {
        Method meth = o.getClass().getDeclaredMethod("someMethod", Object.class);
        Type t = meth.getGenericReturnType();
        if (instanceof ParameterizedType) {
            Stream.of(((ParameterizedType)t).getActualTypeArguments())
                    .forEach(ta -> System.out.println("Type arg = " + ta));
        }
    }
}

As the reader might appreciate, most of the code above is almost self-explanatory; most of the operations the user would expect to be able to do are directly available on Class, which is the main entry point for standard core reflection. As a result, code for looking up members is very succinct. At the same time, since reflective classes for method, fields and constructors acts both as declarations (i.e. you can inspect a method name, flags, etc.) and reflective objects (i.e. you can programmatically invoke a method with given arguments), the result of the lookup can directly be used to perform the desired reflective operations.

Where things start to break down is, unsurprisingly, when we start asking questions about generics; here the user has to remember to use the right type accessor method (i.e. getGenericReturnType and not getReturnType). Additionally, the user must know that the result of this type access is going to be a ParameterizedType instance, and cast appropriately to get to the type-arguments info. While this is all doable, we must also note that the resulting code is more convoluted than the typical non-generic core reflection code illustrated in the former examples.

As an additional note, since all core reflection classes use array types as aggregates, certain operations, such as turning the list of parameter types into a stream are not as straightforward as they could be.

Source reflection

In JDK 6, a new hierarchy of classes was introduced under javax.lang.model to model entities of the Java programming language. This API is meant to give users a way to perform source reflection in the same way java.lang.reflect allows user to do runtime reflection. The javax.lang.model API (JLM henceforth) is a much modern API, and has born with generic types in mind. Among the benefits of this API:

Can this newer API help mitigate some of the growing pains associated with core reflection? Well, it seems like it should - after all, one of the core idea behind the JLM API is to cleanly separate between declarations and types by borrowing the symbol (Element) vs. type (TypeMirror) distinction so familiar to compiler implementors. Let's see how this would work in practice; let's start by defining a very general Reflection class which exposes useful methods for working with an hypothetical JLM-backed reflection:

class Reflection {
    //factory 
    static TypeMirror typeMirrorFor(Object o) { ..}
    //helper objects 
    static Types getTypes() { ..}
    static Elements getElements() { ..}    
    //operational layer 
    static MethodHandle handleFor(ExecutableElement exTypeMirror site) { ..}
    static VarHandle handleFor(VariableElement veTypeMirror site) { ..}
}

There's not much to see here; the class is a very simple entry point which allows programmers to construct new runtime types for a given instance; also, Reflection exposes accessors to retrieve JLM-based classes such as Elements and Types which can be helpful when working on reflective code. The reflective layer is embodied by a couple of methods which can be used to turn a reflective member into a method/var handle; since a member type is identified uniquely by the pair Element (the member declaration), TypeMirror (the type in which the member is accessed), such routines need to accept both arguments.

Below is an attempt to rewrite our usability test (shown in the previous section), this time by taking advantage of a JLM-like reflection API:

class JLMReflectionTest {
 
    String getClassName(Object o) {
        return ((DeclaredType)Reflection.typeMirrorFor(o)).asElement()
                .getSimpleName().toString();
    }
 
    String getPackageName(Object o) {
        return Reflection.getElements().getPackageOf(((DeclaredType)Reflection.typeMirrorFor(o)).asElement())
                .getSimpleName().toString();
    }
 
    boolean isStrictFP(Object o) {
        return ((DeclaredType)Reflection.typeMirrorFor(o)).asElement()
                .getModifiers().contains(Modifier.STRICTFP);
    }
 
    String superClassName(Object o) {
        return ((DeclaredType)((TypeElement)((DeclaredType)Reflection.typeMirrorFor(o)).asElement())
                .getSuperclass()).asElement()
                .getSimpleName().toString();
    }
 
    void printMethodNames(Object o) {
        ((DeclaredType)Reflection.typeMirrorFor(o)).asElement()
                .getEnclosedElements().stream()
                .filter(-> e.getKind() == ElementKind.METHOD)
                .forEach(-> System.out.println(e.getSimpleName()));
    }
 
    void printFieldNames(Object o) {
        ((DeclaredType)Reflection.typeMirrorFor(o)).asElement()
                .getEnclosedElements().stream()
                .filter(-> e.getKind() == ElementKind.FIELD)
                .forEach(-> System.out.println(e.getSimpleName()));
    }
 
    Object createObjectUsingStringConstructor(Object o) throws Throwable {
        DeclaredType site = (DeclaredType)Reflection.typeMirrorFor(o);
        List<ExecutableElement> constructors = site.asElement()
                .getEnclosedElements().stream()
                .filter(-> e.getKind() == ElementKind.CONSTRUCTOR)
                .map(-> (ExecutableElement)e)
                .collect(Collectors.toList());
 
        Element strParam = Reflection.getElements().getTypeElement("java.lang.String");
 
        ExecutableElement constr = constructors.stream()
                .filter(-> ((ExecutableType)c.asType()).getParameterTypes().size() == 1 &&
                        Reflection.getTypes().isSameType(((ExecutableType)c.asType()).getParameterTypes().get(0), strParam.asType()))
                .findFirst().get();
 
        return Reflection.handleFor(constr, site).invoke("Hello!");
    }
 
    void resetField(Object o) {
        DeclaredType site = (DeclaredType)Reflection.typeMirrorFor(o);
        VariableElement field = (VariableElement)site.asElement()
                .getEnclosedElements().stream()
                .filter(-> e.getKind() == ElementKind.FIELD && e.getSimpleName().contentEquals("someField"))
                .findFirst().get();
 
        VarHandle vh = Reflection.handleFor(field, site);
        Object val = vh.get(o);
        vh.set(o, val);
    }
 
    void printStaticTypeArgsOfMethodReturn(Object o) {
        DeclaredType site = (DeclaredType)Reflection.typeMirrorFor(o);
        ExecutableElement method = (ExecutableElement) site.asElement()
                .getEnclosedElements().stream()
                .filter(-> e.getKind() == ElementKind.METHOD && e.getSimpleName().contentEquals("someMethod"))
                .findFirst().get();
        TypeMirror rmirror = method.getReturnType();
        if (rmirror.getKind() == TypeKind.DECLARED) {
            ((DeclaredType)rmirror).getTypeArguments().stream()
                    .forEach(ta -> System.out.println("Type arg = " + ta));
        }
    }
}

As it can be immediately perceived, this code is not nearly as easy to read, nor as concise as the old, Class-based code was. The new code is nearly double the size. The major issue with the new code is that looking up things is very expensive - with standard style core-reflection-based one liners becoming 3-4 lines using complex stream filters. This is due to the fact that lookup is a function of the element (read declaration) world, and not of the type world. So, one has always to go from a runtime type to its element.

Unfortunately, doing so is not straightforward either: since not all TypeMirrors are guaranteed to implement asElement, a cast is always necessary to go from a RuntimeTypeMirror to its corresponding element. This greatly reduce the readability of the code.

Problems don't stop here - once we finally have an element, we must find what we were looking for - and here comes another major usability blow, as the user has to manually filter the elements (as the default member lookup routines in Element provide no filtering capabilities), and he also has often to cast to the desired more specific Element subclass (for instance, for methods the user has to cast to ExcutableElement). Filtering based on signatures is also hard, given that the user has to retrieve the type of the element, cast it to the right interface (i.e. for methods, type is ExecutableType) and then filter out information as required. This seems very, very verbose, and the learning curve also seems unnecessarily steep, given the users now have to have basic knowledge of two parallel hierarchies of classes (Element and TypeMirror) before even basic operations can be performed.

We also note other annoying things, such as the fact that now all name-accessing routines return overly general instance of the Name interface, instead of returning String objects.

In conclusion, while a JLM-backed API would allow for a cleaner separation between declarations and types, we deem the usability tax it imposes on programmers as too high. While the API might be well suited to compiler-minded developers (and indeed, this API really shine when performing complex type calculations), it doesn't score so well on typical reflection usages such as the ones shown above. Our quest continues.

So you want a new reflection API?

There seem to be contrasting use cases at play here; on the one hand we need an API that avoids the pollution of the existing reflection classes, so as to more clearly map the distinction between declarations and runtime types. On the other hand we'd also like to retain the simplicity of the current reflection API - and we have seen how using a more structured API such as JLM represents a clear departure from that simple model.

There's also a less obvious constraint which comes from compatibility; suppose we did come up with a new API for reflecting over specializable classes; how would we teach existing Class-based APIs to interoperate with such a new API? Let's consider a concrete example:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
 
class TestHandle {
    public static void main(String[] args) throws Throwable {
        testErased();
        testSpecialized();
    }
 
    static void testErased() throws Throwable {
        MethodHandle mh = MethodHandles.lookup().findVirtual(A.class, "m", MethodType.methodType(void.class, Object.class));
        Object o = mh.invoke(new A<String>(), "");
    }
 
    static void testSpecialized() throws Throwable {
        MethodHandle mh = MethodHandles.lookup().findVirtual(A<int>.class, "m", MethodType.methodType(void.class, int.class));
        Object o = mh.invoke(new A<int>(), 42);
    }
}
 
class A<any X> {
    void m(X x) { }
}

This program works, and, one could say, also does what it says on the tin. You can lookup an erased member, or a specialized member, and the method handle infrastructure will add the required cast, etc - in other words, everything just works. Being able to say List<int>.class (a feature currently implemented in the Valhalla prototype), have it evaluate to the runtime type for the specialized class and being able to pass that where a Class is expected is, as pointed out in the above example, hugely useful, as it maintains the investment in a lot of existing code and APIs.

So maybe a new API is not needed after all? Well, not so fast; first of all the term 'class' is overloaded; on the one hand it would be handy if .class and getClass() returned an object representing a declaration rather than a runtime type (e.g. the Class object associated with the template class) - this is also the approach explored in model 4 and 5, and the one which guarantees maximum compatibility guarantees with respect to the existing code (i.e. getClass preserves its meaning in the new specialized world).

A possibility would be to add new class literals and new methods to Object - for instance, a specialized literal of the kind List<int>.sclass and an Object.getSpecializedClass method; these new operations would allow the user to retrieve the specialized runtime representation associated with a source type or an instance. Since such representation would still be a Class, the user could still pass a List<int>.sclass where a Class is expected, as in the example above. Legacy API are rescued.

This would indeed be a very minimalistic, but feasible approach; we could tweak Class to implement java.lang.reflect.ParameterizedType instead of just Type - and then have Class also implement a method to retrieve the actual type argument list associated with a specialized class (this method could throw when invoked on the unspecialized, template class, or could just return the list of type parameters declared by that class).

While doable, this approach has one fundamental flaw: it fails to precisely capture the semantics of the new abstractions available in the generic bytecode; after all the java.lang.reflect.Type hierarchy is modeled after source types (the same holds for the newer hierarchy java.lang.reflect.AnnotatedType). As such, this hierarchy can model types that are richer than those directly expressed in the generic bytecode - which, for instance, has no notion of bounded wildcards. At the same time, this hierarchy cannot model types that are available at the bytecode level but which do not have any direct corresponding type in the source code (i.e. the erased token). To properly map the entities available in the bytecode, we'd need yet another type hierarchy in java.lang.reflect, and connect that hierarchy to Class - which means Class will be polluted by yet another set of getXYZSuperclass-like methods. It can be seen now that, while pragmatic, this approach is far from ideal.

One mirror to rule 'em all

In this section we'll present a new API to reflect over specialized types. The goal of this new API is to capture the full set of abstractions available in the generic bytecode format - as such, this API does not make an effort to capture the full set of types (and properties) available in the source language. In that respect, it can be thought of as a bytecode-oriented API.

This API needs to be simple to learn and to use - which is one of the strong assets of core reflection; in order to achieve that, we will need to abandon the theoretical distinction between declarations and types (which highlighted many usability problems - see section on JLM-backed reflection API). But we will need to fold these two concepts in a precise way, to avoid the conceptual overload that is currently afflicting the core reflection API. As we shall see, this is achieved by representing both non-specialized and specialized entities using the same abstractions; the only tangible difference is that a non-specialized entity will not carry concrete type-argument witnesses as the specialized counterpart - and will instead carry the specializable type-variables declared by that same entity. This effectively allows us to describe both a template class (resp. a template method) and a specialized class (resp. method) using the same abstractions, which, as we shall see, results in a big usability win.

As we noted in the previous section, the new API will need to support some kind of interoperability with respect to core reflection, so as to maintain the investment existing legacy (and not so legacy - as in the case of 292) code. For this reason, our new API will have a way to project its entities back to core-reflection entities.

Another goal of this new API is to overcome some of the more pragmatic limitations of the ageing core reflection API, such as the lack of a declarative way to perform member lookups, as well as the use of plain arrays in operations returning aggregates elements.

Finally, we want this new API to present a richer set of choices when it comes to operational reflection; as we shall see, method entities can be mapped not only to plain old Method objects, but also to MethodHandle (and more choices might be available in the future). In other words, we want this API to separate the structural concerns (i.e. the abstractions used to inspect the signature of a method) from the operational concerns (i.e. the abstractions used to invoke that method).

API overview

RuntimeMirror is the common superinterface for all types representable in a generic classfile. These include parameterized types, array types, type variables, primitive types and special type-arguments (such as erased or any). There are six kinds of mirrors; each mirror has a unique kind (see RuntimeMirror.Kind) and can be created using the corresponding factory method in MirrorFactory, as summarized in the table below:

Kind Class Factory RuntimeMirror.Kind
class ClassMirror MirrorFactory.classMirror(Class) RuntimeMirror.Kind.CLASS
array ArrayMirror MirrorFactory.arrayMirror(Class)
MirrorFactory.arrayMirror(RuntimeMirror)
RuntimeMirror.Kind.ARRAY
type-variable TypeVariableMirror MirrorFactory.typeVarMirror(TypeVariable) RuntimeMirror.Kind.TYPEVAR
primitive RuntimeMirror MirrorFactory.primitiveMirror(Class) RuntimeMirror.Kind.VALUE
erased RuntimeMirror MirrorFactory.erasedMirror() RuntimeMirror.Kind.ERASED
any RuntimeMirror MirrorFactory.anyMirror() RuntimeMirror.Kind.ANY

Every runtime mirror support basic operations such as:

Additionally, each mirror class provide some specific structural access to the mirror's contents; for instance, array mirrors provide access to the component type (see ArrayMirror.getComponentType()), class mirrors give access to supertypes (see ClassMirror.getSupertypes()), etc.

It is very likely that, moving forward, some of the mirrors - such as class, array and primitive mirrors - will be lumped together; that will depend largely on whether these mirrors would provide similar enough capabilities (e.g. does it make sense to perform a lookup on an array mirror? What about a primitive?).

Specialization support

Some mirrors are specializable (see GenericMirror); when a mirror is specializable, it contains one or more type-variables, which are accessible using GenericMirror.getTypeArguments(). If a mirror is specializable, it can be specialized using the method GenericMirror.asSpecializedMirror(RuntimeMirror...) - as follows:

RuntimeMirror typeArgs = { ..};
if (mirror.isSpecializable()) {
   ClassMirror s_mirror = mirror.asSpecializedMirror(typeArgs);
}

The resulting mirror will be obtained by replacing the type-variable in the specializable mirror with the provided actual type-arguments. Once a specialized mirror is obtained, one can always go back to the non-specialized version - using GenericMirror.asGenericMirror():

assertEquals(s_mirror.asGenericMirror(), mirror);

There are two kinds of generic mirrors: class mirrors and method mirrors (since both classes and methods can be specialized).

Member lookup

Some mirrors support lookup operations (see ScopeMirror). Such mirrors can be asked the list of classes, methods, fields and constructors defined within themselves (either directly or indirectly, through inheritance). The lookup operations are supported through lookup objects (see MemberLookup. The table below groups the available lookup objects by mirror kind.

Member kind Class MemberLookup
class ClassMirror ClassLookup
field FieldMirror FieldLookup
method MethodMirror MethodLookup
constructor ConstructorMirror ConstructorLookup

Lookups can be either loose or strict, depending on whether the method MemberLookup.findAll() or MemberLookup.findOrFail() is used to perform the lookup. A loose lookup looks like the following:

mirror.methodLookup().findAll();

Which returns all methods in mirror (either declared or inherited).

If the user knows more about the method to be looked up, a more precise lookup can be attempted:

RuntimeMirror[] paramTypes = { ..}
mirror.methodLookup()
   .withName("foo")
   .withParameters(paramTypes)
   .findOrFail();

A strict lookup can either succeed, and return the only member mirror matched by the lookup - or fail, if no unambiguous match was possible.

Since a method can be generic, strict lookup should work on generic methods too; unfortunately performing a strict lookup involving a generic method is more convoluted because a generic method signature could depend on type-parameters declared by the method itself, which are normally not known ahead of lookup. To address this, lookup methods specifically targeting generic methods are provided which can be used as follows:

mirror.methodLookup()
   .withName("genericFoo")
   .withParameters(tvars -> Optional.of(Arrays.asList(tvars.get(1))));

The above lookup searches for a method whose name is 'genericFoo' and whose parameter list is the generic method's second type-variable. Note that the lambda passed as argument receives the method (possibly empty) type-parameter list and constructs an Optional wrapping the list of method parameter types to be matched. This lambda will be executed on each potential lookup match; if the lambda returns an empty optional for a given method, the lookup will simply ignore that method.

Fields, constructors and member inner classes can also be looked up in a similar fashion, using their corresponding lookup helpers.

Operational features

Some mirrors (see ReflectableMirror) can be projected back into a legacy reflective object using ReflectableMirror.reflect(). In order to perform this projection, a mirror should not contain any open type-variables, or an exception will occur:

Class<?> c_m = mirror.reflect(); //throws! 
Class<?> c_sm = smirror.reflect(); //ok 

This compatibility layer satisfy our goal of interoperability with existing Class-based API, such as MethodHandles.Lookup.

In addition to the basic legacy reflective mapping, some mirrors (like method, field and constructor mirrors) provide extra operational capabilities to, for example, map a method mirror into a method handle (see MethodMirror.asHandle()).

Since operational features are separated from structural features, it is likely that new operational projections will be added in the future; for instance, a method could be mapped into a compatible functional interface, and so on.

Usability

In this section we'll revisit the same usability test case we used to assess core reflection and JLM reflection; below is an attempt to rewrite the usability test using the new RuntimeMirror API:

class RuntimeMirrorReflectionTest {
 
    MirrorFactory factory;
 
    String getClassName(Object o) {
        return factory.classMirror(o.getClass()).getTypeString();
    }
 
    String getPackageName(Object o) {
        return factory.classMirror(o.getClass()).reflect().getPackage().getName();
    }
 
    boolean isStrictFP(Object o) {
        return Modifier.isStrict(factory.classMirror(o.getClass()).getModifiers());
    }
 
    String superClassName(Object o) {
        return factory.classMirror(o.getClass()).getSupertypes().get(0).getTypeString();
    }
 
    void printMethodNames(Object o) {
        factory.classMirror(o.getClass()).methodLookup()
                .withInheritanceMode(InheritanceMode.DECLARED)
                .findAll().forEach(-> System.out.println(e.getName()));
    }
 
    void printFieldNames(Object o) {
        factory.classMirror(o.getClass()).fieldLookup()
                .withInheritanceMode(InheritanceMode.DECLARED)
                .findAll().forEach(-> System.out.println(e.getName()));
    }
 
    Object createObjectUsingStringConstructor(Object o) throws Throwable {
        Constructor<?> c = factory.classMirror(o.getClass()).constructorLookup()
                .withParameterTypes(factory.classMirror(String.class))
                .findOrFail().reflect();
        return c.newInstance("Hello!");
    }
 
    void resetField(Object o) throws Throwable {
        Field f = factory.classMirror(o.getClass()).fieldLookup()
                .withName("someField")
                .findOrFail().reflect();
        Object val = f.get(o);
        f.set(o, val);
    }
 
    void printStaticTypeArgsOfMethodReturn(Object o) {
        MethodMirror mm = factory.classMirror(o.getClass()).methodLookup()
                .withName("someMethod")
                .withParameterTypes(factory.classMirror(Object.class))
                .findOrFail();
        RuntimeMirror ret = mm.getReturnType();
        if (ret instanceof ClassMirror) {
            ((ClassMirror)ret).getTypeArguments().forEach(ta -> System.out.println("Type arg = " + ta));
        }
    }
}

As it can be seen, this sample is almost as concise as the original core-reflection-based test. The main differences are in how mirrors are retrieved (the absence of linguistic support makes mirror creation more verbose); there are also some minor usability issues with respect to accessing declaration properties on a mirror, which requires using reflect() - some of these issues could be ameliorated by adding more declaration-site properties to mirrors (e.g. ClassMirror.getPackage and ClassMirror.getFlags).

That said, there are also clear usability advantages when comparing this code against the core-reflection-based counterpart; first, lookups are expressed in a much more declarative and compositional way using the builder pattern. It is now up to the user to set up the rules which should be followed by the lookup logic, as the lookup does not rely on any hard-wired default combination. Secondly, since all methods return Collection aggregates, interoperability with Stream is much more straightforward than in the core-reflection test. Finally, while not explicitly evident in this test, operational features are exposed in multiple ways - e.g. a method can be reflectively called using a plain old Method object (as shown in the above example), or using a more modern MethodHandle - again, the user is free to choose.

Open issues

There are several areas in which the current API could be refined: