泛型的疑惑
有如下的一些代码。
// 调试负载,携带着不同的调试信息(content)
public class DebuggingPayload<T> {
private T content;
}
// 调试内容相关类
public class DebugContent {}
public class StartDebugContent extends DebugContent {}
public class BreakpointsChangedContent extends DebugContent {}
// 省略其他......
// 调试负载处理器相关类
public interface DebuggingPayloadHandler<T extends DebugContent> {
void handle(ApiDebugger apiDebugger, DebuggingPayload<? extends T> payload);
}
public class StartDebuggingHandler implements DebuggingPayloadHandler<StartDebugContent> {
void handle(ApiDebugger apiDebugger, DebuggingPayload<? extends StartDebugContent> payload){
// The logic of this handler.
}
}
public class BreakpointsChangedPayloadHandler implements DebuggingPayloadHandler<BreakpointsChangedContent> {
void handle(ApiDebugger apiDebugger, DebuggingPayload<? extends BreakpointsChangedContent> payload){
// The logic of this handler.
}
}
// 省略其他......
// 调试负载处理器工厂
public class DebuggingPayloadHandlerFactory {
@SuppressWarnings("unchecked")
public static <C extends DebugContent, H extends DebuggingPayloadHandler<C>> H createPayloadHandler(DebugCommand debugCommand) {
switch (debugCommand) {
case TERMINATE:
return (H) getBean(TerminateHandler.class);
case START_DEBUGGING:
return (H) getBean(StartDebuggingHandler.class);
case CONTINUING_DEBUGGING:
return (H) (DebuggingPayloadHandler<C>) (apiDebugger, payload) -> {
};
case BREAKPOINTS_CHANGED:
return (H) getBean(BreakpointsChangedPayloadHandler.class);
case SESSION_CLOSED:
return (H) getBean(SessionClosedPayloadHandler.class);
case EXCEPTION:
return (H) getBean(ExceptionPayloadHandler.class);
default:
throw new IllegalArgumentException("Unknown debug command: " + debugCommand);
}
}
}
这个代码有好几个关于泛型的疑惑。
- 为什么返回DebuggingPayloadHandler<? extends DebugContent>类型,Sonarlint会提示一个警告?
- 为什么不能返回DebuggingPayloadHandler类型?
- 为什么一定要强转成H类型?
针对以上的3个问题,分别做如下的解释。
Sonarlint的警告
假设调试负载处理器工厂写成如下代码。
// 调试负载处理器工厂
public class DebuggingPayloadHandlerFactory {
@SuppressWarnings("unchecked")
public static DebuggingPayloadHandler<? extends DebugContent> createPayloadHandler(DebugCommand debugCommand) {
//......
}
Sonarlint将有如下的警告。
Generic wildcard types should not be used in return types.
Sonarlint给出的解决建议。
A return type containing wildcards cannot be narrowed down in any context. This indicates that the developer’s intention was likely something else. The core problem lies in type variance. Expressions at an input position, such as arguments passed to a method, can have a more specific type than the type expected by the method, which is called covariance. Expressions at an output position, such as a variable that receives the return result from a method, can have a more general type than the method’s return type, which is called contravariance. This can be traced back to the Liskov substitution principle. In Java, type parameters of a generic type are invariant by default due to their potential occurrence in both input and output positions at the same time. A classic example of this is the methods T get() (output position) and add(T element) (input position) in interface java.util.List. We could construct cases with invalid typing in List if T were not invariant. Wildcards can be employed to achieve covariance or contravariance in situations where the type parameter appears in one position only: <? extends Foo> for covariance (input positions) <? super Foo> for contravariance (output positions) However, covariance is ineffective for the return type of a method since it is not an input position. Making it contravariant also has no effect since it is the receiver of the return value which must be contravariant (use-site variance in Java). Consequently, a return type containing wildcards is generally a mistake.
Sonarlint为什么会有这样的警告呢? 站在编译器的角度,面对泛型的时候,它的职责就是要保证类型的安全性。如果方法返回类型是DebuggingPayloadHandler<? extends DebugContent>,从语义上来看,DebuggingPayloadHandler的泛型参数可以是任何继承自DebugContent的子类,那么在接收方法返回值处(程序输出处)就只能用DebuggingPayload<? extends DebugContent>这种类型去接收编译器才能够认为是合法的。因为,假设能够用任意一个DebugContent的子类型去接收方法的返回值,那么将有如下的代码片段。
// 将产生编译报错
// StartDebuggingHandler自己定义了它处理的调试信息类型是StartDebugContent,因此这里在定义handler变量的时候并
// 没有使用泛型
StartDebuggingHandler handler = createPayloadHandler(START_DEBUGGING);
可是createPayloadHandler方法完全是有可能返回一个BreakpointsChangedPayloadHandler(它自己定义了他会处理BreakpointsChangedContent类型的调试信息)的,这将与定义的DebuggingPayloadHandler不同,所以编译器会进行报错,它无法保证这样的代码是类型安全的。既然如此的话,在使用createPayloadHandler的时候,在程序输出处就只能有以下代码。
DebuggingPayloadHandler<? extends DebugContent> handler = createpayloadHandler(any_command)
但是,拿到这样一个handler有什么用呢?什么用都没有。因为传递给这个handler任何的DebugPayload类型都是错的,因为编译器在编译时期完全不知道这个handler到底能处理什么类型的DebugPayload,因此它只能报错。 现在可以看看Sonarlint为什么有这样的警告了。它的意思是“泛型通配符类型不应该用于返回类型”,站在编译器的角度经过一番语义上的分析,就可以理解他为什么发出这样警告了。
为什么不能返回DebuggingPayloadHandler类型?
这个与Java中泛型的不变性有关。与数组不同,数组的Number[]与Integer[]是存在继承关系的,下边这段代码在编译器的眼里是合法的。
Number[] numbers = new Number[]{};
Integer[] integers = new Integer[]{};
numbers = integers;
然而,下边这段代码是不合法的。
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
numbers = integers;// 会产生编译报错,这也是泛型的不变性的体现
至于为什么泛型具有不变性,可以做一下的解释,依然是站在编译器的角度去分析。假设下边边写的这段代码是合法的。
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
numbers = integers;// 会产生编译报错,但是我们暂且认为它是正确的
那么接下来这样写,会发生什么呢?
numbers.add(7.25);
编译器将不会报出任何的错误,因为在它的眼里numbers是一个Number类型对象的列表,而7.25是满足这个条件的,所以如果泛型不具备不变性,那编译器是无法保证这种情况下的类型检查不出错的。 因此如果返回DebuggingPayloadHandler类型,这个方法将无法实现,由于泛型的不变性,返回什么编译器都会报错。
为什么一定要强转成H类型?
这个就比较好理解了。H是在方法定义的时候指定的一个泛型参数,也就是说它是一个形参,在使用这个方法的地方会将实参传入。因此,H这个类型世纪是什么类型是由是由这个方法的使用者决定的,在这个方法的内部是无法知道H的具体类型的。如果不强转,编译器会认为你返回什么类型都是不合法的,因为H的类型是不确定的,编译器无法确认你返回的类型是正确的。
理论上的总结
泛型有一些不变性、协变性、逆变性相关的概念。这些概念也很重要,协变性和逆变性可以带来更多的灵活性,避免了泛型的不变性造成的多态的缺失,而不变性带来了更多的安全性。
在写代码的时候有记住一些约束性的规则会提升写代码的效率,而不必每次都要使用语义分析这种费力的方式。 使用泛型通配符的规则可以记住一下:
- 在程序输入处使用协变性和逆变性,而不是输出处(createPayloadHandler如果返回了DebuggingPayloadhandler<? extends DebugContent>就违反了这个规则)。
- 协变变量只能作为生产者,而不能作为消费者(createPayloadHandler如果返回了DebuggingPayloadhandler<? extends DebugContent>,返回变量将被作为消费者,同样违反了这个规则)。
- 逆变变量只能作为消费者,而不能作为生产者。