本帖最后由 长沙-小知姐姐 于 2019-1-3 09:31 编辑
1. 基本概念:
1.无边界的通配符:
主要作用就是让泛型能够接受未知类型的数据。
2.固定上边界的通配符:
接受指定类及其子类类型的数据。
3.固定下边界的通配符:
接受指定类及其父类类型的数据。
2. 基本使用方法:
2.1. 无边界的通配符的使用: 我们以在集合List中使用<?>为例:
这种使用List<?>的方式就是父类引用指向子类对象. 注意, 这里的printList方法不能写成public static void printList(List list)的形式,因为,虽然Object是所有引用数据类型的父类,但是List并不是List的父类,所以如果定义成List,那么就只能传List类型进入方法; 有一点我们必须明确, 我们不能对List<?>使用add方法, 仅有一个例外, 就是 add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素. 请看下面代码
由于我们根本不知道list会接受到具有什么样的泛型List, 所以除了null之外什么也不能add.还有, \List<?>也不能使用get方法, 只有Object类型是个例外.原因也很简单, 因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get,但是Object是所有数据类型的父类, 所以只有接受他可以, 请看下面代码:
那位说了, 不是有强制类型转换么? 是有, 但是我们不知道会传入什么类型, 比如我们将其强转为String,编译是通过了, 但是如果传入个Integer泛型的List, 一运行还会出错. 那位又说了, 那么保证传入的String类型的数据不就好了么? 那样是没问题了, 但是那还用<?>干嘛呀? 直接List不就行了
2..2 固定上边界的通配符的使用:
我们仍旧以List为例来说明:
注意:List<?extends E>不能使用add方法, 请看如下代码:
原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer,也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:
那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能 add. 但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.
2.3. 固定下边界通配符的使用:
这个较前面的两个有点难理解, 首先仍以List为例:
这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型. 使用? super E还有个常见的场景就是Comparator. TreeSet有这么一个构造方法:就是使用Comparator来创建TreeSet, 大家应该都清楚, 那么请看下面的代码:
不知大家有想过没有, 为什么Comparator这里用的是父类Person, 而不是子类 Student. 初学时很容易困惑, ? super E不应该E是子类才对么? 其实, 实现接口时我们所设定的类型参数不是E, 而是?; E是在创建TreeSet时设定的. 如
这里实例化的comparatorTest的泛型就是和(我这么写只是为了说明白). 在实现接口时使用:
那么上面的结果就成了: 和, 显然是错误的.
3. 总结: 我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:
1,in或者producer就是你要读取出数据以供随后使用(想象一下List的 get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
2,out或者consumer就是你要将已有的数据写入对象(想象一下List的 add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
3,当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符; 4,当你需要一个既能读又能写的对象时, 就不要使用通配符了.
|