大家好,我是奕叔😈,一个工作八年快退休的程序员,微信:lyfinal,交个朋友,一起交流Java后端技术,相互成长,成为更优秀的🐒~
先看一段例子,定义一个简单的泛型类 Some<T>
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class Some<T> {
private final T value;
private Some(final T t) {
this.value = t;
}
static <T> Some<T> of(final Supplier<T> supplier) {
return new Some<>(supplier.get());
}
public Some<T> peek(final Consumer<T> consumer) {
consumer.accept(value);
return this;
}
public T get() {
return value;
}
}
这个类很简单,他的值由一个 supplier 函数通过静态工厂方法提供,还有额外两个方法 peek 、 get .
注意 peek 返回的是 this ,这样方便链式调用。
public static void main(String[] args) {
Some<List<? extends CharSequence>> some =
Some.of(() -> Arrays.asList("a", "b", "c"));
System.out.println(some.get());
}
上面这段代码一如预期的正常编译,并且打印结果 [a, b, c] . 下面我们做点小改动,链式调用 peek .
将 System.out.println 作为 consumer 传入 peek 。
public static void main(String[] args) {
//❌编译错误
Some<List<? extends CharSequence>> some =
Some.of(() -> Arrays.asList("a", "b", "c")).peek(System.out::println);
}
上面的代码将会产生出乎预料的结果,无法正常编译。
编译器报类型不兼容的原因,是因为Java泛型是 invariant (可以参见covariant VS invariant)。
子类型关系不会扩展到参数化类型,也即是如果 T 是 S 的子类型,不能推断出 C<T> 是 C<S> 的子类型。
那为什么上面第一个列子调用方式没有 peek 可以, peek 返回 this ,和 of 返回结果一样,第二个例子调用却不可以。
这里涉及到Java8引入的泛型目标类型推断(JEP 101: Generalized Target-Type Inference),泛型推断让编译器能够利用上下文信息来推断出合理的类型。下面逐步分析。
笔者所用的 IDE 是 IntelliJ IDEA , 这里有个小技巧, Mac 下按住 Command ( Window 下应该是 Ctrl , 待验证) 键同时将光标移动到泛型方法上,既可以看出Java编译器根据上下文推断出的类型,如果想让浮动弹窗长期保持,按住 Command 的同时将光标移动到浮窗,点击下,这时即可松开 Command , 移走光标浮窗也会保持,若未点击浮窗,则松开 Command 键,浮窗就会消失。
从截图中可以看出编译器推断 类型变量 T 是 List<? extends CharSequence> , of 返回类型是 Some<List<? extends CharSequence>> ,和表达式左边的类型是一致的。
可以看出编译器推断出类型变量 T 是 List<String> , peek 返回结果是 Some<List<String>> 类型。和表达式左边的类型是不一样的。
这也就清楚为什么第二个例子会报类型不兼容了。因为 List<? extends CharSequence> 与 List<String> 是不兼容的,原因就是上面提到的 Java泛型是 invariant 。
再看看下面的例子,注意 of 是一个静态工厂方法。
//✅正常编译
Some<List<? extends CharSequence>> some =
Some.of(() -> Arrays.asList("a", "b", "c"))
.of(() -> Arrays.asList("a", "b", "c"));
第一个 of 推断出 T 是 List<String> ,返回类型是 Some<List<String>> ,第二个 of T 是 List<? extends CharSequence> ,返回类型是 Some<List<? extends CharSequence>> 。
为什么编译器会推断出 T 同时是两个类型?
注意方法声明 of 是一个静态泛型方法,实则前后两个 of 方法的类型变量 T 根本不是同一个,所以可以类型不同,最后赋值给表达式左边的是第二个 of 的返回值,而他们类型是一样的,所以可以正常编译。
实际第一个 of 中 T 无论是 List<String> 还是 List<Integer> , 表达式都合法,因为最终返回类型由链路中最后一个方法决定,只要它的类型和表达式左边被赋值对象类型兼容就合法。
由于 peek 是实例方法,所以他的变量类型 T 其实早已由前面 of 构造出的对象决定了。
那为什么 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个表达式中会推断出不同类型?
这是因为 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个语句中所处的上下文不一样,我们知道泛型目标类型推断,编译器需要结合上下文来推断更合理的具体类型或者是它的子类。
Some.of(() -> Arrays.asList("a", "b", "c")) 这个语句中,编译器可以推断出类型变量 T 可以为 List<String> 、 List<? extends CharSequence> 、 List<? extends Object> , 但有多个候选类型时,编译器会选择更为具体、继承链中更接近的类型。下面 JLS 关于类型推断的一句描述可以佐证。
第一个截图中, of 所处上下文,编译器既要让 of 的结果类型满足表达式左边的类型(针对类型变量 T ) List<? extends CharSequence>又 要满足入参的类型(List<String> 、 List<? extends CharSequence> 、 List<? extends Object> 任一皆可),这时会取两者交集,所以最终推断出 T 是 List<? extends CharSequence>。
第二个截图中, of 所处上下文并不是表达式链路的最后,他的返回结果不需要赋值给表达式左边的变量,所以编译器类型推断时只需要考虑兼容入参类型,这时会选择三种候选类型中最具体的List<String> 。而后面的 peek 方法实则是 of 构造方法返回对象的实例方法,它的类型已经确定是 List<String> ,故不存在类型推断的过程。 peek 的返回类型也就知道是 Some<List<String> 。
有两种方式可以解决图2调用 peek 的问题:
- 现在我们知道图2中的
of是由于没有提供足够的上下文供编译器参考,所以可以通过显示指定具体泛型类型。
public static void main(String[] args) {
Some<List<? extends CharSequence>> some =
Some.<List<? extends CharSequence>>of(() -> Arrays.asList("a", "b", "c"))
.peek(System.out::println); //✅
}
- 图2是由于
peek的链式调用,导致前面的of失去了返回类型的上下文约束信息,所以可以不采用链式调用,给予编译器更多的上下文信息。
public static void main(String[] args) {
//✅
Some<List<? extends CharSequence>> some = Some.of(() -> Arrays.asList("a", "b", "c"));
some.peek(System.out::println);
}
下面看道题,根据上面的分析,可以类推下面的使用方式是错误的,因为 new ArrayList<>() 所处的上下文没有提供任何类型约束,编译器只能推断 ArrayList 的类型变量是 Object , Iterator<Object> 自然无法和 Iterator<String> 兼容。
Iterator<String> it = new ArrayList<>().iterator(); //❌
那有没有办法写出一个类型安全的赋值方式?
public static <T> Iterator<T> iter(Iterable<T> i)
{
return i.iterator();
}
public static void main(String[] args)
{
Iterator<String> it = iter( new ArrayList<>() ); //✅
____________________________/
}
这里 iter 方法所处的上下文,为了满足表达式左边的类型,编译器推断出 T 的类型是 String ,而 new ArrayList<>() 也是泛型,假设他的类型变量是 E ,为了满足 iter 入参的类型, 编译器推断 E 也是 String , 上面的错误实例中之所以 new ArrayList<>() 被推断为 Object , 是因为语句既没有作为返回类型受到赋值类型的约束,也没有作为入参受到方法声明的约束,缺失这些上下文约束提示,编译器只能推断出 Object 。
根据前面的理论知识,你应该不难推测出,下面的写法仅仅是比上面少了结果赋值,那么这种情况由于 iter 缺失返回类型约束,入参本身也是泛型,也不能提供上下文约束条件,所以 iter 的推断结果是 Object , new ArrayList<>() 由于 iter 是 Object ,所以 它的类型变量推断结果也是 Object .
iter( new ArrayList<>() );
下面通过 idea 的自动提取变量来验证我们的猜测。通过选中 1处语句,按下 idea 提取变量快捷键,就会补全成 2这样的语句,这个其实就是通过编译器的泛型目标类型推断来推断变量类型。
iter( new ArrayList<>() ); // 1
Iterator<Object> iter = iter(new ArrayList<>()); // 2
最后再看看泛型推断在泛型实例化中的作用
没有泛型推断前,我们必须按照@1来实例化泛型类,这种使用方式看起来很累赘,前后都要声明参数化类型。有了泛型推断,就可以用如@2简洁的写法,用一对空尖括号 <> 代替。但是如果你用如@3这种写法,将会得到一个编译器警告⚠️,因为 new HashMap() 表示一个 raw type HashMap (不需要泛型推断),无法确定元素具体类型,因此将 HashMap 类型赋值给 Map<String, List<String>> 会产生警告。
Map<String, List<String>> myMap = new HashMap<String, List<String>>(); //@1
Map<String, List<String>> myMap = new HashMap<>(); //@2
Map<String, List<String>> myMap = new HashMap(); //@3 ⚠️ unchecked conversion warning