【问题标题】:Why can't you create an instance of a generic type using "new" operator?为什么不能使用“new”运算符创建泛型类型的实例?
【发布时间】:2015-06-04 14:23:11
【问题描述】:

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

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

澄清一下,我的问题不是如何它可以做到。我知道这在 C# 中是可能的,那么为什么不在 Java 中呢?我很好奇为什么 Java 人没有实现类似的机制?为什么要强制 Java 开发人员使用可能导致运行时错误的笨拙变通方法?这种机制有没有潜在的危害?

【问题讨论】:

  • 泛型类型可以是任何东西。那么,你将如何调用构造函数呢?
  • 在 C# 中,您可以通过强制执行“新”约束(“指定泛型类声明中的任何类型参数必须具有公共无参数构造函数”)。所以从技术上讲,有可能这导致我建议它只是在该语言中不可用。
  • 你当然可以创建一个实例泛型对象
  • @user489041 我的意思是为什么不能使用 new 运算符。
  • 你能举一个你认为你应该能够做什么的例子吗?我根据您的link 给出了答案

标签: java generics type-erasure


【解决方案1】:

简答: Java 是compiled programming language,这意味着您的字节码在运行时是不变的。如果E 未知,则无法为new 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

【讨论】:

  • 这是迄今为止最好的答案,但在提问之前我确实阅读了有关此问题的 Oracle 文档。事实上,这就是我问这个问题的原因......我很好奇的是为什么“它只是没有实施”......
  • 顺便说一句,这种解决方法也是我有时在我的代码中使用的东西。它只是丑陋和烦人......
  • @traveh 我更新了答案,所以现在你可以看到问题的答案了
  • 简短的回答非常具有误导性。 C++ 也是一种编译语言,但没有这个问题。 Rust 是另一种没有这个问题的编译语言。 Java 存在这个问题,因为泛型函数的行为取决于其参数的动态类型。
【解决方案2】:

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

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

泛型类型参数在编译时存在于您的源代码中,但在编译过程中,几乎所有关于它们的证据都会从字节码中删除。之所以选择这种泛型实现,是因为它维护了预泛型代码和 Java 5+ 泛型代码之间的互操作性。因此,泛型的类型安全在很大程度上是仅编译时的现象。如果你的泛型代码编译没有错误和警告,那么你就确信你的代码是类型的安全。

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

  • 可具体化。例如StringInteger 等。可具体化类型在编译时与运行时具有相同的类型信息。

  • 不可具体化。例如List&lt;String&gt;List&lt;T&gt;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 对象),这会限制它们的用处。

【讨论】:

  • 据我了解,如果没有限制,类型参数将替换为字节码中的Object。如果是这样,那么不应允许T t = new T();,因为它将是 Object t = new Object();在字节码中?
  • 嗯,当然……但这就是问题所在,不是吗?在运行时,您要求系统创建一个类型为 T 的新实例,但 JVM 不知道 T 是什么,而 Object 可以是任何实例。因为 Object 不是所有 Object 的 T,所以创建任何 Object 然后将其分配给 T 将是不安全的,并且会违反泛型类型系统的基本保证。
  • 在编译期间,代码T t = new T(); 将变为Object o = new Object();。所以运行时与T 无关。它将看到它必须创建一个 Object 类的实例并分配给 Object 类的引用。您能否解释一下Because an Object isn't a T for all Objects, 的声明。以及如何分配 Object 变得不安全。不安全性不是已经在编译时处理了吗?
  • "在编译期间,代码 T t = new T(); 将变为 Object o = new Object();" ...实际上,不,它没有'吨。 Java 的泛型类型系统的基本保证是,如果您的代码在没有警告或错误的情况下编译,您将永远不会得到 ClassCastException。由于上述原因,引用的分配违反了此保证。这不安全。因此,这个赋值从来没有真正编译成任何东西。
【解决方案3】:

从泛型创建一个类。请注意,这依赖于被参数化的类。这将返回泛型的类对象,您可以通过它进行进一步的反射以创建对象。

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

与普遍看法相反,班级级别的通用信息并未“删除”。

【讨论】:

  • 我持有你提到的关于类型擦除的普遍看法。您能否提供一个链接,可能会消除我的这种想法并为我提供启发?
  • 看我的小例子。猜猜newT 会是什么类型。该类保留了String 的通用参数,因此newT 将是一个字符串。你可以自己运行它。相信我,当我第一次看到它时,我也认为它很神奇。至于官方参考,请参见:docs.oracle.com/javase/7/docs/api/java/lang/reflect/…
  • 我担心它现在出于某种原因在我脑海中飘过。当我有更多的时间时,我会有一个谷歌。顺便说一句,我毫不怀疑你的代码有效,我只需要清楚地理解它,然后我就可以说服自己类型擦除是一个神话。
  • @PhilAnderson 只需要注意一件事,这是在类级别,而不是方法级别。这是我必须做出的一个区别。
  • 谢谢 - 我查看了那个链接,我的思绪有点被震撼了!
【解决方案4】:

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

您不能说“return new T()”,因为编译器无法知道该怎么做。如果没有特定的类型,编译器就无法知道要调用什么构造函数,甚至无法知道它是否存在。此外,为了“新建”T 类型的实例,您需要调用一些东西。 T 只是一个符号。它不是ClassObject。在您提供有关 T 是哪种实例的信息(例如 List&lt;String&gt;)之前,编译器无法执行您正在尝试执行的操作。

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

也许考虑像模板这样的泛型会有所帮助。不是软件开发模式,而是更抽象的概念。模板的概念表明这种结构没有任何内部细节。如果您想按照模板创建这个东西,请使用模板作为您的起点,但是您必须填写详细信息来制作这个东西。泛型类型有点像 - 它允许您构建某些东西,但不提供有关该结构中发生的任何细节。

我知道当引入泛型时,我在这方面遇到了很多困难。起初,我发现先为特定类型编写实现然后使用泛型将其抽象出来是最容易的。在了解泛型之后,我现在发现从泛型接口开始然后实现它更容易。

【讨论】:

  • 泛型作为一个概念不仅仅关乎编译时安全;例如,在 C# 中,您可以 实例化泛型类型。这个限制是由 Java 中泛型的实现方式造成的。
  • 感谢您的努力,但我不同意。编译器在编译时确实知道泛型类型具有哪个构造函数,那么让开发人员标记类或接口必须具有默认构造函数,甚至是带有特定类型参数的构造函数的问题是什么?
  • @traveh 并非所有类型都有默认构造函数。如果我定义了一个非默认构造函数,它就不再有一个。此外,采用特定类型的参数与返回未定义的类型 T 完全不同。我认为这就是问题所在。
猜你喜欢
  • 1970-01-01
  • 2020-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
相关资源
最近更新 更多