Why Should I Care That Java Doesn't Have Reified Generics

Why should I care that Java doesn't have reified generics?

From the few times that I came across this "need", it ultimately boils down to this construct:

public class Foo<T> {

private T t;

public Foo() {
this.t = new T(); // Help?
}

}

This does work in C# assuming that T has a default constructor. You can even get the runtime type by typeof(T) and get the constructors by Type.GetConstructor().

The common Java solution would be to pass the Class<T> as argument.

public class Foo<T> {

private T t;

public Foo(Class<T> cls) throws Exception {
this.t = cls.newInstance();
}

}

(it does not necessarily need to be passed as constructor argument, as a method argument is also fine, the above is just an example, also the try-catch is omitted for brevity)

For all other generic type constructs, the actual type can easily be resolved with a bit help of reflection. The below Q&A illustrate the use cases and possibilities:

  • Get generic type of java.util.List
  • How to get the generic type at runtime?
  • Get actual type of generic type argument on abstract superclass

What are Reified Generics? How do they solve Type Erasure problems and why can't they be added without major changes?

The whole point is that reified generics have support in the compiler for preserving type information, whereas type erased generics don't. AFAIK, the whole point of having type erasure in the first place was to enable backwards compatibility (e.g. lower versioned JVMs could still understand generic classes).

You can explicitly add the type information in the implementation, as you have above, but that requires additional code every time the list is used, and is pretty messy in my opinion. Also, in this case, you still don't have runtime type checking for all of the list methods unless you add the checks yourself, however reified generics will ensure the runtime types.

Is it possible for Scala to have reified generics without changing the JVM?

No - it is not possible for Scala to run as Java-equivalent bytecode if that bytecode does not support reified generics.

When you ask "what is it that needs to be changed?", the answer is: the bytecode specification. Currently the bytecode does not allow for the parametrized type of a variable to be defined. It has been decided that as a modification to the bytecode to support reified generics would break backwards compatibility, that generics would have to be implemented via type erasure.

In order to get around this, Scala has used the power of its implicit mechanism to define a Manifest which can be imported in any scope to discover type information at runtime. Manifests are experimental and largely undocumented but they are coming as part of the library in 2.8. Here is another good resource on Scala reified generics / Manifests

Why does Java claim 1.5 version need to be backward compatible?

Consider a Java program that was written to use Java collection types prior to Java 1.5.

Prior to Java 1.5, I would write

 List l = new ArrayList();
l.append("Hello");
l.append("World");
String hello = (String) l.get(0);

Now with the type erasure model, I can compile that code in Java 1.5 or later ... without even a compiler warning. But I can also use the exact same collection classes in the modern way; e.g.

 List<String> l = new ArrayList<>();
l.append("Hello");
l.append("World");
String hello = l.get(0);

And note that I am using the same classes and interfaces in both examples.

Without the erasure model to paper over the cracks, the Java designers would have had to create a parallel set of classes and interfaces for collections; i.e.

  • Pre-1.5 collections without generics and type parameters
  • Post-1.5 collections with generics and type parameters

Since the Java type equivalence is based on type names / identity rather than signature-based (or duck typing) equivalence, those two collection hierarchies would be incompatible. That would mean that APIs and implementations that used collections would need to choose between pre-1.5 and post-1.5 collections. Mixing them would be awkward ... and inefficient.

The end result would have been a big problem for people / organizations who needed to use legacy Java libraries and applications. Basically, migrating to Java 1.5 would have meant a lot of rewriting of applications that worked just fine. That would have killed generics, and Java as an enterprise language.


We cannot give you specific examples that provably wouldn't work in a Java language where generics were template based and / or there wasn't erasure.

  • That hypothetical Java language does not exist.
  • Given enough developer effort, any pre-1.5 Java program could have been ported to such a language.

What is reification?

Reification is the process of taking an abstract thing and creating a concrete thing.

The term reification in C# generics refers to the process by which a generic type definition and one or more generic type arguments (the abstract thing) are combined to create a new generic type (the concrete thing).

To phrase it differently, it is the process of taking the definition of List<T> and int and producing a concrete List<int> type.

