泛型的通配符扩展
为了对通配符的了解更为透切,定义如下几个类:
public class Animal { private String name; public Animal(String name) { this.name = name; } public void eat() { System.out.println(getName() + " can eat."); } public String getName(){ return name; } }
public class Cat extends Animal { public Cat(String name) { super(name); } public void jump(){ System.out.println(getName() + " can jump."); } }
public class Bird extends Animal { public Bird(String name) { super(name); } public void fly(){ System.out.println(getName() + " can fly."); } }
public class Magpie extends Bird { public Magpie(String name) { super(name); } public void sing(){ System.out.println(getName() + " can not only eat,but sing"); } }
首先我们看一下无通配符的使用示例,如下:
public class AnimalTrainer { public void act(List<Animal> list) { for (Animal animal : list) { animal.eat(); } } }
测试代码如下:
public class TestAnimal { public static void main(String[] args) { AnimalTrainer animalTrainer = new AnimalTrainer(); //Test 1 List<Animal> animalList = new ArrayList<>(); animalList.add(new Cat("cat1")); animalList.add(new Bird("bird1")); animalTrainer.act(animalList); //可以通过编译 //Test 2 List<Cat> catList = new ArrayList<>(); catList.add(new Cat("cat2")); catList.add(new Cat("cat3")); animalTrainer.act(catList); //无法通过编译 } }
如上,Test 1 的执行应该可以理解的,不过顺带提醒一下的是,因为cat1和bird1都是Animal对象,自然可以添加List<Animal>里 。对于Test 2,无法通过编译是因为List<Cat>并不是List<Animal>子类,传入参数有误,也就无法通过编译了。现在尝试去修改AnimalTrainer.act()代码,让它变得更为通用的一点,即不仅仅是接受List<Animal>参数,还可以接受List<Bird>等参数。那如何更改呢?
一、通配符的上界
既然知道List<Cat>并不是List<Anilmal>的子类型,那就需要去寻找替他解决的办法, 是AnimalTrianer.act()方法变得更为通用(既可以接受List<Animal>类型,也可以接受List<Cat>等参数)。在java里解决办法就是使用通配符“?”,具体到AnimalTrianer,就是将方法改为act(List<? extends Animal> list),当中“?”就是通配符,而“? extends Animal”则表示通配符“?”的上界为Animal,换句话说就是,“? extends Animal”可以代表Animal或其子类,可代表不了Animal的父类(如Object),因为通配符的上界是Animal。如下,为改进之后的AnimalTrianer.
public class AnimalTrainer { public void act(List<? extends Animal> list) { for (Animal animal : list) { animal.eat(); } } }
再来测试一下,如下,发现Test2 可以通过编译了:
public class TestAnimal { public static void main(String[] args) { AnimalTrainer animalTrainer = new AnimalTrainer(); //Test 1 List<Animal> animalList = new ArrayList<>(); animalList.add(new Cat("cat1")); animalList.add(new Bird("bird1")); animalTrainer.act(animalList); //可以通过编译 //Test 2 List<Cat> catList = new ArrayList<>(); catList.add(new Cat("cat2")); catList.add(new Cat("cat3")); animalTrainer.act(catList); //也可以通过编译 } }
经过上述分析,可以知道List<Animal>和List<Cat>都是List<? extends Animal>的子类型,类似有List<Bird>,List<Magpie>也是List<? extends Animal>的子类型。现总结如下,对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List<E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))
- G<? extends Y> 是 G<? extends X>的子类型(如List<? extends Cat> 是 List<? extends Animal>的子类型)。
- G<X> 是 G<? extends X>的子类型(如List<Animal> 是 List<? extends Animal>的子类型)
- G<?> 与 G<? extends Object>等同,如List<?> 与List<? extends Objext>等同。
学到这里,可能会遇到一些疑惑的地方,或者说事理解不透的地方,先观察如下两段代码片段,判断一下其是否可行??
public void testAdd(List<? extends Animal> list){ list.add(new Animal("animal")); list.add(new Bird("bird")); list.add(new Cat("cat")); }
给testAdd传入不同的参数,三个“add”操作都可能引发类型不兼容问题,而传入的参数是未知的,所以java为了保护其类型一致,禁止向List<? extends Animal>添加任意对象,不过却可以添加 null,即list.add(null)是可行的。 这个方法的组要作用其实在于对list遍历操作,而不是修改list。而且是除了设置为null,是不能修改的。
使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
二、通配符的下界
既然有了通配符的上界,自然有着通配符的下界。可以如此定义通配符的下界 List<? super Bird>,其中”Bird“就是通配符的下界。注意:不能同时声明泛型通配符申明上界和下界。
在谈注意细节之前,我们先看一下通配符的使用规则——对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List<E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))
- G<? super X> 是 G<? super Y>的子类型(如List<? super Animal> 是 List<? super Bird>的子类型)。
- G<X> 是 G<? super X>的子类型(如List<Animal> 是 List<? super Animal>的子类型)
现在再来看如下代码,判断其是否符合逻辑:
public void testAdd(List<? super Bird> list){ list.add(new Bird("bird")); list.add(new Magpie("magpie")); }
看第一段代码,其分析如下,因为”? super Bird”代表了Bird或其父类,而Magpie是Bird的子类,所以上诉代码不可通过编译。而事实上是”行“,为什么呢?2?
在解疑之前,再来强调一个知识点,子类可以指向父类,即Bird也是Animal对象。现在考虑传入到testAdd()的所有可能的参数,可以是List<Bird>,List<Animal>,或者List<Objext>等等,发现这些参数的类型是Bird或其父类,那我们可以这样看,把bird、magpie看成Bird对象,也可以将bird、magpie看成Animal对象,类似的可看成Object对象,最后发现这些添加到List<? supe Bird> list里的对象都是同一类对象(如本文刚开篇提到的Test 1),因此testAdd方法是符合逻辑,可以通过编译的。
既然无法确定其父类对象,那该如何遍历List<? super Bird> ? 因为Object是所有类的根类,所以可以用Object来遍历。如下,不过貌似其意义不大。
for (Object object : list) {//...}
三、无界通配符
1.问题,定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?
错误的方式:
/** * 定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢? * 错误方式 */ public void printCollection1(Collection<Object> cols){ for(Object obj : cols){ System.out.println(obj); } cols.add("abc"); //没错 //cols = new HashSet<String>(); //错误的,不符合参数化类型不考虑类型参数的继承关系参数化类型不考虑类型参数的继承关系 }
Cols<Object> 中的Object只是说明Cols<Object> 实例对象中的方法接受的参数是Object,Cols<Object> 是一种具体类型,new HashSet<Date>也是一种具体类型,两者没有兼容性问题。只有参数类型一样才是同一种参数化类型。
2.正确方式
/** * 定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢? * 正确方式 */ public void printCollection2(Collection<?> cols){ for(Object obj : cols){ System.out.println(obj); } //cols.add("abc"); //错误的,因为它不知自己未来匹配就一定是String。 cols.size();//没错,此方法与类型参数没有关系 cols = new HashSet<Date>(); //正确的 }
Collection<?> a可以与任意参数化的类型匹配,但到底匹配的是什么类型,只有以后才知道,所以,a = new ArrayList<Integer>和a = new ArrayList<String>都可以,但a.add(new Date())或a.add(“abc”)都不行。
3.注意点
a). 使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
b). List<?> = List<? extends Object>
c).List<Object>与List<?>并不等同,List<Object>是List<?>的子类。还有不能往List<?> list里添加任意对象,除了null。
四、总结
1.设置通配符的上界,即<? extends Animal>的list,可以调用与参数化无关的方法,不能调用与参数化有关的方法,只能add(null),其他都不可以。
2.设置了通配符下界的,即<? super Animal>的list,能够add Animal本身极其子类。
3.无界通配符,即<?> 等同于<? extends Object>的list,可以调用与参数化无关的方法,不能调用与参数化有关的方法,只能add(null),其他都不可以。
4.使用了通配符的泛型类型作为方法的参数的方法,一般是用来读取这个参数的,而不是在方法里修改的。
参考文档:
1.http://825635381.iteye.com/blog/2017650
2.https://www.zhihu.com/question/31429113
3.http://zhidao.baidu.com/link?url=jQMPmgEBj1ixHhp52XwcllWJHlsFpKtxOB94LWtG1zkF9A34nX-xP71Pc2ao1ityHLHt9tNXw8I8XLiEdA2yYq
4.http://www.linuxidc.com/Linux/2013-10/90928.htm
Not more, but better!