在Java中创建泛型类型的实例?

651

在Java中,是否可以创建泛型类型的实例?根据我所见,由于类型擦除,答案是不行,但如果有人能发现我所忽略的情况,我很感兴趣:

class SomeContainer<E>
{
    E createContents()
    {
        return what???
    }
}

编辑:事实证明超级类型标记可以用来解决我的问题,但需要大量基于反射的代码,正如下面的一些答案所指出的。

我会将其保持开放一段时间,看是否有人提出了与Ian Robertson的Artima文章截然不同的解决方法。


3
在Android设备上测试了性能,进行了10000次操作:使用new SomeClass()耗时8-9毫秒,使用Factory<SomeClass>.createInstance()耗时9-11毫秒,使用最短的反射方式SomeClass z = SomeClass.class.newInstance()耗时64-71毫秒。而且所有的测试都在单个try-catch块中完成。记得Reflection newInstance()会抛出4个不同的异常吗?所以我决定使用工厂模式。 - Deepscorn
另请参见:https://dev59.com/vnA75IYBdhLWcg3wKluM#5684761 - Dave Jarvis
4
在Java 8中,现在可以传递构造函数引用或lambda表达式,这使得解决这个问题变得相当容易。有关详细信息,请参见下面的我的回答 - Daniel Pryden
我认为编写这样的代码是个坏主意,有更加优雅和易读的方法来解决底层问题。 - Krzysztof Cichocki
2
“只是一会儿”,David Citron说道……从那时起已经过去了十一个年头…… - MC Emperor
@KrzysztofCichocki 通常是正确的,但有些情况不同。例如,当您想要为检查调用add(...)set(0,...)以检查是否为空的通用列表编写断言时。 - Egor Hans
29个回答

376

您说得对。您不能使用new E()。但您可以将其更改为

private static class SomeContainer<E> {
    E createContents(Class<E> clazz) {
        return clazz.newInstance();
    }
}

虽然有些麻烦,但这个方法有效。将其套用工厂模式可以使其更容易管理。


14
我看到了那个解决方案,但它只在你已经有一个想要实例化的类对象的引用时才起作用。 - David Citron
11
知道了。如果你能够执行 E.class 就好了,但由于类型擦除的原因,实际上只会返回 Object.class。 - Justin Rudd
6
这是解决这个问题的正确方法。通常情况下可能不是你所期望的,但却是你得到的结果。 - Joachim Sauer
52
创建createContents()方法该怎么调用? - Alexis Dufrenoy
10
现在已经有一种更好的方法可以做到这一点,不再需要使用Guava和TypeToken传递Class<?>引用了。请参见此答案以获取代码和链接! - user177800
显示剩余9条评论

153

在Java 8中,您可以使用Supplier函数接口来轻松实现此目标:

class SomeContainer<E> {
  private Supplier<E> supplier;

  SomeContainer(Supplier<E> supplier) {
    this.supplier = supplier;
  }

  E createContents() {
    return supplier.get();
  }
}

你可以按照以下方式构造这个类:

SomeContainer<String> stringContainer = new SomeContainer<>(String::new);

该行代码中的语法 String::new 是一个构造函数引用

如果您的构造函数需要参数,可以改用 lambda 表达式:

SomeContainer<BigInteger> bigIntegerContainer
    = new SomeContainer<>(() -> new BigInteger(1));

