泛型通配符详解

一、基本概念:
在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:
1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>.
  无边界的通配符的主要作用就是让泛型能够接受未知类型的数据. 
2. 固定上边界的通配符(Upper Bounded Wildcards):
  使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类. 
3. 固定下边界的通配符(Lower Bounded Wildcards):
  使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界.

二、基本使用方法:
1. 无边界的通配符的使用, 我们以在集合List中使用<?>为例. 如:

 1 public static void printList(List<?> list) {
 2     for (Object o : list) {
 3         System.out.println(o);
 4     }
 5 }
 6
 7 public static void main(String[] args) {
 8     List<String> l1 = new ArrayList<>();
 9     l1.add("aa");
10     l1.add("bb");
11     l1.add("cc");
12     printList(l1);
13     List<Integer> l2 = new ArrayList<>();
14     l2.add(11);
15     l2.add(22);
16     l2.add(33);
17     printList(l2);
18
19 }

这种使用List<?>的方式就是父类引用指向子类对象. 注意, 这里的printList方法不能写成public static void printList(List<Object> list)的形式, 原因我在上一篇博文中已经讲过, 虽然Object类是所有类的父类, 但是List<Object>跟其他泛型的List如List<String>, List<Integer>不存在继承关系, 因此会报错.
有一点我们必须明确, 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素. 请看下面代码:

1 public static void addTest(List<?> list) {
2     Object o = new Object();
3     // list.add(o); // 编译报错
4     // list.add(1); // 编译报错
5     // list.add("ABC"); // 编译报错
6     list.add(null);
7 }

由于我们根本不知道list会接受到具有什么样的泛型List, 所以除了null之外什么也不能add.
还有, List<?>也不能使用get方法, 只有Object类型是个例外. 原因也很简单, 因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get, 但是Object是所有数据类型的父类, 所以只有接受他可以, 请看下面代码:

1 public static void getTest(List<?> list) {
2     // String s = list.get(0); // 编译报错
3     // Integer i = list.get(1); // 编译报错
4     Object o = list.get(2);
5 }

那位说了, 不是有强制类型转换么? 是有, 但是我们不知道会传入什么类型, 比如我们将其强转为String, 编译是通过了, 但是如果传入个Integer泛型的List, 一运行还会出错. 那位又说了, 那么保证传入的String类型的数据不就好了么? 那样是没问题了, 但是那还用<?>干嘛呀? 直接List<String>不就行了.

2. 固定上边界的通配符的使用, 我仍旧以List为例来说明:

 1 public static double sumOfList(List<? extends Number> list) {
 2     double s = 0.0;
 3     for (Number n : list) {
 4         // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
 5         s += n.doubleValue();
 6     }
 7     return s;
 8 }
 9
10 public static void main(String[] args) {
11     List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
12     System.out.println(sumOfList(list1));
13     List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
14     System.out.println(sumOfList(list2));
15 }

有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:

1 public static void addTest2(List<? extends Number> l) {
2     // l.add(1); // 编译报错
3     // l.add(1.1); //编译报错
4     l.add(null);
5 }

原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:

1 List<Integer> list = new ArrayList<>();
2 addTest(list);

那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能add. 但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.

3. 固定下边界通配符的使用. 这个较前面的两个有点难理解, 首先仍以List为例:

 1 public static void addNumbers(List<? super Integer> list) {
 2     for (int i = 1; i <= 10; i++) {
 3         list.add(i);
 4     }
 5 }
 6
 7 public static void main(String[] args) {
 8     List<Object> list1 = new ArrayList<>();
 9     addNumbers(list1);
10     System.out.println(list1);
11     List<Number> list2 = new ArrayList<>();
12     addNumbers(list2);
13     System.out.println(list2);
14     List<Double> list3 = new ArrayList<>();
15     // addNumbers(list3); // 编译报错
16 }

我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代码:

1 public static void getTest2(List<? super Integer> list) {
2     // Integer i = list.get(0); //编译报错
3     Object o = list.get(1);
4 }

这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.
使用? super E还有个常见的场景就是Comparator. TreeSet有这么一个构造方法:

1 TreeSet(Comparator<? super E> comparator) 

就是使用Comparator来创建TreeSet, 大家应该都清楚, 那么请看下面的代码:

 1 public class Person {
 2     private String name;
 3     private int age;
 4     /*
 5      * 构造函数与getter, setter省略
 6      */
 7 }
 8
 9 public class Student extends Person {
10     public Student() {}
11
12     public Student(String name, int age) {
13         super(name, age);
14     }
15 }
16
17 class comparatorTest implements Comparator<Person>{
18     @Override
19     public int compare(Student s1, Student s2) {
20         int num = s1.getAge() - s2.getAge();
21         return num == 0 ? s1.getName().compareTo(s2.getName()) :  num;
22     }
23 }
24
25 public class GenericTest {
26     public static void main(String[] args) {
27         TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
28         ts1.add(new Person("Tom", 20));
29         ts1.add(new Person("Jack", 25));
30         ts1.add(new Person("John", 22));
31         System.out.println(ts1);
32
33         TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
34         ts2.add(new Student("Susan", 23));
35         ts2.add(new Student("Rose", 27));
36         ts2.add(new Student("Jane", 19));
37         System.out.println(ts2);
38     }
39 }

