鉴于此代码示例:
class A {
}
public class TestGenerics {
private static <T extends A> T failsToCompile() {
return new A();
}
private static <T extends A> T compiles(T param) {
return param;
}
}
为什么第一种方法不能编译,但第二种方法呢?
错误是:
不兼容的类型。要求:T。发现:A
基本上我想要实现的是返回子类型的具体实例(例如,类B,扩展A)。工厂方法的排序。
编辑:好的,对于那些投票的人,我决定进一步详细说明。请参阅更新的示例,该示例不再使用String(是的,我知道String是最终的,不需要说明明显的)
编辑2 :问题的替代版本:
如何实现此方法以便编译(没有未经检查的强制转换和警告)?
abstract <T extends A> T failsToCompile();
编辑3 :这里的代码示例更贴近我试图解决的问题:
class Foo { }
class Bar extends Foo { }
class A<T extends Foo> { }
class B extends A<Bar> { }
public class TestGenerics {
private static <T extends Foo> A<T> failsToCompile() {
return new B();
}
}
此处,方法返回类型为A<T>
。考虑到T
被定义为 Foo或其子类,并且B的定义如下:B extends A<Bar>
,为什么我只能返回{{1} }}?我想这个问题归结为你不能指定new B()
,我理解为什么。但这有一个优雅的解决方案吗?
答案 0 :(得分:4)
忽略String
为final
的事实(因此您不能拥有String
的子类),您可以将通用类Class<T>
传递给通用案例的方法。像,
private static <T extends String> T get(Class<T> cls)
throws InstantiationException, IllegalAccessException {
return cls.newInstance();
}
答案 1 :(得分:2)
我试图更详细地阐述我的评论,以回答&#34;为什么&#34;。
您在问题中显示的是通用方法(而非通用类型),请参阅例如Oracles "The Java™ Tutorials", Generic Methods
让我们考虑对工作方法的明确调用:TestGenerics.<A>compiles(new A());
这会实例化对类T
的方法调用的泛型类型A
。请记住,类型擦除在编译时会删除所有这些时髦的泛型类型,JVM不会看到泛型类型,并且编译器会处理泛型。编译器现在知道,您想要使用泛型类型T
&#34; set&#34;上课A
。从中可以推断出返回的类型也是类A
(因为T
根据方法代码返回,并且T
被实例化为A
)。这就是你使用泛型所获得的一切。
您可以从方法调用中移除<A>
,使其看起来像普通&#34;普通&#34;方法调用,因为编译器可以推断出T
必须是A
:TestGenerics.compiles(new A());
让TestGenerics.<B>compiles(new B());
与class B extends A {}
一起查看。如上所述,编译器知道此方法调用将返回类B
的对象。
现在想象一下(不编译)代码TestGenerics.<B>compiles(new A());
。由于传递给方法的对象不是B
类型,编译器将抛出错误。否则该方法将返回类A
的对象 - 但泛型类型断言该方法在此返回类型为B
的对象。
这实际上相当于Andreas在评论中给出的例子(B b = failsToCompile()
)。
直到你实例化泛型类型 - 即使你设置了边界(如T extends A
) - 它也没有类类型。因此,您无法返回具体类,因为这不符合泛型类型参数。
为了好玩,让我们试着为上述推理制作一个真实世界的例子:比萨饼。为了举例,我们假设每个披萨都是Margherita的一个子类型,即您将成分添加到Margherita以获得您最喜欢的其他披萨。
class PizzaMargherita {};
class PizzaProsciutto {
PizzaProsciutto() {
super();
addIngredient(new Prosciutto());
}
}
您的compiles()
方法现在烘焙披萨:
public static <P extends Margherita> P bake(P pizza) {
heat(pizza);
return pizza;
}
烘烤玛格丽特(Margherita)并从烤箱中取出(烘烤过的)玛格丽特(Margherita),烘烤意大利熏火腿并获得意大利熏火腿。
现在想一想:
public static <P extends Margherita> P doesntBake(P pizza) {
return new PizzaMargherita();
}
一个烤箱总是返回玛格丽特,独立于你投入的东西?!?编译器不会批准。
- &GT;您需要使用混凝土比萨来烘焙它,或者您需要一种解决方法,例如类型标记:
public static <P extends Margherita> P bakePizza(Ingredients<P> ingredient) {
P pizza = buildPizza(ingredients);
return bake(pizza);
}
您必须使用运行时类型令牌,如@Elliott Frisch在其答案中所示:
private static <T extends A> T failedToCompile(Class<T> c)
throws InstantiationException, IllegalAccessException {
return c.newInstance();
}
您无法实例化通用类型 - new T()
无法正常工作,因为JVM对通用类型T
一无所知。因此,您在编辑2中寻找的内容并不起作用。但是TestGenerics.<A>failedToCompile(A.class);
有效,因为A.class
是java字节代码的一部分。
根据您的具体要求,通用工厂类可能会对您有所帮助:
class Factory<T extends A> {
private final Class<T> typeToken;
public Factory(Class<T> typeToken) {
this.typeToken = typeToken;
}
public T build()
throws InstantiationException, IllegalAccessException {
return this.typeToken.newInstance();
}
}
你仍然需要某种形式的Map来获得正确的工厂来构建你需要的类,但你现在可以使用你需要创建类型为T的对象的任何可用的东西。
Map<String, Factory<?>> factories = new HashMap<>();
Map<DAO, Factory<?>> factories = new HashMap<>();
Map<ValueObject, Factory<?>> factories = new HashMap<>();
factories.get(valueObjectTypeA);
答案 2 :(得分:1)
其他人已经清楚地解释了failsToCompile
方法无法编译的原因。安德烈亚斯甚至展示了一个例子......
至于如何规避这一点,我认为你应该考虑使用Supplier
,这只是你需要的工厂:
private static <T extends A> T nowItCompilesFine(Supplier<? extends T> factory) {
return factory.get();
}
你称之为:
A a = nowItCompilesFine(() -> new A());
或者使用对构造函数的引用:
A a = nowItCompilesFine(A::new);
与A
的任何后代相同:
class Child extends A { }
List<Supplier<? extends A>> factories = Arrays.asList(A::new, Child::new);
Supplier<? extends A> factoryA = factories.get(0);
A a1 = nowItCompilesFine(factoryA);
Supplier<? extends A> factoryChild = factories.get(1);
A a2 = nowItCompilesFine(factoryChild);
System.out.println(a2.getClass().getSimpleName()); // Child