To understand it further, compare the following approaches:

  • In Java generics, a generic type definition is transformed to essentially one concrete generic type shared across all allowed type argument combinations. Thus, multiple (source code level) types are mapped to one (binary level) type - but as a result, information about the type arguments of an instance is discarded in that instance (type erasure).

    1. As a side effect of this implementation technique, the only generic type arguments that are natively allowed are those types that can share the binary code of their concrete type; which means those types whose storage locations have interchangeable representations; which means reference types. Using value types as generic type arguments requires boxing them (placing them in a simple reference type wrapper).
    2. No code is duplicated in order to implement generics this way.
    3. Type information that could have been available at runtime (using reflection) is lost. This, in turn, means that specialization of a generic type (the ability to use specialized source code for any particular generic argument combination) is very restricted.
    4. This mechanism doesn't require support from the runtime environment.
    5. There are a few workarounds to retain type information that a Java program or a JVM-based language can use.
  • In C# generics, the generic type definition is maintained in memory at runtime. Whenever a new concrete type is required, the runtime environment combines the generic type definition and the type arguments and creates the new type (reification). So we get a new type for each combination of the type arguments, at runtime.

    1. This implementation technique allows any kind of type argument combination to be instantiated. Using value types as generic type arguments does not cause boxing, since these types get their own implementation. (Boxing still exists in C#, of course - but it happens in other scenarios, not this one.)
    2. Code duplication could be an issue - but in practice it isn't, because sufficiently smart implementations (this includes Microsoft .NET and Mono) can share code for some instantiations.
    3. Type information is maintained, which allows specialization to an extent, by examining type arguments using reflection. However, the degree of specialization is limited, as a result of the fact that a generic type definition is compiled before any reification happens (this is done by compiling the definition against the constraints on the type parameters - thus, the compiler has to be able "understand" the definition even in the absence of specific type arguments).
    4. This implementation technique depends heavily on runtime support and JIT-compilation (which is why you often hear that C# generics have some limitations on platforms like iOS, where dynamic code generation is restricted).
    5. In the context of C# generics, reification is done for you by the runtime environment. However, if you want to more intuitively understand the difference between a generic type definition and a concrete generic type, you can always perform a reification on your own, using the System.Type class (even if the particular generic type argument combination you're instantiating didn't appear in your source code directly).
  • In C++ templates, the template definition is maintained in memory at compile time. Whenever a new instantiation of a template type is required in the source code, the compiler combines the template definition and the template arguments and creates the new type. So we get a unique type for each combination of the template arguments, at compile time.

    1. This implementation technique allows any kind of type argument combination to be instantiated.
    2. This is known to duplicate binary code but a sufficiently smart tool-chain could still detect this and share code for some instantiations.
    3. The template definition itself is not "compiled" - only its concrete instantiations are actually compiled. This places fewer constraints on the compiler and allows a greater degree of template specialization.
    4. Since template instantiations are performed at compile time, no runtime support is needed here either.
    5. This process is lately referred to as monomorphization, especially in the Rust community. The word is used in contrast to parametric polymorphism, which is the name of the concept that generics come from.

Dart 2 runtimeType and reified generics - Java ClassT equivalent

If you change testChecker to return a is Checker<T>;, would that be enough for you? That would be checking the runtime type of a, not the static type of it at the call-point. If you want the latter, you can do return new Checker<C>() is Checker<T>;.

It is correct that you should never use Type objects for anything like this, so .runtimeType is not useful.

The reason the example didn't work was that it did return C is T;. The is operator takes an object on the left and a type on the right. So, C is evaluated as an object, which means it evaluates to an object of type Type. Then it checks whether Type is a subtype of T, which it isn't.
You can't compare type variables directly, you have to compare objects to types. One option is to do <C>[] is List<T> - that is: Check that a ist of C (object) is-a list of T (type).
Since I already had a generic class available, I just used Checker<C>() is Checker<T> instead.

How to access the .class from a class with a Generic?

You can always do this:

class Foo<T>{
Class<Foo<T>> getMyClass(){
return (Class<Foo<T>>)(Class<?>)Foo.class
}
}

You will have unchecked cast warnings, because it indeed is unsafe -- as others have mentioned, the returned class object is not any more "Foo<T>'s class" as "Foo<SomethingElse>'s class".



Related Topics



Leave a reply



Submit