【后端之旅】泛型语法糖 Generics 篇

170 阅读7分钟

你是清华土木博士,毕业进了央企当经理;我是二本计科学士,毕业去了美团送外卖。你我都是共产主义接班人,我们都有光明的未来!

泛型是什么

泛型的本质是参数化类型。要掌握泛型,就必须了解到 Java 虚拟机对泛型一无所知。所有对泛型的各种操作,都是由编译器完成的。在本系列文章的第一篇,我们已经了解了多态这个概念,并且知道了所有的类都是 Object 的子类及其后代。泛型恰恰是(编译器)将模板类型全部视为 Object 类型,然后在获取模板类型变量时又强制转换为实际类型。

泛型的局限

由上一小节,我们知道了所有的模板类型都会被编译器编译成 Object 类型,所有模板类型修饰的变量在获取时的代码都会被加上强制(实际)类型转换。而我们知道,非基本类型包装类的引用类型是无法被强制转换为基本类型的!于是泛型就有了以下的局限:

  • 模板类型不能是基本类型
  • 不能实例化一个泛型数组
  • 在代码中无法获知模板类型的真实类型
  • 本类无法获取模板类型的真实类型,也就无法将模板类型实例化(编译器直接不允许)
  • 不同实际类型的泛型,其实例化后调用 getClass(),所获取的类是同一个,因为实际类型都被替换成 Object

泛型的使用

  • 泛型方法

    • 返回值类型被泛型类型修饰 的方法即为泛型方法
    • 定义格式:修饰符 泛型标记符一 方法名(泛型标记符二或常规类型 参数名, ...)
      /**
       * 假设当前类名为 Service
       */
      public T getObjectById(Integer id) {
          // 这一句看不懂没有关系,直接略过即可。未来的文章会详细介绍 Optional
          Optional<T> res = this.findById(id);
      
          if (res == null !! res.isPresent() != true) {
              return null;
          }
          return res.get();
      }
      
    • 调用格式:方法名(实参),只是实参可以不同的引用类型,或者不同的引用类型的数组
      Service service = new Service();
      
      // 查询学生信息
      Student object1 = getObjectById(1);
      
      // 查询老师信息
      Teacher object2 = getObjectById(9);
      
  • 泛型类(接口)

    • 类(接口)的名字后接 <泛型类型> 即为泛型类(接口)
    • 定义格式:修饰符 类名<泛型标记符>
      public class Couple<T, K> {
          private T male;
          private K female;
          
          public Couple(T male, K female) {
              this.male = male;
              this.female = female;
          }
      }
      
    • 调用格式:类名<实际类型名> 变量标记符 = new 类名<实际类型名>()
      /**
       * 假设当前已经引入了 Engineer 和 Teacher 等职业实体类
       */
      
      // 声明的部分已经确定了 T 和 K 的值,因此实例化的部分可省略 <> 中的值,由编译器推断获得
      Couple<Engineer, Teacher> couple = new Couple<>(engineer, teacher);
      

标记符与通配符

泛型的局限 一节,我们已经知道泛型 T 在编译阶段会被编译成 Object。但在这个过程中,编译器还是会根据 T 的实际值来判断两个泛型是否可转换。所以以下代码是无法通过编译的:

class Count<T> {  
    private T t;  
    public Count(T t) {  
        this.t = t;  
    }  
}

public class Application {
    public static void main(String[] args) {
        Count<Integer> j = new Count<>(3);
        
        // 以下代码会报错,编译无法通过!
        // java: 不兼容的类型: Count<java.lang.Integer>无法转换为 Count<java.lang.Number>
        Count<Number> i = j;
    }  
}

可以看到,尽管 IntegerNumber 的子类,但 Count<Integer> 不是 Count<Number> 的子类!要使 Count<Number> i = j; 生效,就必须使用上界通配符<? extends Number>

class Count<T> {  
    private T t;  
    public Count(T t) {  
        this.t = t;  
    }  
}

public class Application {
    public static void main(String[] args) {
        Count<Integer> j = new Count<>(3);
        
        // 编译通过!
        // 这下,变量 i 既能接受 Count<Number> 类型又能接受 Count<Integer> 类型了
        Count<? extends Number> i = j;
    }
}

