【问题标题】:Generics Hell: Can I construct a TypeLiteral<Set<T>> using generics?泛型地狱:我可以使用泛型构造一个 TypeLiteral<Set<T>> 吗?
【发布时间】:2012-01-07 19:49:15
【问题描述】:

我能够使以下通用方法工作的唯一方法是传递看似多余的TypeLiteral&lt;Set&lt;T&gt;&gt; 参数。我相信应该可以在给定另一个参数的情况下以编程方式构造这个参数,但不知道如何。

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

客户端代码如下:

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

其中 A 和 B 是接口。

如果我尝试以下操作(删除 TypeLiteral&lt;Set&lt;T&gt;&gt; superClassSet 参数),我会收到 java.util.Set&lt;T&gt; cannot be used as a key; It is not fully specified. 运行时错误。

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

【问题讨论】:

标签: java generics types set guice


【解决方案1】:

如果您已经知道大部分答案,请原谅我:很难对您的水平做出假设。

问题的原因是类型擦除,正如您已经知道的那样。为了摆脱类型擦除,Guice 使用了具体祖先的技巧,如下所示:

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());
    }
}

重点是,当您创建一个扩展通用超类的类并明确指定类型参数时,您通常需要编写metods which accept that very specific type, and these methods' signatures cannot be erased。在这种情况下,FAQ 中讨论的问题没有问题,但是编译器保存了类型信息,无论如何:您的类的用户需要知道确切的类型才能使用这些方法。

现在您的版本没有从TypeLiteral&lt;Set&lt;YourSpecificType&gt;&gt; 继承的具体类,它只有TypeLiteral&lt;Set&lt;T&gt;&gt;——这就是它失败的地方。

改变我的小例子,那就是:

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());
    }
}

如您所见,我们的GenericTest$1 不再具体:它仍然有一个类型参数,并且它的具体值,这里是String,在编译过程中丢失了。

您当然可以避免这种情况,但为了做到这一点,您需要创建一个类,其中包含用于继承的特定类型参数——这样 Guice 就能够计算出细节.稍等,我试着举个例子。

更新:原来是很长的一段。所以这里有一个更新版本:

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;
    }
}

如您所见,类型信息现在已保留。我相信这种方法没有被使用,因为即使是这个草稿也太痛苦了。

【讨论】:

    【解决方案2】:

    完全指定表示所有类型参数的值都是已知的。使用 Guice 公共 API 从 TypeLiteral&lt;T&gt; 构造完全指定的 TypeLiteral&lt;Set&lt;T&gt;&gt; 似乎是不可能的。具体来说,TypeLiteral 只有两个构造函数。第一个是:

    /**
     * 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();
    }
    

    此构造函数尝试从TypeLiteral 的运行时类中推断类型参数的值。仅当运行时类确定类型​​参数时,这将产生一个完全指定类型。但是,因为泛型类的所有实例共享同一个运行时类(即new HashSet&lt;String&gt;().getClass() == new HashSet&lt;Integer&gt;().getClass(),所以类型参数只有在实例化TypeLiteral非泛型 子类时才知道。即,我们不能为T 的不同值重用相同的类声明,而是必须为每个T 定义一个新类。正如alf 的回答所表明的那样,这相当麻烦。

    这给我们留下了另一个构造函数,它更有帮助,但不是公共 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();
    }
    

    我们可以这样使用这个构造函数:

    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())); 
        }
    }
    

    测试用例:

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

    在一个完美的世界里,Guice 会提供一个公共 API 来完成这个......

    【讨论】:

    • 是的,看起来容易多了 :) 谢谢,我确实希望我的锻炼不是唯一的方法 :)
    • @alf 实现此目的的一种方法是使用提供必要 API 的com.google.inject.util.Types
    猜你喜欢
    • 2011-08-13
    • 2011-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多