zoukankan      html  css  js  c++  java
  • Java泛型学习笔记

    一、基本概念:
    在学习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类中的方法访问时, 使用无边界通配符;
    • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.

    P.S. 泛型的通配符感觉好麻烦, 中英文资料研究了一整天搞了个大概其. 自己能力有限也不知表达清楚了没有. 

    References:
    [1] The Java™ Tutorials - Upper Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/upperBounded.html
    [2] The Java™ Tutorials - Lower Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html
    [3] The Java™ Tutorials - Guidelines for Wildcard Use - https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html
    [4] stackoverflow - Difference between <? super T> and <? extends T> in Java - http://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java

  • 相关阅读:
    poj 3321 Apple Tree
    hdu 1520 Anniversary party
    Light OJ 1089 Points in Segments (II)
    Timus 1018 Binary Apple Tree
    zoj 3299 Fall the Brick
    HFUT 1287 法默尔的农场
    Codeforces 159C String Manipulation 1.0
    GraphQL + React Apollo + React Hook 大型项目实战(32 个视频)
    使用 TypeScript & mocha & chai 写测试代码实战(17 个视频)
    GraphQL + React Apollo + React Hook + Express + Mongodb 大型前后端分离项目实战之后端(19 个视频)
  • 原文地址:https://www.cnblogs.com/zhenyu-go/p/5536667.html
Copyright © 2011-2022 走看看