Java泛型返回类型问题

15
我有以下方法:

我有以下方法:

public <T extends Result> T execute(Command<T> command)
{
     return new LoginResult();
}

这里,Result 是一个接口,而类 LoginResult 实现了该接口。然而,我遇到了以下错误:

不兼容的类型,需要: T,但发现: com.foo.LoginResult

如果我将方法签名更改为:

public Result execute(Command<T> command)

然后同样的返回行就可以正常工作,没有任何错误。

问题出在哪里?我怎样才能从这个方法中返回 LoginResult

编辑:我想使用泛型的原因是,这样我就可以做以下的事情:

Command<LoginResult> login = new Command<>();
 LoginResult result = execute( login );

6
你为什么希望将<T extends Result>作为返回值?如果只放一个Result,它也可以返回子类。你想从哪里获取泛型类型?这个类也是泛型的吗? - NeplatnyUdaj
确实,@NeplatnyUdaj,像这样使用泛型只有在集合内部才真正有意义。 - Richard Tingle
@NeplatnyUdaj 嗯,我的实际代码签名与我在这里展示的不同。我会编辑问题并更新。 - Ali
1
添加一个 Class<T> cls 参数,并使用 Class.cast() 进行向下转型。这将消除编译器错误,并导致 ClassCastException 仅在代码中实际出现转换时发生。 - millimoose
5个回答

16

你无法这样做,因为编译器无法确认LoginResultT类型,因为它是在调用点推断的(即调用者决定将使用哪个类型参数)。


13
@ClickUpvote 因为 <T extends Result> 并不意味着“任何继承自 Result 的类”,而是指“一个特定的继承自 Result 的类,但我们不知道那个类是什么”。 - Anthony Grist
这种类型的问题经常被问到。例如:https://dev59.com/qnVD5IYBdhLWcg3wI3-L - SimonC
@PaŭloEbermann 嗯,由于类型擦除,调用者实际上并没有做出决定 - 没有办法将其传达给被调用方。(除非您使用显式类型标记,这是每个编写通用代码的人都应该知道的做法。) - millimoose
1
@millimoose 你是对的:调用者决定她想要返回什么,而被调用者不知道。这意味着你只能从这样的方法中有效地返回 null - Paŭlo Ebermann
@PaŭloEbermann 这就是为什么原作者的方法签名无法挽救的原因。 - millimoose
显示剩余3条评论

6
回答您编辑后的问题,如果没有显式转换是无法完成的。因此,最简单(但也是最暴力)的解决方案是:
public <T extends Result> T execute(Command<T> command) {
    return (T) new LoginResult();
}

但这样做,您需要为正确的命令实例化正确的结果承担全部责任,因为编译器不再帮助您。

唯一可以帮助您动态实例化事物的是对实际 Class<T> 的引用。

因此,如果您添加一个像 Class<T> getResultType() 这样的方法到您的命令中,您就可以写出:

return command.getResultType().newInstance(); // instead of new SpecificResult()

当然这意味着您在每个 Result 实现中都有一个默认构造函数等等...
更加面向对象的方法(无反射)是让命令实例化自己的结果(使用工厂方法T instantiateResult()):
return command.instantiateResult();

好的,实际上我在我的真实代码中使用了Class<T> actualType方法,我已经编辑了我的问题以展示这一点。但是我仍然无法返回LoginResult - Ali
@ClickUpvote我建议您发布一个SSCCE的真实代码,以获得更具体的分析和解决方案来解决您的问题。 - Luiggi Mendoza
你需要返回一个 actualType 的实例。如果它总是 LoginResult,那么其中没有任何动态的内容,所以你应该避免使用通用的头疼问题。 - Costi Ciudatu
@ClickUpvote 对我们来说很有用,但是看起来它没有满足你的期望。因此,请提供更多信息以获得更详细和准确的答案。 - Luiggi Mendoza
@CostiCiudatu 不,它并不总是LoginResult,它可以是实现了Result接口的任何类,但是它会像我现在实例化LoginResult一样被实例化。 - Ali
显示剩余3条评论

2

进一步解释SimonC的答案。

假设你有以下类:

Result

LoginResult extends Result

OtherResult extends Result

如果您的类型T是OtherResult,那么试图返回LoginResult是无意义的。编译器只有在编译时能够保证合理性时才会编译它。由于T可能与LoginResult不兼容,因此目前不能保证合理性。
返回类型为并不意味着您必须返回一个Result。它意味着您需要返回一个T,但T必须是Result的子类。
关于您的编辑