7
不错。它避免了反思和处理异常情况。 - Bruno Leite
2
太好了。不幸的是,这需要API级别24或更高的Android用户。 - Michael Updike
3
这并不与这个更早的答案有所区别,它展示了其背后的技术模式甚至比Java支持lambda表达式和方法引用还要老,而且一旦升级编译器,你甚至可以在其中使用那些更老的代码。 - Holger
“是否可以只写SomeContainer stringContainer = new SomeContainer(String::new);?” - Aaron Franke
@AaronFranke:不行,因为那样你会使用原始类型。 - Daniel Pryden
你好 @DanielPryden!我在这里创建了一个主题(https://stackoverflow.com/questions/68685234/initialize-a-bounded-generic-type-java),关于使用Supplier初始化有界泛型的问题,我很想听听你的想法。 :) - DTK

145

我不知道这是否有帮助,但当您子类化(包括匿名子类化)一个泛型类型时,类型信息可通过反射获得。例如:

public abstract class Foo<E> {

  public E instance;  

  public Foo() throws Exception {
    instance = ((Class)((ParameterizedType)this.getClass().
       getGenericSuperclass()).getActualTypeArguments()[0]).newInstance();
    ...
  }

}

因此,当您创建Foo的子类时,您将获得Bar的实例,例如:

// notice that this in anonymous subclass of Foo
assert( new Foo<Bar>() {}.instance instanceof Bar );

但这需要大量的工作,而且只适用于子类。但可能会很方便。


2
是的,这很好,特别是如果通用类是抽象的,你可以在具体子类中这样做 :) - Pierre Henry
这种方法也适用于Foo类不是抽象的情况。但为什么它只适用于Foo的匿名子类?假设我们将Foo变成具体类(去掉abstract),为什么new Foo<Bar>();会导致错误,而new Foo<Bar>(){};则不会呢?(异常:“无法将类转换为ParameterizedType”) - Tim Kuipers
3
class Foo<E> 中的 <E> 没有绑定到任何特定的类型。只要 E 没有 静态 绑定,就会看到异常行为,例如:new Foo<Bar>()new Foo<T>() {...}class Fizz <E> extends Foo<E>。第一种情况在编译时被 擦除 了,没有静态绑定。第二种情况会用另一个类型变量 (T) 替换 E,但仍然没有绑定。最后一种情况显然 E 仍未绑定。 - William Price
5
一个静态绑定类型参数的例子是 class Fizz extends Foo<Bar> -- 在这种情况下,使用 Fizz 的用户得到的是一个 Foo<Bar>,除了 Foo<Bar> 之外,它不能是任何其他东西。因此,在这种情况下,编译器很高兴将该信息编码到 Fizz 的类元数据中,并将其作为 ParameterizedType 提供给反射代码。当你创建一个匿名内部类,比如 new Foo<Bar>() {...},它也做着同样的事情,只不过编译器生成一个“匿名”的类名,直到外部类被编译才会知道。 - William Price
4
请注意,如果类型参数也是ParameterizedType(参数化类型),则这样做将不起作用。例如,Foo<Bar<Baz>>。你将创建一个无法明确创建的ParameterizedTypeImpl实例。因此,最好检查getActualTypeArguments()[0]是否返回ParameterizedType。如果是,则要获取原始类型,并创建该类型的实例。 - crush
显示剩余4条评论

90
你需要某种抽象工厂来传递任务:
interface Factory<E> {
    E create();
}

class SomeContainer<E> {
    private final Factory<E> factory;
    SomeContainer(Factory<E> factory) {
        this.factory = factory;
    }
    E createContents() {
        return factory.create();
    }
}

Factory.create() 是什么样子的? - OhadR
7
Factory<>是一个接口,因此没有具体实现。重点是需要通过间接层传递任务到那些懂得如何构造实例的方法中去。最好使用普通代码来完成这个过程,而不是使用元语言的ClassConstructor,因为反射会带来一系列麻烦。 - Tom Hawtin - tackline
2
现在,您可以使用方法引用表达式创建工厂实例,如下所示:SomeContainer<SomeElement> cont = new SomeContainer<>(SomeElement::new); - Lii

27
package org.foo.com;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * Basically the same answer as noah's.
 */
public class Home<E>
{

    @SuppressWarnings ("unchecked")
    public Class<E> getTypeParameterClass()
    {
        Type type = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) type;
        return (Class<E>) paramType.getActualTypeArguments()[0];
    }

    private static class StringHome extends Home<String>
    {
    }

    private static class StringBuilderHome extends Home<StringBuilder>
    {
    }

    private static class StringBufferHome extends Home<StringBuffer>
    {
    }   

    /**
     * This prints "String", "StringBuilder" and "StringBuffer"
     */
    public static void main(String[] args) throws InstantiationException, IllegalAccessException
    {
        Object object0 = new StringHome().getTypeParameterClass().newInstance();
        Object object1 = new StringBuilderHome().getTypeParameterClass().newInstance();
        Object object2 = new StringBufferHome().getTypeParameterClass().newInstance();
        System.out.println(object0.getClass().getSimpleName());
        System.out.println(object1.getClass().getSimpleName());
        System.out.println(object2.getClass().getSimpleName());
    }

}

4
如果您在泛型中使用了这段代码的好方法,可能会导致ClassCastException。然后,您检索actualType参数后,应该检查它是否也是ParameterizedType,如果是,则返回它的RawType(或比此更好的东西)。另一个问题是,当我们多次扩展此代码时,它也会抛出ClassCastException。 - Damian Leszczyński - Vash
3
由于:java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl 无法转换为 java.lang.Class。 - juan
@DamianLeszczyński-Vash 也会失败,例如 class GenericHome<T> extends Home<T>{} - Holger

25

如果你需要在泛型类中创建一个新的类型参数实例,则可以通过在构造函数中要求其类来实现...

