《Java编程的逻辑》笔记7: 泛型

Part3 泛型与容器

第8章 泛型

8.1 基本概念和原理
  • 1.什么是泛型

泛型将接口的概念进一步延伸,"泛型"字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,同时,还可以提高代码的可读性和安全性。

  • 2.简单的泛型类

      public class Pair<T> {
          T first;
          T second;
    
          public Pair(T first, T second) {
              this.first = first;
              this.second = second;
          }
    
          public T getFirst() {
              return first;
          }
    
          public T getSecond() {
              return second;
          }
      }
    

    在上述中T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。使用泛型类:

      public static void main(String[] args) {
          Pair<Integer> minmax = new Pair<>(1, 100);
          Integer min = minmax.getFirst();
          Integer max = minmax.getSecond();
          System.out.println("min: " + min);
          System.out.println("max: " + max);
      }
    

    类型参数可以有多个,Pair类中的first和second可以是不同的类型, 改进后的Pair类定义:

      public class Pair<U, V> {
          U first;
          V second;
    
          public Pair(U first, V second) {
              this.first = first;
              this.second = second;
          }
    
          public U getFirst() {
              return first;
          }
    
          public V getSecond() {
              return second;
          }
    
          public static void main(String[] args) {
              Pair<String, Integer> pair = new Pair<>("xinyue", 100);
              String first = pair.getFirst();
              Integer second = pair.getSecond();
              System.out.println("min: " + first);
              System.out.println("max: " + second);
          }
      }
    
  • 3.基本原理

    泛型类型参数到底是什么呢?为什么一定要定义类型参数呢?定义普通类,直接使用Object不就行了吗?

    比如,Pair类可以写为:

      public class Pair {
    
          Object first;
          Object second;
          
          public Pair(Object first, Object second){
              this.first = first;
              this.second = second;
          }
          
          public Object getFirst() {
              return first;
          }
          
          public Object getSecond() {
              return second;
          }
      }
    

    使用Pair类:

      Pair minmax = new Pair(1,100);
      Integer min = (Integer)minmax.getFirst();
      Integer max = (Integer)minmax.getSecond();
    
      Pair kv = new Pair("name","xinyue");
      String key = (String)kv.getFirst();
      String value = (String)kv.getSecond();
    

    上述定义正是Java内部泛型的实现原理:

    对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair类代码及其使用代码一样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,它只知道普通的类及代码。

    Java泛型是通过擦除实现的

  • 4.泛型的好处

    既然只使用普通类和Object就是可以的,而且泛型最后也转换为了普通类,那为什么还要用泛型呢?或者说,泛型到底有什么好处呢?

    主要有两个好处:

    • 更好的安全性
    • 更好的可读性

    语言和程序设计的一个重要目标是将bug尽量消灭在摇篮里,即在编译时就发现并报告错误,而不是等到运行时。

    只使用Object,代码写错的时候,开发环境和编译器不能帮我们发现问题:

      Pair pair = new Pair("xinyue",1);
      Integer id = (Integer)pair.getFirst();
      String name = (String)pair.getSecond();
    

    上述代码编译时并不会报错,但在运行时,程序会抛出类型转换异常ClassCastException。如果使用泛型,则不可能犯这个错误:

      Pair<String,Integer> pair = new Pair<>("xinyue",1);
      Integer id = pair.getFirst();
      String name = pair.getSecond();
    

    上述代码会在编译时开发环境和编译器就会提示你错误,这称之为类型安全

  • 5.容器类

    泛型类最常见的用途是作为容器类,所谓容器类,简单说就是容纳并管理多项数据的类.

    如Java集合中的ArrayListHashMap在使用的时候需要传递相应参数类型:

    List<Integer> li = new ArrayList<>();
    Map<String, Integer> map = new HashMap<>();
    

    查看ArrayList内部源码,理解其实现原理。

  • 6.泛型方法

    除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。

      //泛型方法测试
      public class GeMethod {
          public static <T> int indexOf(T[] arr, T elm) {
              for(int i=0; i<arr.length; i++) {
                  if(arr[i].equals(elm)) {
                      return i;
                  }
              }
              return -1;
          }
    
          public static void main(String[] args) {
              int index = indexOf(new Integer[]{1,3,5}, 3);
              System.out.println(index);
          }
      }
    

    这个方法就是一个泛型方法,类型参数为T,放在返回值前面,注意泛型只能使用包装类型(Integer)进行实例化,不能是int。

    多个泛型参数:

      public static <U,V> Pair<U,V> makePair(U first, V second){
          Pair<U,V> pair = new Pair<>(first, second);
          return pair;
      }
    
      //makePair(1,"xinyue");
    
  • 7.泛型接口

    接口也可以是泛型的, 如Comparable和Comparator接口:

      public interface Comparable<T> {
          public int compareTo(T o);
      }
      public interface Comparator<T> {
          int compare(T o1, T o2);
          boolean equals(Object obj);
      }
    

    实现接口时,应该指定具体的类型,比如,对Integer类,实现代码是:

      public final class Integer extends Number implements Comparable<Integer>{
          public int compareTo(Integer anotherInteger) {
              return compare(this.value, anotherInteger.value);
          }
          //...
      }
    
  • 8.类型参数的限定

    在之前的介绍中,无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当做Object,但Java支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过extends这个关键字来表示的。

    (1)上界为某个具体类

    比如说,上面的Pair类,可以定义一个子类NumberPair,限定两个类型参数必须为Number,代码如下:

      public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
          public NumberPair(U first, V second) {
              super(first, second);
          }
      }
    

    限定类型后,就可以使用该类型的方法了,比如说,对于NumberPair类,first和second变量就可以当做Number进行处理了:

      public double sum() {
          return getFirst().doubleValue() + getSecond().doubleValue();
      }
    

    使用方法:

      public static void main(String[] args) {
          NumberPair<Integer,Double> pair = new NumberPair<>(10, 12.34);
          double sum = pair.sum();
          System.out.println(sum);
      }
    

    限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型

    (2)上界为某个接口

    在泛型方法中,一种常见的场景是限定类型必须实现Comparable接口:

      public static <T extends Comparable> T max(T[] arr){
          T max = arr[0];
          for(int i=1; i<arr.length; i++){
              if(arr[i].compareTo(max)>0){
                  max = arr[i];
              }
          }
          return max;
      }
    

    不过,直接这么写代码,Java中会给一个警告信息,因为Comparable是一个泛型接口,它也需要一个类型参数,所以完整的方法声明应该是:

      public static <T extends Comparable<T>> T max(T[] arr){
    
      //...
    
      }
    

    这种形式称之为递归类型限制

    (3)上界为其他类型参数

    上面的限定都是指定了一个明确的类或接口,Java支持一个类型参数以另一个类型参数作为上界.

    具体原因还不是很理解。

泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法,能够应用于各种数据类型,而且还可以保证类型安全,提高可读性.

8.2 泛型通配符
  • 为什么需要通配符

    正是为了解决保持「向上转型」概念在 Java 语言中的统一,使泛型也支持向上转型,所以 Java 推出了通配符的概念

    我们看一个具体的例子:

    class Fruit {}
    class Apple extends Fruit {}
    

    上述Apple类是Fruit类的子类,在Java中,我们可以这样写:

    Fruit apple = new Apple();
    

    使用 Fruit 类型的变量指向了一个 Apple对象,这称之为向上转型。现在我们有一个Plate类表示装水果的盘子,其代码如下:

    public class Plate<T> {
      private List<T> list;
      public Plate() {list = new ArrayList<>();}
      public void add(T item) {list.add(item);}
      public T get() {return list.get(0);}
    }
    

    我们可以这样定义一个装水果的盘子:

    Plate<Fruit> plate = new Plate<Fruit>();
    plate.add(new Fruit());
    plate.add(new Apple());
    

    按照Java向上转型的原则,我们这样定义:

    Plate<Fruit> plate = new Plate<Apple>();  //Error
    

    上述代码编译时会报错,原因时泛型并不直接支持向上转型。使用通配符后就可以正常编译了,修改后如下:

    Plate<? extends Fruit> plate = new Plate<Apple>();
    

    上述我们使用的是上界通配符。

  • 无界通配符

    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

    Plate<?> plate = new Plate<Fruit>();
    

    上述无界通配符也可以改为普通的类型参数。

  • 上界通配符

    使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据

    Plate<? extends Fruit> plate = new Plate<XXX>()
    

    上面我们对盘子的定义中,plate 可以指向任何 Fruit 类对象,或者任何 Fruit 的子类对象,我们下面几种定义都是正确的:

    Plate<? extends Fruit> plate = new Plate<Apple>();
    Plate<? extends Fruit> plate = new Plate<Fruit>();
    

    注意:使用上界通配符只能读,不能写

    plate<? extends Fruit> plate = new Plate<Apple>();
    //不能存入东西
    p.add(new Fruit()); //Error
    p.add(new Apple()); //Error
    //读取出的东西只能存放在Fruit或其基类里
    Fruit f1 = p.get();
    Object f2 = p.get();
    Apple f3 = p.get(); //Error
    

    这是因为我们在使用上界通配符的时候表明容器里是Fruit类或其基类,但具体是什么类型JVM不知道。既然我们不能确定要往里面放的类型,那 JVM 就干脆什么都不给放,避免出错。

    那为什么又可以取出数据呢?因为无论是取出苹果,还是橙子,还是香蕉,我们都可以通过向上转型用 Fruit 类型的变量指向它

  • 下界通配符

    使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。

    Plate<? super Apple> plate = new Plate<Fruit>();
    

    上述表示plate可以指向Apple或Apple的父类。

    注意:下界通配符可以往里存,但往外取只能放在Object对象里

  • PECS原则

    (1)Producer Extends 说的是当你的情景是生产者类型,需要获取资源以供生产时,我们建议使用 extends 通配符,因为使用了 extends 通配符的类型更适合获取资源。

    (2)Consumer Super 说的是当你的场景是消费者类型,需要存入资源以供消费时,我们建议使用 super 通配符,因为使用 super 通配符的类型更适合存入资源。

参考:

  1. 《Java编程的逻辑》 第8章 泛型
  2. https://www.zhihu.com/question/20400700
  3. https://www.imooc.com/article/22909
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值