为什么不能使用 "new" 操作符创建泛型类型的实例?

13

我找到了许多关于如何克服这个限制的帖子,但没有一个解释为什么会有这个限制(除了这篇,它只是提到这与类型擦除有关)。

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

澄清一下,我的问题不是如何实现它。我知道在C#中是可能的,那么为什么Java不能呢?我想知道为什么Java开发人员没有实现类似的机制?为什么强制Java开发人员使用笨拙的解决方法,有可能导致运行时错误?这样的机制是否存在任何潜在危险?


2
泛型类型可以是任何东西。那么,你该如何调用构造函数呢? - Bubletan
1
在C#中,您可以通过强制使用“new”约束(“指定泛型类声明中的任何类型参数必须具有公共无参数构造函数”)来实现。因此,在技术上是可能的,这让我认为它只是在语言中不可用。 - Stephen Kennedy
1
你肯定可以创建一个泛型对象的实例。 - user489041
1
@user489041 我的意思是为什么不能使用new运算符来实现它。 - traveh
你能举个例子说明你认为自己应该能做什么吗?我根据你的链接给出了我的答案。 - MadConan
当然。定义E具有默认构造函数,然后执行E e = new E() - traveh
4个回答

17

简短回答: Java是一种编译型编程语言,这意味着在运行时您的字节码是固定的。如果E未知,则不可能为new E()生成字节码。

解释: 通用信息在运行时被擦除:

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

在字节码类BoxWithPresent中包含类型为Present的字段item,但是类SimpleBox中包含类型为Object的字段item(因为未指定类型E)。
现在,您要编写抽象实例化方法:
public class Container<E> {
    public <E> E createE() {
        return new E(); // imagine if that was allowed
    }
}

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

那么...可以用 new Object() 替换 new T() 吗?这是个坏主意,因为 BoxWithPresent 类期望 EPresent

它可以被替换成 class.newInstance() 吗?同样不行,在方法范围内没有 class 变量。

这就是为什么 new E() 是不可能的。
但是有一些解决方法,比如将class作为参数传递,或者提取泛型信息


这是目前为止最好的答案,但在提问之前我确实阅读了有关此事的Oracle文档。事实上,这就是我提出问题的原因...我想知道的是为什么“它只是没有被实现”... - traveh
顺便说一句,那个解决方法有时候我也会在我的代码中使用。只是很丑陋和烦人... - traveh
@traveh,我更新了答案,现在你可以在问题上看到答案了。 - AdamSkywalker
简短的回答很误导人。C++也是一种编译语言,但没有这个问题。Rust是另一种编译语言,也不会出现这个问题。Java有这个问题,因为泛型函数的行为取决于其参数的动态类型。 - Indiana Kernick

13
最简短的答案是泛型类型参数在运行时不存在。
Java 5中将泛型添加到Java语言中。为了保持与现有代码库的向后兼容性,它们通过擦除进行实现。
泛型类型参数存在于编译时的源代码中,但几乎所有关于它们的证据都在编译过程中的字节码中被删除。选择这种泛型实现是因为它在预泛型代码和Java 5+泛型代码之间保持互操作性。因此,泛型的类型安全性主要是一个仅在编译时存在的现象。如果您的泛型代码在没有错误和警告的情况下编译,则可以确保您的代码是类型安全的。
由于擦除,然而,在Java 5中有两种类型:
- 可重用类型。例如 `String`、`Integer` 等。可重用类型在编译时和运行时具有相同的类型信息。 - 不可重用类型。例如 `List`、`List` 和 `T`。不可重用类型在运行时具有比编译时更少的类型信息。事实上,上述类型的运行时类型分别为 `List`、`List` 和 `Object`。在编译过程中,泛型类型信息被擦除。
您不能使用 `new` 运算符来创建不可重用类型的对象,因为在运行时,JVM 无法以类型安全的方式生成正确类型的对象。
源代码:
T myObject = new T();

上述代码无法编译。在运行时,T已被擦除。为了规避Java泛型中类型擦除的一些问题,可以使用“类型标记”策略。以下是实现该策略的通用方法,用于创建新的T对象:
public <T> T newInstance(Class<T> cls) {
    T myObject = cls.newInstance();
    return myObject;
}

通用方法会从传递的Class对象中捕获类型信息,这个参数叫做类型标记。不幸的是,类型标记本身必须始终是可具体化的(因为你不能为不可具体化的类型获取Class对象),这可能会限制它们的实用性。