Edit: The reason I want to use generics, is so I can do something like the following:

Command<LoginResult> login = new Command<>();
 LoginResult result = execute( login );
我不确定 execute 应该做什么,但我的第一个想法是将 execute 设为 Command 的实例方法。这样你就会有:
public T execute()
{

}

问题在于你需要一种实例化LoginResult的方法。为了给出详细的答案,我们需要更多关于你具体问题的信息。
我建议在Result中创建一个静态方法newInstance。由于你知道TResult的某个子类,因此你可以调用T.newInstance()。那么你可以将LoginResult的构造函数设置为私有,并通过它的newInstance方法调用它。
这需要定义命令如下:
public class Command<T extends Result>

结果必须有一个带有以下签名的方法:

public static Result newInstance()

可能会出现的另一个问题是,您不想将Command限制为Results。没关系,您可以创建一个新类:

public class ResultCommand<T extends Result> extends Command

1

比未经检查的转换更安全的解决方案:

public <T extends Result> T execute(Command<T> command, Class<T> cls)
{
    return cls.cast(new LoginResult());
}

LoginResult result = execute(loginCommand, LoginResult.class);

同时拥有这两个参数的原因在于,execute 方法的约定被暗示为:

  • 要么 使用 Command 的某个方法获取类型为 T 的结果并返回它。
  • 要么 以其他方式创建结果,但确保通过向下转换使其成为类型为 T 的结果。

使用 Java 的泛型,只有形式为 <T> T foo(Bar<T>, Baz<T>) 的泛型方法的参数可以安全地 "创建" 类型为 T 的对象。因此,如果在您的情况下,您没有使用 command 参数创建结果,则需要另一个参数来处理类型检查。


尽管在大多数T的选择中,这种方法会抛出ClassCastException异常。 - Paŭlo Ebermann
无论如何,使用现有的设计都无法避免这种情况。想法是从一个带有转换运算符或 .cast() 调用的代码行中抛出 ClassCastException。不同的设计是可能的,可以使用 cls 参数以其他方式 - 也许使用 .newInstance() 创建适当类型的结果。或使用 isInstance() 选择多个可能的结果对象之一。根本思想是,额外的参数使您能够做一些而不是“在某个愚蠢的地方失败”。 - millimoose

-1
编译器需要知道返回值将匹配类型T,但它无法确定。 如果您将Command作为参数传递,则会匹配,但如果您传递Command,可能不会匹配,因为T将被定义为另一种类型。
您可以在运行时进行强制转换,如果返回的对象与T不匹配,则会抛出ClassCastException异常。
public <T extends Result> T execute(Command<T> command, Class<T> type)
{

     return type.cast( new LoginResult() );
}

你可以这样称呼它:
Command<LoginResult> login = new Command<>();
LoginResult result = execute( login, LoginResult.class );

请注意这里存在冗余信息,但编译时需要,因为泛型会丢失。

-1:这不是类型安全的,可能会在一个令人困惑的位置导致ClassCastException。当你确信“转换”有效时,你才应该忽略那些未经检查的转换警告,而产生这些警告是有非常好的理由的。 - millimoose
我没有告诉任何人忽略警告,我说警告是为了让你检查你的代码。那个警告意味着编译器无法为您检查代码的“正确性”,您必须自己完成所有工作。 - aalku
2
无论如何,添加Class.cast()都是一种改进,因为这样异常至少会从实际的代码中可见的强制转换处抛出,而不是由编译器生成;并且尽可能早地抛出异常。即使您忽略了未检查的强制转换,实际的受检强制转换也必须发生在某个地方。最好快速且以清晰的方式失败。 - millimoose
@user270349 不,警告本应该是编译时错误,但由于向后兼容性要求,它并不是。带有未经检查的警告的代码在未经证明之前都是有问题的,而您还没有证明它们是正确的。除非在极少数情况下,您不应该“检查正确性”,而应该消除警告。这不是那些罕见情况之一。(例如,一个 Map<Class, Object>,其中您希望对象是键的类型,其中关系无法在编译时证明,但可以通过包装映射来确保。) - millimoose
@millimoose 我该如何使用 Class.cast?如果我尝试:return T.class.cast(new LoginResult()); 我会得到一个错误:'无法从类型变量中选择'。 - Ali
显示剩余3条评论

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