接下来,我们将全面介绍泛型的标记符以及其通配符体系:

  • T - Type
    • 通常用于表示任何类
      // 定义
      class GenericClass<T> {
          private T sth;
          
          public GenericClass(T sth) {
              this.sth = sth;
          }
          
          public T getSth() {
              return sth;
          }
      }
      
      public class Application {
          public static void main(String[] args) {
              // 传入 String 作为特定的 T
              GenericClass<String> gg = new GenericClass<>("ego");
      
              // 不传 T,相当于:GenericClass<Object> gg = new GenericClass<>();
              GenericClass gg = new GenericClass();
          }
      }
      
  • E - Element
    • 通常用于表示集合的元素或异常
      // 定义
      class CollectionClass<E> {
          private E[] arr;
      
          public CollectionClass(E[] arr) {
              this.arr = arr;
          }
      
          public E[] getArr() {
              return arr;
          }
      }
      
      public class Application {
          public static void main(String[] args) {
              CollectionClass<Integer> nc = new CollectionClass<>(new Integer[]{1, 3, 5, 7, 9});
              CollectionClass<String> ns = new CollectionClass<>(new String[]{"1", "3", "5"});
          }
      }
      
  • K - Key
    • 通常用于表示键值对中的键
  • V - Value
    • 通常用于表示键值对中的值
      // 定义
      class MapClass<K, V> {
          private K key;
          private V val;
      
          public MapClass(K key, V val) {
              this.key = key;
              this.val = val;
          }
      
          public K getKey() {
              return key;
          }
      
          public V getVal() {
              return val;
          }
      }
      
      public class Application {
          public static void main(String[] args) {
              MapClass<Integer, String> mc = new MapClass<>(1, "ego");  
              
              Integer key = mc.getKey();
              String val = mc.getVal();
          }
      }
      
  • ? - Unknown
    • 又称无限定通配符,用于表示不确定的 Java 类型
    • 不允许调用 set(T) 方法并传入引用(null 除外)
    • 不允许调用 T get() 方法并获取 T 引用(只能获取 Object 引用)
    • ClassName<?> 是所有 ClassName<T> 的超类
      // mc 的方法既不能读,又不能写,只能做一些 null 判断工作这样子
      boolean isNull(MapClass<?, ?> mc) {
          return mc.getKey() == null || mc.getVal() == null;
      }
      
  • ? extends T
    • 上界通配符。即继承关系的上界
    • 常用于修饰方法参数
    • 以 T 使用 Number 为例,则可接受泛型类型为 Number 及其子类如 Integer
    • 允许调用读方法 T get() 获取 T 的引用,但不允许调用写方法 set(T) 传入 T 的引用(传入 null 除外)
    • 即被修饰的变量不可写
      public class Collections {
          // 该方法的 <T> 的作用是为了确定参数 <? super T> 和 <? extends T> 中的 T 到底是什么,不能省略
          public static <T> void copy(List<? super T> dest, List<? extends T> src) {
              for (int i = 0; i < src.size(); i++) {
                  // src 可以调用 getter 方法
                  T t = src.get(i);
                  // dest 可以调用 setter 方法
                  dest.add(t);
              }
          }
      }
      
  • T extends Number
    • 常用于修饰 类 或 方法返回值类型
    • Number 可替换为其他类型。当前使用的是 Number,可接受泛型类型为 Number 及其子类如 Integer
      // 要求参数的实际类型必须实现了 Comparable 接口,并且 T 必须是 T 类型本身或 T 的任何父类
      public static <T extends Comparable<T>> void max(T x, T y) {
          T max = x;
          
          if (y.compareTo(max) > 0 ){
              max = y;
          }
          
          return max;
      }
      
  • T extends ArrayList & Runnable & Serializable
    • 常用于修饰 类 或 方法返回值类型
    • 限定多个类型时用 & 隔开
    • 如果限定的类型是 class 而不是 interface,则 class 必须放在限定类表中的第一个(这里例子是 ArrayList),且最多只能存在一个class
      class Util {
          // 泛型方法,其中 T 必须是 ArrayList 的子类型,并且实现了 Runnable 和 Serializable 接口
          public static <T extends ArrayList & Runnable & Serializable> void process(T list) {
              // 因为 T 是 Runnable 的子类型,我们可以调用 run 方法
              list.run();
      
              // 因为 T 是 ArrayList 的子类型,我们可以使用 ArrayList 的方法,例如 size()
              System.out.println("List size: " + list.size());
      
              // 这里可以添加更多与 T 相关的逻辑
          }
      }
      
      // 示例类,继承自 ArrayList 并实现了 Runnable 和 Serializable 接口
      class MyArrayList<T> extends ArrayList<T> implements Runnable, Serializable {
          public void run() {
              System.out.println("Running with MyArrayList");
          }
      }
      
      // 创建 MyArrayList 实例
      MyArrayList<Integer> mal = new MyArrayList<>();
      
      // 添加一些元素
      mal.add(1);
      mal.add(2);
      
      // 调用泛型方法
      Util.process(mal);
      
  • ? super T
    • 下界通配符。即继承关系的下界
    • 常用于修饰方法参数
    • 以 T 使用 Integer 为例,则可接受泛型类型为 Integer 及其父类如 Number、Object
    • 允许调用写方法 set(T) 传入 T 的引用,但不允许调用读方法 T get() 获取 T 的引用(获取 Object 除外)
    • 即被修饰的变量不可读
      public class Collections {
          public static <T> void copy(List<? super T> dest, List<? extends T> src) {
              for (int i = 0; i < src.size(); i++) {
                  // 报错!dest 不可以调用 getter 方法
                  T t = dest.get(i);
                  // 报错!src 不可以调用 setter 方法
                  src.add(t);
              }
          }
      }
      

小结

泛型对初学者是非常不友好的。一个字,绕。所以如果您作为项目开发负责人,所带的团队不是一个特别稳定的团队的话,对泛型的使用不要过于深入。

参考

廖雪峰的官方网站