为什么不能使用“new”运算符创建泛型类型的实例?

时间:2015-06-04 14:23:11

标签: java generics type-erasure

我发现很多关于如何克服此限制的帖子,但没有关于为什么存在这种限制的帖子(this one除外,它只是提到它与类型擦除有关)。

那么为什么不能创建泛型类型的实例呢?

为了澄清,我的问题不是如何可以完成。我知道在C#中有可能,为什么不用Java?我很好奇为什么Java人员没有实现类似的机制?为什么迫使Java开发人员使用可能导致运行时错误的尴尬变通方法?这种机制是否存在潜在危害?

4 个答案:

答案 0 :(得分:9)

简答: Java是compiled programming language,这意味着您的字节码在运行时是常量。如果new E()未知,则无法为E生成字节码。

解释:通用信息为erased in runtime

public class Container<E> {
     private E item;
     public E getItem() {return item;}
}
class BoxWithPresent extends Container<Present> {
}
class SimpleBox extends Container {
}

在字节码类中,BoxWithPresent包含item类型的字段Present,但类SimpleBox包含类型为item的字段Object(因为类型)未指定E。)

现在你编写抽象实例化方法:

public class Container<E> {
    public <E> E createE() {
        return new E(); // imagine if that was allowed
    }
}

这里应该生成什么字节码?现在,在编译时生成.class文件,但我们不知道E类型是什么。

那么......可以new T()替换new Object()吗?错误的想法,班级BoxWithPresent会不喜欢它,因为它希望EPresent

可以用class.newInstance()替换吗?同样不,方法范围中没有class变量。

这就是new E()不可能的原因 但是有一些解决方法可以将class作为参数传递,或extracting generic information

答案 1 :(得分:8)

最简单的答案是在运行时不存在泛型类型参数。

在第5版中,泛型被改进为Java语言。为了保持与现有代码库的向后兼容性,它们是通过擦除实现的。

通用类型参数在编译时存在于源代码中,但在编译期间,几乎所有的证据都会在字节代码中被删除。之所以选择这种泛型,是因为它保持了前仿制代码和Java 5+通用代码之间的互操作性。因此,使用泛型的类型安全很大程度上只是编译时现象。如果您的通用代码编译而没有错误且没有警告,那么您可以放心,您的代码是类型安全

然而,由于擦除,(从Java 5开始)有两种类型:

  • <强> Reifiable 即可。例如StringInteger等。可编译类型在编译时具有与运行时相同的类型信息。

  • <强>非reifiable 即可。例如List<String>List<T>T。不可重新生成的类型在编译时的运行时具有较少的类型信息。实际上,上述运行时类型为ListListObject。在编译期间,通用类型信息将被删除。

您不能将new运算符与不可重复类型一起使用,因为在运行时没有类型安全的方法让JVM生成正确类型的对象。

源代码:

T myObject = new T();

以上不编译。在运行时,T已被删除。

规避类型擦除和Java泛型的一些问题的策略是使用类型令牌。此策略在以下创建新T对象的通用方法中实现:

public <T> T newInstance(Class<T> cls) {
    T myObject = cls.newInstance();
    return myObject;
}

泛型方法从作为参数传递的Class对象中捕获类型信息。此参数称为类型标记。不幸的是,类型标记本身必须始终是可以恢复的(因为你不能获得一个非可再生类型的Class对象),这可能会限制它们的用途。

答案 2 :(得分:0)

请记住,泛型类型与编译时的安全性有关。编译时类型检查允许编译器为您提供有关代码问题的警告/错误。这并不能直接帮助您解决问题,但保持编译时和运行时的概念非常清晰非常重要。

你不能说,“return new T()”因为编译器无法知道如何做到这一点。如果没有特定类型,编译器就无法知道要调用的构造函数,即使它存在。此外,为了“新建”类型T的实例,您需要调用一些内容。 T只是一个象征。它不是ClassObject。在您提供有关实例T的类型(例如List<String>)的信息之前,编译器无法执行您要执行的操作。

键入只是确保给定和返回的类型在编译时正确匹配的一种方法。当您指定类型细节时,您可以创建它的实例。这就是为什么你通常会在界面上看到完全开放的类型(例如List<T>Function<T,R>等)。因此,接口的实现者可以指定将使用的类型。

考虑像模板这样的泛型可能会有所帮助。不是软件开发模式,而是更抽象的概念。模板的概念说这个结构没有任何内部细节。如果你想在模板后面创建这个东西,可以使用模板作为起点,但你必须填写细节来制作东西。泛型类型有点像 - 它允许你构造一些东西但不提供有关该结构中发生的事情的任何细节。

我知道在引入仿制药时我对此非常挣扎。起初我发现最简单的方法是首先编写特定类型的实现,然后使用泛型将其抽象出来。在我了解了泛型之后,我现在发现更容易从一个通用的界面开始然后实现它。

答案 3 :(得分:0)

从泛型创建一个类。请注意,这取决于参数化的类。这将返回泛型的类对象,通过该类对象可以执行进一步的反射以创建对象。

 public static <T> Class<T> getClassFromGeneric(
            Object parentObj,
            int oridnalParamterizedTypeIndex) throws Exception{
          Type[] typeArray = getParameterizedTypeListAsArray(parentObj);
          return (Class<T>)typeArray[oridnalParamterizedTypeIndex];
    }

    public static <T> Type[] getParameterizedTypeListAsArray(Object parentObj){
        try{
            return  ((ParameterizedType) parentObj.getClass()
                    .getGenericSuperclass())
                    .getActualTypeArguments();
        }
        catch(ClassCastException e){
            logger.log(Level.SEVERE, "Most likely, somewhere in your inhetirance chain,"
                    + "there is a class that uses a raw type and not the generic param."
                    + "See: http://stackoverflow.com/questions/23074446/java-lang-classcastexception-java-lang-class-cannot-be-cast-to-java-lang-reflec"
                    + " for more info",e);
            throw e;
        }
    }

用法:

public class GenericBaseClass<T>{}
public class GenericImpl extends GenericBaseClass<String>{
    public static void main(String[] args){
        new GenericImpl();
    }

    public GenericImpl(){
        Class tClazz = getClassFromGeneric(this,0);
        Constructor constructor = tClazz.getConstructor();
        T newT = constructor.newInstance();
    }
}

与流行的看法相反,班级的一般信息不会被“删除”。