Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics Hell: Can I construct a TypeLiteral<Set<T>> using generics?

The only way I was able to get the below generic method to work was to pass the seemingly redundant TypeLiteral<Set<T>> parameter. I believe it should be possible to construct this parameter programmatically given the other parameter, but can't figure out how.

protected <T> Key<Set<T>> bindMultibinder(
 TypeLiteral<Set<T>> superClassSet, TypeLiteral<T> superClass) {
   final Key<Set<T>> multibinderKey = Key.get(superClassSet, randomAnnotation);
   return multibinderKey;
}

Client code looks like:

bindMultibinder(new TypeLiteral<Set<A<B>>>(){}, new TypeLiteral<A<B>>(){});

Where A and B are interfaces.

If I try the following (removing the TypeLiteral<Set<T>> superClassSet parameter), I get a java.util.Set<T> cannot be used as a key; It is not fully specified. runtime error.

protected <T> Key<Set<T>> bindMultibinder(TypeLiteral<T> superClass) {
   final Key<Set<T>> multibinderKey = Key.get(
    new TypeLiteral<Set<T>>() {}, randomAnnotation);
   return multibinderKey;
}
like image 830
Jeff Axelrod Avatar asked Jan 07 '12 19:01

Jeff Axelrod


People also ask

Can you create instances of generic type parameters?

A generic type is like a template. You cannot create instances of it unless you specify real types for its generic type parameters. To do this at run time, using reflection, requires the MakeGenericType method.

How do you use T generic in Java?

To update the Box class to use generics, you create a generic type declaration by changing the code "public class Box" to "public class Box<T>". This introduces the type variable, T, that can be used anywhere inside the class. As you can see, all occurrences of Object are replaced by T.

How do you provide a generic parameterized type?

In order to use a generic type we must provide one type argument per type parameter that was declared for the generic type. The type argument list is a comma separated list that is delimited by angle brackets and follows the type name. The result is a so-called parameterized type.

How do I restrict a generic type in Java?

Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class.


2 Answers

Please forgive me if you already know most of the answer: it's hard to make an assumption on your level.

The reason of the problem is type erasure, as you know already. In order to get rid of type erasure, Guice uses a trick with concrete ancestors, as shown below:

class Trick<T> {
    T t;
}

public class GenericTest {
    public static void main(String[] args) {
        Trick<Set<String>> trick = new Trick<Set<String>>() {
        };

        // Prints "class org.acm.afilippov.GenericTest$1"
        System.out.println(trick.getClass());
        // Prints "org.acm.afilippov.Trick<java.util.Set<java.lang.String>>"
        System.out.println(trick.getClass().getGenericSuperclass());
    }
}

Point is, when you create a class which extends a generic superclass and explicitly specifies the type parameter, you will usually need to write metods which accept that very specific type, and these methods' signatures cannot be erased. In this case we have no problem discussed in FAQ, but compiler saves the type information, anyway: the users of your class will need to know the exact types in order to use the methods.

Now your version does not have a concrete class inherited from TypeLiteral<Set<YourSpecificType>>, it only has TypeLiteral<Set<T>>—and that's where it all fails.

Changing my little example, that would be:

public class GenericTest {
    public static void main(String[] args) {
        tryMe(String.class);
    }

    private static <T> void tryMe(Class<T> clazz) {
        Trick<Set<T>> trick = new Trick<Set<T>>() {
        };

        // Prints "class org.acm.afilippov.GenericTest$1"
        System.out.println(trick.getClass());
        // Prints "org.acm.afilippov.Trick<java.util.Set<T>>"
        System.out.println(trick.getClass().getGenericSuperclass());
    }
}

As you can see, our GenericTest$1 is not concrete any more: it still have a type parameter, and its concrete value, here String, is lost during the compilation.

You can of course avoid that, but in order to do so, you need to create a class with a specific type parameter used for inheritance—so that Guice would be able to work out the details. Wait a bit, I'll try to come up with an example.

Update: it turned out to be a VERY long bit. So here's an updated version for you:

public class GenericTest {
    public static void main(String[] args) throws Exception {
        tryMe(String.class);
    }