public final class Foo<T> {

    private Class<T> typeArgumentClass;

    public Foo(Class<T> typeArgumentClass) {

        this.typeArgumentClass = typeArgumentClass;
    }

    public void doSomethingThatRequiresNewT() throws Exception {

        T myNewT = typeArgumentClass.newInstance();
        ...
    }
}

使用方法:

Foo<Bar> barFoo = new Foo<Bar>(Bar.class);
Foo<Etc> etcFoo = new Foo<Etc>(Etc.class);

优点:

  • 比罗伯逊的超级类型令牌(STT)方法简单得多(也不会有问题)。
  • 比STT方法更高效(STT方法会吃掉你的手机早餐)。

缺点:

  • 不能将类传递给默认构造函数(这就是为什么Foo是final的原因)。如果你真的需要一个默认构造函数,你可以添加一个setter方法,但之后必须记得调用它。
  • 罗伯逊反对...比黑羊还多的酒吧(虽然再次指定类型参数类并不会杀死你)。而且与罗伯逊的说法相反,这并不违反DRY原则,因为编译器会确保类型正确性。
  • 不完全支持Foo<L>。首先...如果类型参数类没有默认构造函数,newInstance()会抛出异常。不过,这个问题在所有已知的解决方案中都存在。
  • 缺少STT方法的完全封装。不过这不是什么大问题(考虑到STT的极高性能开销)。

25

现在您可以这样做,而且不需要大量的反射代码。

import com.google.common.reflect.TypeToken;

public class Q26289147
{
    public static void main(final String[] args) throws IllegalAccessException, InstantiationException
    {
        final StrawManParameterizedClass<String> smpc = new StrawManParameterizedClass<String>() {};
        final String string = (String) smpc.type.getRawType().newInstance();
        System.out.format("string = \"%s\"",string);
    }

    static abstract class StrawManParameterizedClass<T>
    {
        final TypeToken<T> type = new TypeToken<T>(getClass()) {};
    }
}

当然,如果你需要调用构造函数,则需要进行一些反射操作,但这是非常好文档化的,而这个技巧则没有!
这里是TypeToken的JavaDoc文档

6
这个解决方案只适用于有限的情况,就像@noah的反射答案一样。我今天试了所有这些方法...最终,我将参数类的实例传递给带参数类型的类(以便能够调用.newInstance() )。 “泛型”非常缺陷...new Foo<Bar>(Bar.class);...class Foo<T> { private final Class<T> mTFactory; Foo(Class<T> tClass) { mTFactory = tClass; ... } T instance = tFactory.newInstance(); } - yvolk
这适用于所有情况,甚至是接受泛型参数的静态工厂方法。 - user177800
2
公共类 Q26289147?为什么呢? - Hannes Schneidermayer

19

摘自Java教程-泛型限制:

无法创建类型参数的实例

无法创建类型参数的实例。例如,下面的代码会导致编译时错误:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

解决方法是,您可以通过反射创建一个类型参数对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.getDeclaredConstructor().newInstance();   // OK
    list.add(elem);
}
你可以按照以下方式调用append方法:
List<String> ls = new ArrayList<>();
append(ls, String.class);

2
cls.newInstance()已经被弃用,推荐使用cls.getDeclaredConstructor().newInstance() - antikbd
@antikbd 谢谢你的提示!我已相应地更新了这个例子。 - Sergiy Sokolenko

15

考虑一种更加功能化的方法:不要从无中创造出某个E(这显然是代码异味),而是传递一个知道如何创建E的函数,即

E createContents(Callable<E> makeone) {
     return makeone.call(); // most simple case clearly not that useful
}

6
技术上讲,你不是在传递一个函数,而是在传递一个“函数对象”(也称为“函数符”)。 - Lorne Laliberte
3
或许可以使用 Supplier<E> 来避免捕获 Exception - Martin D

9

在编译时,当您使用E时,您实际上并不关心实际的通用类型“E”(无论是使用反射还是使用通用类型的基类),因此让子类提供E的实例。

abstract class SomeContainer<E>
{
    abstract protected E createContents();
    public void doWork(){
        E obj = createContents();
        // Do the work with E 
     }
}

class BlackContainer extends SomeContainer<Black>{
    protected Black createContents() {
        return new Black();
    }
}

1
我喜欢这种方法,因为它易读且不会引入任何类型转换的魔法。缺点是你必须在每个派生类中实现createContents,即使你不需要它。另一种方法是将createContents设置为非抽象方法,但使用空实现(返回null/抛出异常)...在这种情况下,只有在需要时才能实现它。 - michal.jakubeczy

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