不知大家有想过没有, 为什么Comparator<Person>这里用的是父类Person, 而不是子类Student. 初学时很容易困惑, ? super E不应该E是子类才对么? 其实, 实现接口时我们所设定的类型参数不是E, 而是?; E是在创建TreeSet时设定的. 如:

1 TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
2 TreeSet<Student> ts2 = new TreeSet(new comparatorTest());

这里实例化的comparatorTest的泛型就是<Student super Student>和<Person super Student>(我这么写只是为了说明白). 在实现接口时使用:

1 // 这是错误的
2 class comparatorTest implements Comparator<Student> {...}

那么上面的结果就成了: <Student super Person>和<Person super Person>, <Student super Person>显然是错误的.

三、总结:

我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:

  • in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
  • out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
  • 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
  • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.
时间: 2024-12-15 04:46:58

泛型通配符详解的相关文章

java 泛型实例详解(普通泛型、 通配符、 泛型接口)

java 泛型详解(普通泛型. 通配符. 泛型接口) 2013-02-04 19:49:49| 分类: JAVA | 标签:java |举报|字号 订阅 下载LOFTER客户端 JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能-----Java的泛型. 1.Java泛型 其实Java的泛型就是创建一个用类型作为参数的类.就象我们写类的方法一样,方法是这样的method(String str1,String

泛型与通配符详解

1回顾泛型类 泛型类:具有一个或多个泛型变量的类被称之为泛型类. class ClassGenericity<T> { //在类里面可以直接使用T的类型 T aa; public void test11(T bb) { //................ } //静态方法 在类上面定义的泛型,不能再静态方法里面使用 public static <A> void test12(A cc) { //.................. } } public class TestCla

Java中泛型区别以及泛型擦除详解

一.引言 复习javac的编译过程中的解语法糖的时候看见了泛型擦除中的举例,网上的资料大多比较散各针对性不一,在此做出自己的一些详细且易懂的总结. 二.泛型简介 泛型是JDK 1.5的一项新特性,一种编译器使用的范式,语法糖的一种,能保证类型安全.[注意:继承中,子类泛型数必须不少于父类泛型数] 为了方便理解,我将泛型分为普通泛型和通配泛型 三.泛型分类 1.普通泛型 就是没有设置通配的泛型,泛型表示为某一个类. 声明时: class Test<T>{...} 使用时: Test<Int

CSS系列(6) CSS通配符详解

通配符使用星号*表示,意思是“所有的”. 平时使用电脑,比如要搜索C盘里所有的网页,可以使用 *.html来搜索,.html是网页的后缀名,*代表了所有网页的名称: 也就是使用 * 加后缀名,就可以在电脑中搜索文件. 在CSS中,同样使用 * 代表所有的标签或元素,它叫做通配符选择器. 比如:* { color : red; } 这里就把所有元素的字体设置为红色. *会匹配所有的元素,因此针对所有元素的设置可以使用*来完成,用的最多的例子如下: *{margin:0px; padding:0px

Struts2通配符详解

Struts.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC     "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"     "http://struts.apache.org/dtds/struts-2.3.dtd"> <str

C#泛型实例详解

本文以实例形式讲述了C#泛型的用法,有助于读者深入理解C#泛型的原理,具体分析如下: 首先需要明白什么时候使用泛型: 当针对不同的数据类型,采用相似的逻辑算法,为了避免重复,可以考虑使用泛型. 一.针对类的泛型 针对不同类型的数组,写一个针对数组的"冒泡排序". 1.思路 ● 针对类的泛型,泛型打在类旁. ● 由于在"冒泡排序"中需要对元素进行比较,所以泛型要约束成实现IComparable接口. 1 class Program 2 { 3 static void

c#泛型使用详解:泛型特点、泛型继承、泛型接口、泛型委托

泛型:通过参数化类型来实现在同一份代码上操作多种数据类型.利用"参数化类型"将类型抽象化,从而实现灵活的复用.在.NET类库中处处都可以看到泛型的身影,尤其是数组和集合中,泛型的存在也大大提高了程序员的开发效率.更重要的是,C#的泛型比C++的模板使用更加安全,并且通过避免装箱和拆箱操作来达到性能提升的目的.因此,我们很有必要掌握并善用这个强大的语言特性. C#泛型特点: 1.如果实例化泛型类型的参数相同,那么JIT编辑器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模板可能

一步一步造个Ioc轮子(二),详解泛型工厂

一步一步造个Ioc轮子目录 .net core发布了,一步一步造个Ioc轮子,弄点.net魔法,近new的速度(一) 一步一步造个Ioc轮子(二),详解泛型工厂 详解泛型工厂 既然我说Ioc容器就是一个豪华版工厂,自动化装配的工厂,那我们就从工厂入手吧,先造个工厂,然后升级成Ioc容器 首先我们来写一个最最最简单的抽象工厂类,还是以前一篇的短信为例 public class SMSFactory { public static ISMS Get() { return new XSMS(); }

java 泛型详解(普通泛型、 通配符、 泛型接口)

java 泛型详解(普通泛型. 通配符. 泛型接口) JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能-----Java的泛型. 1.Java泛型 其 实Java的泛型就是创建一个用类型作为参数的类.就象我们写类的方法一样,方法是这样的method(String str1,String str2 ),方法中参数str1.str2的值是可变的.而泛型也是一样的,这样写class Java_Generics<K