1
据我理解,如果没有限制,类型参数将在字节码中被替换为“Object”。如果是这样,那么“T t = new T();”不应该被允许,因为它将在字节码中变成“Object t = new Object();”? - Yug Singh
1
当然...但这就是问题所在,不是吗?在运行时,您要求系统创建类型T的新实例,但JVM不知道T是什么,而Object可以是任何实例。因为并非所有对象都是T的对象,创建任何对象然后将其分配给T将是不安全的,并且违反了泛型类型系统所做的基本保证。 - scottb
1
在编译时,代码T t = new T();将变为Object o = new Object();。因此,运行时不关心T。它只会看到它需要创建一个Object类的实例并将其分配给Object类的引用。另外,您能否解释一下语句“因为并非所有对象都是T的对象”?赋值Object为什么会变得不安全?难道不会在编译时处理不安全性吗? - Yug Singh
1
"在编译时,代码T t = new T();将变成Object o = new Object();" ... 实际上并不是这样的。Java泛型类型系统的基本保证是,如果您的代码在没有警告或错误的情况下编译通过,您永远不会遇到ClassCastException。由于上述原因,引用的赋值违反了这个保证。它是不安全的。因此,这个赋值根本就没有被编译成任何东西。 - scottb
@scottb 我知道这个问题有点晚了。 "因为对于所有对象来说,一个对象并不是T类型,创建任何对象然后将其分配给T是不安全的"......这在后端中每种情况下都会发生对吧? public class Test <T> { T var; public Test(T var) { this.var=var; } System.out.print(var); } 根据我的理解,在字节码中,T将被替换为Object,然后被转换为我们传递的任何参数(Integer,String等)。 为什么在这里 T t = new T(); 不能工作呢? - hamal
@sehrpennt:你上面写的代码是类型安全的。但并不是所有可以在T t = new T();例程中编写的代码都是类型安全的。例如,可能会创建一个本应该是String实例并将其分配给Integer引用。此外,可能会将这些不安全创建的对象传递到远离实例化的代码中,并建立堆污染或在难以调试的位置引发异常。 - scottb

1
从泛型创建一个类。请注意,这取决于类是否被参数化。这将返回泛型的类对象,通过它,您可以执行进一步的反射以创建一个对象。
 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: https://dev59.com/UH7aa4cB1Zd3GeqPt7lm"
                    + " 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将是一个字符串。你可以自己运行它。相信我,当我第一次看到它时,我也觉得这是魔法。至于官方参考,请参见:https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/ParameterizedType.html#getActualTypeArguments%28%29 - user489041
我现在有些不理解,可能是因为这个问题超出了我的能力范围。等我有时间的时候,我会去搜索一下。顺便说一句,我毫不怀疑你的代码是有效的,只是在我能够清楚地理解它之前,我需要确信类型擦除是一个神话。 - Phil Anderson
@PhilAnderson 只需要注意一件事,这是在类级别而不是方法级别。这是我必须要做出的一个区分。 - user489041
谢谢 - 我看了那个链接,我的思维稍微有些震惊! - Phil Anderson
我不明白这怎么回答我的问题。我问的是为什么不可能,而不是如何做到... - traveh
显示剩余4条评论

0

记得泛型类型是关于编译时的安全性。类型的编译时检查允许编译器向您发出有关代码问题的警告/错误。虽然这并没有直接帮助您的问题,但保持编译时和运行时的概念非常清晰很重要。

您不能说“return new T()”,因为编译器无法知道如何执行此操作。没有具体的类型,编译器将不知道调用哪个构造函数,甚至不知道是否存在。另外,为了“new up”类型T的实例,您需要调用某些内容。 T只是一个符号。它不是一个ClassObject。在您提供有关T实例的类型信息之前(例如List<String>),编译器无法完成您尝试执行的操作。

打字只是一种确保编译时给定和返回的类型匹配的方式。当您指定类型细节时,才能创建其实例。这就是为什么您通常在接口上看到完全开放的类型(例如List<T>Function<T,R>等)。因此,接口的实现者可以指定将使用哪些类型。

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

我知道当引入泛型时,我曾经很困惑。起初,我发现先针对特定类型编写实现,然后再使用泛型进行抽象化最容易。在我理解泛型之后,我现在发现从泛型接口开始实现更容易。


泛型作为一个概念不仅仅是关于编译时的安全性;例如在C#中,你可以实例化泛型类型。这个限制是由于Java中泛型的实现方式所导致的。 - Mark Rotteveel
感谢你的努力,但我不同意。编译器在编译时确实知道泛型类型有哪些构造函数,那么允许开发人员标记一个类或接口必须具有默认构造函数,甚至是带有特定类型参数的构造函数有什么问题呢? - traveh
@traveh 并非所有类型都有默认构造函数。如果我定义了一个非默认构造函数,它就不再有默认构造函数了。而且,接受特定类型的参数与返回未定义类型 T 是完全不同的。我以为这就是问题所在。 - MadConan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接