    private static <T> void tryMe(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        Class c = loadClass("org.acm.afilippov.ASMTrick", generateClass(clazz));

        Trick<Set<T>> trick = (Trick<Set<T>>) c.newInstance();

        // Prints "class org.acm.afilippov.ASMTrick"
        System.out.println(trick.getClass());
        // Prints "org.acm.afilippov.Trick<java.util.Set<java.lang.String>>"
        System.out.println(trick.getClass().getGenericSuperclass());
    }

    private static byte[] generateClass(Class<?> element) {
        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_6, ACC_FINAL + ACC_SUPER, "org/acm/afilippov/ASMTrick",
                "Lorg/acm/afilippov/Trick<Ljava/util/Set<L" + element.getName().replaceAll("\\.", "/") + ";>;>;",
                "org/acm/afilippov/Trick", null);

        {
            mv = cw.visitMethod(0, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "org/acm/afilippov/Trick", "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

    private static Class loadClass(String className, byte[] b) {
        //override classDefine (as it is protected) and define the class.
        Class clazz = null;
        try {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Class cls = Class.forName("java.lang.ClassLoader");
            java.lang.reflect.Method method =
                    cls.getDeclaredMethod("defineClass", new Class[]{String.class, byte[].class, int.class, int.class});

            // protected method invocaton
            method.setAccessible(true);
            try {
                Object[] args = new Object[]{className, b, new Integer(0), new Integer(b.length)};
                clazz = (Class) method.invoke(loader, args);
            } finally {
                method.setAccessible(false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
        return clazz;
    }
}

As you can see, the type information is now preserved. I believe this approach is not used because it's way too painful even for this draft.

like image 180
alf Avatar answered Sep 23 '22 18:09

alf


Fully specified means that the values of all type parameters are known. Constructing a fully specified TypeLiteral<Set<T>> from a TypeLiteral<T> appears to be impossible using the Guice public API. Specifically, TypeLiteral only has two constructors. The first is:

/**
 * Constructs a new type literal. Derives represented class from type
 * parameter.
 *
 * <p>Clients create an empty anonymous subclass. Doing so embeds the type
 * parameter in the anonymous class's type hierarchy so we can reconstitute it
 * at runtime despite erasure.
 */
@SuppressWarnings("unchecked")
protected TypeLiteral() {
  this.type = getSuperclassTypeParameter(getClass());
  this.rawType = (Class<? super T>) MoreTypes.getRawType(type);
  this.hashCode = type.hashCode();
}

This constructor attempts to deduce the values of type parameters from the TypeLiteral's runtime class. This will yield a fully specified type only if the runtime class determines the type parameter. However, because all instances of a generic class share the same runtime class (that is, new HashSet<String>().getClass() == new HashSet<Integer>().getClass(), the type parameter is only known if a non-generic subclass of TypeLiteral is instantiated. That is, we can't reuse the same class declaration for different values of T, but must define a new class for each T. This is rather cumbersome, as alf's answer demonstrates.

This leaves us with the other constructor, which is more helpful, but not part of the public API:

/**
 * Unsafe. Constructs a type literal manually.
 */
@SuppressWarnings("unchecked")
TypeLiteral(Type type) {
  this.type = canonicalize(checkNotNull(type, "type"));
  this.rawType = (Class<? super T>) MoreTypes.getRawType(this.type);
  this.hashCode = this.type.hashCode();
}

We can use this constructor as follows:

package com.google.inject;

import java.util.Set;

import com.google.inject.internal.MoreTypes;

public class Types {
    public static <T> TypeLiteral<Set<T>> setOf(TypeLiteral<T> lit) {
        return new TypeLiteral<Set<T>>(new MoreTypes.ParameterizedTypeImpl(null, Set.class, lit.getType())); 
    }
}

Testcase:

public static void main(String[] args) {
    System.out.println(setOf(new TypeLiteral<String>() {}));
}

In a perfect world, Guice would offer a public API to accomplish this ...

like image 37
meriton Avatar answered Sep 22 '22 18:09

meriton