Java泛型入门篇: 泛型类、泛型接口以及泛型方法
Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符
Java泛型原理篇: 类型擦除以及桥接方法
前言
在介绍通配符之前,首先我们来思考如下问题:
public class Animal {
}
public class Cat extends Animal {
}
public static void main(String[] args) {
Cat cat = new Cat();
Animal animal = cat; ❶
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; ❷
}
以上代码中①处以及②处代码是否正确?
①处的代码没有任何问题,是多态的典型用法,即父类引用指向子类对象
,而②处的代码与①处类似,但是却提示编译错误。原因是因为Java泛型中规定,即使泛型类型具有继承关系,但是并不意味着该泛型类型的容器也具有继承关系。在上一篇文章中曾介绍过,我们可以把泛型理解成一个标签,一个list中贴上了animal的标签,另一个list贴上了cat的标签,可以说cat是animal的子类,但是不能说贴了cat标签的list就是贴了animal标签list的子类,他们实际上都是list。
知道泛型的这一特性后,就出现了一个比较麻烦的问题: 泛型容器似乎没办法实现多态。
举个例子,在上例中我们为Animal添加一个方法getFood,同时Cat类需要重写该方法
public class Animal {
public String getFood() {
return "食肉动物吃肉,食草动物吃草";
}
}
public class Cat implements Animal {
@Override
public String getFood() {
return "猫粮,小鱼干,猫薄荷";
}
}
如果想在控制台输出不同动物的食物,此时需要定一个print方法,利用多态特性,形参可定义为父类Animal,这样实参就可以传Animal以及其子类对象了。
public void print(Animal animal) {
System.out.println(animal.getFood());
}
public static void main(String[] args) {
Test test = new Test();
test.print(new Animal());
test.print(new Cat());
}
但是如果想打印一批动物的食物信息,就无法利用多态这一特性,因为List<Cat>
并不是List<Animal>
的子类,所以只能额外增加方法。
public void printAnimalFood(List<Animal> animals) {
animals.forEach(a -> System.out.println(a.getFood());
}
public void printCatFood(List<Cat> cats) {
cats.forEach(c -> System.out.println(c.getFood());
}
public static void main(String[] args) {
Test test = new Test();
List<Cat> cats = Arrays.asList(new Cat());
test.printAnimalFood(cats); // 编译错误,List<Cat>不是List<Animal>的子类
test.printCatFood(cats); // 正确执行
}
如果还需要增加Dog,Rabbit等子类,那么就需要增加相应对应的方法printDogFood,printRabbitFood等,久而久之代码就会很臃肿,那么怎么解决这件事呢,那就是接下来要介绍的通配符的相关知识。
1. 无界通配符
使用方法
Java泛型提供了通配符(Wildcards),用?
表示,例如List<?>
、Set<?>
等。我们知道Object
是任意类的父类,而在Java泛型中,通配符就是任意同泛型类型容器的父类,如List<?>
是所有泛型List的父类,为了与接下来的要介绍的另外两种形式的通配符予以区分,可以称之为无界通配符
,意味没有界限限制。
由于?
不是泛型标识,所以也就无法在类、接口以及方法中传递,以下写法是不允许的
public class TestGeneric<?> {
private ? a;
}
public <?> ? test(? a) {
// Do something
}
这么做也是合情合理,因为?
不符合Java变量的命名标准。所以?
只能出现在引用中或方法入参中,如
// 出现在引用中
Class<?> cls = xxx;
// 出现在方法入参中
public void print(List<?> list) {
// Do something
}
需要注意的是方法入参中的无界通配符
只起到了告知的作用,并不能进行传递,也不需要传递(因为是任意类型)。
总结:
无界通配符
不能标注在泛型类、泛型接口以及泛型方法上无界通配符
只能使用在引用以及方法入参中,并且无法进行传递
另外无界通配符
也可以起到占位以及修饰作用,可以直观告诉使用者这里可以接受任意类型。比如Java中的反射,我们经常写成下面的写法:
Class<?> cls = xxx;
这里其实通配符?
是可以不写的,但是写了就可以显示的告诉使用者,这里可以是任意泛型类型。
List<?>与List
知道了无界通配符的含义之后,我们可以把前言中的例子做一下优化
public void print(List<?> animals) {
animals.forEach(a -> System.out.println(((Animal) a).getFood()));
}
public static void main(String[] args) {
Test test = new Test();
List<Animal> animals = Arrays.asList(new Animal());
List<Cat> cats = Arrays.asList(new Cat());
test.print(animals); // 正确执行
test.print(cats); // 正确执行
}
使用无界通配符
后,即使后续有新增的子类,此方法也可以正确执行。至此我们解决了参数传递问题,但是新问题出现了:使用无界通配符
似乎与不使用泛型而直接使用List
没有任何区别:list中的元素可以是任意类型,并且取元素时需要强制转换。无界通配符
看起来并没有特殊的用途,那为什么要需要使用无界通配符
呢?
List
可以理解为持有任意Object类型的原始列表。List<?>
可以理解为想要使用泛型列表,但是不确定具体的类型,用?
标识任意类型都可以。由于是泛型,所以列表中的元素的类型理应一致,但是以下写法是可以通过编译的
// 正确执行
List list = new ArrayList();
list.add(1);
list.add("2");
List<?> list2 = list;
public void print(List<?> list) {
//..
}
// 正确执行
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("2");
new Test().print(list);
}
共同点:由于List<?>
和List
在编译期间都无法确定元素的实际类型,所以获取的时候都为Object类型
List<?> list1 = ...;
Object obj1 = list1.get(0);
List list2 = ...;
Object obj2 = list2.get(0);
不同点:List<?>
由于无法在编译期间确定泛型的实际类型,所以没法向List<?>
中添加除了null
外的任意类型元素。而List
由于可以存放Object对象,所以List
可以添加任意类型的元素。
List<?> list1 = new ArrayList<>();
list1.add(1); // 编译错误
list1.add("2"); // 编译错误
list1.add(null); // 正确执行
List list2 = new ArrayList();
list2.add(1); // 正确执行
list2.add("2"); // 正确执行
应用场景
可以使用无界通配符来实现通用的方法,如本节中的优化方案,使用List<?>
来实现print方法的通用,当然这里使用List
的效果是一样的,下例可能更能说明问题
public void testMap(Map<String, ?> map) {
// Do something
}
Map<String, String> map1 = ...;
Map<String, Integer> map2 = ...;
Map<String, Object> map3 = ...;
Test test = new Test();
test.testMap(map1); // 正确执行
test.testMap(map2); // 正确执行
test.testMap(map3); // 正确执行
testMap方法入参map的value想接受任意类型,只能使用无界通配符
来进行占位。
2. 上界通配符
无界通配符由于可以接受任意类型,所以某些情况下还是不太适用,往往需要强制转换类型,一是不方便,二是可能引发转换异常,这个时候就可能需要使用上界通配符
来解决我们的问题了。
上界通配符
(Upper Bounde Wildcard),顾名思义,存在一个最上级的界限,即指定一个最高级别的父类,它表示对于该上界类型以及其子类都适用。
我们也可以将无界通配符
上界为Object
的上界通配符
。
使用方法
基本写法
? extends xx
同无界通配符
,由于包含?
,所以这种写法只能出现在引用以及方法入参中,如:
List<? extends Number> numbers = xxx;
public void test(Class<? extends Number> cls) {
}
指定泛型标识
前面分析?
并不能进行泛型类型传递,所以如果想在在泛型类、泛型接口、泛型方法中使用上界通配符
,需要将?
指定为标识类型,如
// 泛型类
public class TestGeneric<T extends Number> {
private T t;
public TestGeneric(T t) {
this.t = t;
}
}
// 泛型接口
public interface TestGerneric<T extends Number> {
}
// 泛型
public <E extends Collection> void test(E e) {
System.out.println(e.size());
}
多重上界
当需要指定多个上界时,需要使用&
来连接,并且只能在指定泛型标识的时候使用,例如:
public class TestGeneric<T extends LongFur & BlueEye> {
}
上例中表示该泛型类型为同时满足为两个指定上界类型或其子类的类型。
需要注意的是,被指定的多个上界不能有有冲突的方法,否则会编译错误
特点
我们在编译期只能知道上界通配符
的上界是什么类型,所以在取元素时,只能获取到上界的类型。具体的实际类型只有在运行时才能确定,同时也构成了多态。
List<Integer> ints = Arrays.asList(1);
List<Double> doubles = Arrays.asList(2.2);
```java
public void test(List<? extends Number> numbers) {
// 编译期间无法确定实际类型是Integer、Long、Double还是其他,只能使用Number接收
// 运行时构成多态:Number number = 1; Number number = 2.2
Number number = numbers.get(0);
System.out.println(number);
}
同时与无界通配符
相似,由于编译期无法知晓具体的实际类型,所以不支持上界通配符
的容器添加元素(或赋值)
List<? extends Number> list = new ArrayList<>();
list.add(1); // 编译错误
list.add(2.2); // 编译错误
// 如果以上操作允许,那么list中的类型就不统一了
应用场景
我们再使用上界通配符
来优化前言的例子
public void print(List<? extends Animal> animals) {
animals.forEach(a -> System.out.println(a.getFood()));
}
public static void main(String[] args) {
Test test = new Test();
List<Animal> animals = Arrays.asList(new Animal());
List<Cat> cats = Arrays.asList(new Cat());
test.print(animals); // 正确执行
test.print(cats); // 正确执行
}
由于方法形参定义为List<? extends Animal>
,所以不用担心转型的问题,获取的元素肯定为Animal类型(包括子类),也不用担心实参类型传递错误。
通过优化我们可以感受到上界通配符
的好处,既能够做到向无界通配符
一样的通用型,又能够限制具体的类型范围,所以如果想达到此目的,就可以使用上界通配符
,而实际编码中我们使用上界通配符
的次数也是最多的。
以JDK为例,Collection#addAll
方法就使用了上界通配符
3. 下界通配符
与上界通配符
相反,下界通配符
(Lower Bound Wildcard),顾名思义,存在一个最低级的界限,即指定一个最低级别的子类,它表示对于该下界类型以及其父类都适用。
使用方法
下界通配符
与上界通配符
类似,只需将extends
改为super
即可:
List<? super Integer> list = xxx;
public void test(Class<? super Integer> cls) {
}
值得注意的是下界通配符
不支持制定泛型标识以及多重下界的写法。
特点
与上界通配符
相反,我们在编译期只能知道下界通配符
的下界是什么类型,所以在添加元素时,只能向其中添加下界类型。
public void test(List<? super Integer> list) {
// 编译期间无法确定实际类型是Integer还是Number或更高级别的父类,只能添加Integer
list.add(1);
}
List<Number> numbers = Arrays.asList(1, 2L, 3.3);
xxx.test(numbers);
同时由于编译期无法知晓具体的实际类型,所以只能使用Object
来接收获取的元素
List<? super Integer> list = new ArrayList<>();
// 编译失败,因为无法确定是否是Integer,有可能是Number
Integer i = list.get(0);
// 编译失败,因为无法确定是否是Number,有可能是Object
Number m = list.get(1);
// 编译正确,Object可以接受任意值
Object o = list.get(2);
应用场景
下界通配符
在实际使用中还是比较少的,可以通过JDK的实际例子来感受一下使用方式
在java.util.Comparator
接口中存在大量的下界通配符
的使用,如