泛型总结

概述

  • 关于泛型,最根本的理解就是:泛型提供了编译时类型安全检测机制,该机制允许程序在编译时检测到非法的类型,以保证类型安全。泛型的本质是参数化类型,在面向对象编程的语言中,允许程序员在强类型校验下定义某些可变部分,以达到代码复用的目的

    • 强调的是编译期间的检查,如果在运行期间进行操作,比如使用反射是可以绕过泛型的编译检查的,当然最后可能就会引入一些系列的类型不兼容的错误

泛型优势

  • 解耦类型,比如一个类、接口或者方法要兼容多种数据类型的场景,提升代码可重用性
  • 保证类型安全,避免类型转换错误

    • 比如泛型与集合的联合使用,避免了集合充斥各种数据类型的数据最终不可避免的导致数据转换异常的情况发生
  • 提升可读性,从编码阶段就明确某个类或者方法要处理的对象类型是什么

泛型原理

  • Java 的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉(Java编译器生成的字节码是不包涵泛型信息的),这也就是通常所说类型擦除

    • 所谓的类型擦除就是将普泛型T改为Object,设置了限定通配符extends的,擦除为类型上限,设定了限定通配符super的被擦除为Object,使用?作为通配符的同样被擦除为Object
    • 伪泛型之伪在于泛型不能代表任意类型,比方说泛型不可以是基本数据类型(注意,泛型可以是数组类型),C++、C#都实现了真正的泛型,而Java只在编译层面使用泛型信息,检查完就擦除了,对于JVM来说泛型本质上就是Object类型
    • Java的伪泛型实现了最大的兼容性,相比与其他真泛型的语言来说,最直观的体验就是即便源码中定义了泛型,但是实际上泛型可用可不用,与Java5的早期版本也进行了兼容

      • 实际上如果使用IDEA编程时,如果提供了泛型但是不使用泛型的话,会有Raw use of parameterized class 'Class名' 提示,用来提示使用泛型以获得使用泛型的优势
    • 对于泛型类型擦除的误解:

      • 既然编译器将泛型擦除了,那么为什么在运行时还能通过反射获得泛型信息呢,这是因为,上边说的泛型擦除实际上是将代码中用到的泛型替换为Object,但是在特定的情况下,泛型作为函数或者类的声明的一部分,会被当做是元信息存储,并可以在反射时使用,注意此时只是获得了编译时即确定的泛型信息,但是泛型运行时的具体类型仍然无法获得(运行时并不存在泛型,已经被擦除),可以参考Java类型系统Java反射的理解

        • 这种反射对于获取泛型信息的支持可以看做是JVM对于Java泛型擦除丢失类型信息的一种弥补
    • 这么看Java的泛型就像是语法糖。这种实现方式的好处在于不必修改JVM,减少了潜在改动带来的风险
  • 因为Java引入了泛型,所以,只用Class来标识类型已经不够了。实际上,Java的类型系统结构如下

    image-20220402200110107

  • 对于泛型擦除的理解还可以参考下边的案例进行理解

    // 这是我们自己编写的泛型
    public class Pair<T> {
      private T first;
      private T last;
      public Pair(T first, T last) {
        this.first = first;
        this.last = last;
      }
      public T getFirst() {
        return first;
      }
      public T getLast() {
        return last;
      }
    }
    
    // 使用泛型的时候的代码
    Pair<String> p = new Pair<>("Hello", "world");
    String first = p.getFirst();
    String last = p.getLast();
    
    // 编译之后的代码
    Pair p = new Pair("Hello", "world");
    String first = (String) p.getFirst();
    String last = (String) p.getLast();
    
    // 对应的字节码
     0 new #2 <com/diego/genericType/Pair>
     3 dup
     4 ldc #3 <Hello>
     6 ldc #4 <world>
     8 invokespecial #5 <com/diego/genericType/Pair.<init> : (Ljava/lang/Object;Ljava/lang/Object;)V>
    11 astore_1
    12 aload_1
    13 invokevirtual #6 <com/diego/genericType/Pair.getFirst : ()Ljava/lang/Object;>
    16 checkcast #7 <java/lang/String>
    19 astore_2
    20 aload_1
    21 invokevirtual #8 <com/diego/genericType/Pair.getLast : ()Ljava/lang/Object;>
    24 checkcast #7 <java/lang/String>
    27 astore_3
    28 return
    
    
    Pair<String> pair1 = new Pair<>("li","jia");
    Pair<String> pair2 = new Pair<>("li","jia");
    // JVM只知道有Pair类型的对象,而不认识泛型
    System.out.println(pair1.getClass() == pair2.getClass()); // true
    • 从字节码中一窥泛型究竟时,可以看到泛型T全部擦除为Object类型,同时在返回类型为泛型的方法中,还有一个特殊的指令checkcast即执行类型强转,因为泛型的使用保证了返回值必定是指定类型,所以类型强制转化(先向上提升为Object,再还原回来)一定成功,如果不匹配则抛出ClassCastException运行时异常(正确使用泛型则不会出现该异常)
    • 可以总结道:泛型起作用的过程除了编译时的安全类型检查外还包括,在泛型变量与确定类型的变量做赋值的位置会做安全的强制类型转换(得益于编译过程中的类型一致性检查)
  • 把泛型直接看做是Object类也是不对的,参考下边的例子理解

    // 实例化泛型的需求
    public class Pair<T> {
      private T first;
      private T last;
      public Pair() {
        // Compile error:
        first = new T();
        last = new T();
      }
      
      // 使用泛型重写equals方法时的尴尬
      public boolean equals(T t) {
        // ....
      }
    }
    • 当我们有在类中实例化泛型的需求的时候,是不能直接使用new进行实例化的,因为如果直接这样做可行的话,编译器会将其转换为创建一个Object对象,那么在使用泛型的时候(毕竟泛型是由用户指定的任意引用类型),不能进行强转,否则会出现类型强转错误,正确的做法是依赖外部的参数

      public class Pair<T> {
        private T first;
        private T last;
        public Pair(Class<T> clazz) {
          first = clazz.newInstance();
          last = clazz.newInstance();
        }
      }
      
      // 这是模块中很常见的手段
      Pair<String> pair = new Pair<>(String.class);
    • equals方法实际上在擦拭后与父类Object的equals方法构成重写,形成了事实上的歧义,因此实际上定义这样的方法时会直接报错,应将泛型T改为Object以实现重写,或者更改方法名不进行重写的尝试

Java泛型的局限

  1. <T>不能是基本类型,例如int,因为实际类型是ObjectObject类型无法持有基本类型
  2. 无法取得带泛型的Class

    Pair<String> p1 = new Pair<>("Hello", "world");
    Pair<Integer> p2 = new Pair<>(123, 456);
    Class c1 = p1.getClass();
    Class c2 = p2.getClass();
    System.out.println(c1==c2); // true
    System.out.println(c1==Pair.class); // true
  3. 无法判断带泛型的类型

    Pair<Integer> p = new Pair<>(123, 456);
    // Compile error:
    if (p instanceof Pair<String>) {
    }
    • 并不存在Pair<String>.class,而是只有唯一的Pair.class
  4. 不能使用new实例化T类型以及T类型数组

泛型的使用

常用的泛型符号

  • 常用的通配符为: TEKV?仅仅是约定俗成的符号而已,就跟参数的名字一样

    • T (type) 表示具体的一个Java类型
    • K V (key value) 分别代表java键值中的Key Value------常用于双泛型
    • E (element) 代表Element,用于表示集合中的元素
    • 表示不确定的 Java 类型,或者说是任意的Java类型
  • 再次强调,泛型本身是Java数据类型的代号,而不是数据类型本身,比如下边的例子

    public class GenericTypeTest<T> {
    
        <String, T, Alibaba> String get(String string, Alibaba alibaba) {
    
            return string;
        }
    
        public static void main(String[] args) {
    
            GenericTypeTest<Integer> test = new GenericTypeTest<>();
            Integer s = test.get(123, 123);
        }
    
    }
    • String与Alibaba都是具体类型的一个代表符号,而这个具体类型是什么由函数调用方传入的参数的实际类型定义

泛型通配

  • 所谓通配即是指定泛型可以代表一系列的类型,以提升泛型的使用效率,但是如果放开接收任意类型的话,则失去了泛型的类型限制作用,因此引入了以下两种可接收多种受泛型约束的类型的泛型形式:

    • < ? extends T > 确保泛型必须是 T 的子类来设定泛型的上界,适用于消费集合元素的场景,进一步讲就是适合只读数据场景,比如计算集合中所有元素的和

      int sumOfList(List<? extends Integer> list) {
        int sum = 0;
        for (int i=0; i<list.size(); i++) {
          Integer n = list.get(i);
          sum = sum + n;
        }
        return sum;
      }
    • < ? super T >确保类型必须是 T 的父类来设定泛型的下界,适用于添加集合元素的场景,进一步讲就是适合写数据场景,比如一个简单的set方法

      void set(Pair<? super Integer> p, Integer first, Integer last) {
          p.setFirst(first);
          p.setLast(last);
      }
    • 最完美的展示extends通配符和super通配符使用的例子就是Java标准库的Collections类定义的copy()方法:

      public class Collections {
          // 把src的每个元素复制到dest中:
          public static <T> void copy(List<? super T> dest, List<? extends T> src) {
              for (int i=0; i<src.size(); i++) {
                  T t = src.get(i);
                  dest.add(t);
              }
          }
      }
  • 泛型通配的应用实际上可以分为代表泛型类型匹配与代表泛型集合的特性两部分
extends
  • 可以向<? extends T>约束的集合引用赋值任何 T 及 T 子类的集合

    • 注意反过来是不允许的
  • 允许从集合中数据,但是数据都会被强制转为类型T限制写数据,只允许写入null。以下边的代码为例:

    Pair<? extends Number, ? extends Number> pair = new Pair<>(1,2);
    // Integer key = pair.getKey(); // 编译时类型转换报错,实际上是Integer key = (Number)pair.getKey();
    • 类型强转的限制是有道理的,假如集合中存储有Double类型的数据话,可以使用Integer去承接吗,显然不合适,使用Number类型的引用承接更合理,也可以理解为集合中的子类数据取出时类型被擦除了
    • 同理,Pair<? extends Number>持有的类型都是Number或者Number的子类,但是又不能确定究竟是哪个类型因此只能传入人畜无害的null
super
  • 可以向<? super T>约束的集合引用赋值任何 T 及 T 的父类集合

    • 注意反过来是不允许的
  • 允许向集合中数据,但是数据类型只能是T或者是T类型的子类类型,可以读数据,但是类型全部丢失,只能返回Object类型的数据。以下边的代码为例:

    Pair<? super Cat> pair = new Pair<>();
    pair.setFirst(new Cat()); // 编译通过
    pair.setFirst(new Garfield()); // 编译通过
    // pair.setFirst(1); // 编译错误
    Object first = pair.getFirst(); // 编译通过
    • 写数据时,只能写入类型T或者其子类,这样可以保持集合内的类型兼容,如果可以写T的某个父类类型或者其他任意类型,就很难保持类型兼容
    • 因为可以向受约束的引用赋值T以及T的任意父类的集合,所以取数据时直接丢掉类型以保持类型兼容,当然也可以理解为类型信息直接被擦除掉了
PECS原则
  • 对于什么时候用extends,什么时候用super的问题,《Effective Java》书中给出了一个结论即PECS原则producer-extends, consumer-super

    • 要从泛型类取数据时,用extends
    • 要往泛型类写数据时,用super
    • 既要取又要写,就不用通配符(即extends与super都不用)
多重限定
  • 类型参数可以使用多重限定,即使用&来表示,指定泛型T必须是A和B这两个接口的共同实现类

    public class Multilimit implements MultiInterfaceA ,MultiInterfaceB {
      public static <T extends MultiInterfaceA & MultiInterfaceB> void test (T t) {
    
      }
    }
    • 与集合中的泛型应用做区分

使用案例

普通泛型的使用
  • 以下边的代码为例,理解一般的泛型的使用

    public class ListNoGeneric {
    
        public static void GenericTest() {
            // 1. 集合类中不使用泛型
            List a1 = new ArrayList();
            a1.add(new Object());
            a1.add(new Integer(111));
            a1.add(new String("hello a1a1"));
    
            // 2. 把a1引用赋值给a2,注意a2的类型中引入了泛型<Object>
            List<Object> a2 = a1;
    
            a2.add(new Object());
            a2.add(new Integer(222));
            a2.add(new String("hello a2a2"));
    
            // 3. 把a1引用赋值给a3,注意a3的类型中引入了泛型<Integer>
    
            List<Integer> a3 = a1;
            a3.add(new Integer(333));
            // 下边两行代码编译错误
            //a3.add(new Object());
            //a3.add(new String("hello a3a3"));
    
            // 4. 把a1引用赋值给a4,注意区别在于a4的类型引入了通配符
            List<?> a4 = a1;
            a4 = a3;
    
            a4.remove(0);
            a4.clear();
    
            // 编译出错,不允许添加任何元素
            //a4.add(new Object());
            // 允许添加null
            a4.add(null);
            // 读数据时,丢失类型信息,直接擦除为Object
            Object o = a4.get(0);
        }
    }
    • 第一段代码演示了泛型推出之前的集合的使用,各种类型可以一股脑的放到集合中,但是取出使用时则要小心的校验类型,否则会出现类型转换错误
    • 第二段引入了Object作为泛型类型,此时仍可以添加各种类型的数据,因为都是兼容Object的类型

      • 同时也展示了Java中泛型的向前兼容,即非泛型集合可以赋值给任何泛型限制的集合,尽管Java泛型保持了对历史代码的兼容,但是这种兼容很容易引入BUG,参考下边的代码,未来还是要尽量使用泛型定义

        /**
         * 演示泛型的类型兼容引入的BUG
         */
        public class TypeCompatibility {
        
            public static void main(String[] args) {
        
                JSONObject jsonObject = JSONObject.parseObject("{\"level\":[\"3\"]}");
                List<Integer> intList = new ArrayList<>(10);
        
                if (jsonObject != null) {
                    intList.addAll(jsonObject.getJSONArray("level"));
                    int amount = 0;
                    for (Integer i : intList) {
                        // ClassCastException: String cannot be cast to Integer
                        amount = amount + i;
                    }
                }
            }
        }
        • adAll方法的方法声明是:boolean addAll(Collection<? extends E> c);,而JSONArray类型的定义是public final class JSONArray extends AbstractJSON implements JSON, List,由于泛型兼容,所以添加成功,但是实际添加的不是Integer类型的数据而是String类型,最终导致出现强转异常

          • 实际上在fastjson(1.2.79)中JSONArray的类型定义已经更改为public class JSONArray extends JSON implements List<Object>, Cloneable, RandomAccess, Serializable,此时类型检查起作用,addAll处直接提示类型不兼容,这里体现了ListList<Object>的不同
    • 第三段代码也展示了Java泛型的兼容性,如果将不带泛型的a1改为List<Object>,则第三段代码的赋值就会编译出错,反过来同样会编译错误;与之对比的是,如果数组这样赋值则不会出错,因为数组是协变的(所谓协变的概念可参考Java类型系统

      List<Object> b = new ArrayList<>();
      // List<Integer> c = b; // java.util.List<java.lang.Object>无法转换为java.util.List<java.lang.Integer>
      List<Integer> c = new ArrayList<>();
      // List<Object> d = c; // java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>
      
      Integer[] integers = new Integer[]{1,2,3};
      Object[] objects = integers; // 可转换
      
      Object[] objs = new Object[3];
      // Integer[] ins = objs; // java.lang.Object[]无法转换为java.lang.Integer[]
      • 确定的泛型类型(即未使用通配符的泛型类型)是没有协变性的
    • 第四段代码说明,当使用?作为泛型通配符时,表示该泛型类型的引用可以指向任何泛型类型的集合,但是不能添加任何元素,读出的数据也会丢失类型信息,但是可以执行remove或者clear,比如如果在上述代码中使用a4 = a3也会正常编译通过。但是不要误以为使用?做通配符时可以允许任意类型的数据添加到集合中,如果这样的话,泛型实际上失去了类型保护的意义,使用?做泛型的集合一般作为参数接收外部的集合或者返回一个不明确元素类型的集合。这种性质在自定义的使用泛型的类中是一样的:

      public class Pair<T> {
      
          private T first;
          private T last;
      
          public Pair() {}
      
          public Pair(T first, T last) {
      
              this.first = first;
              this.last = last;
          }
      
          public T getFirst() {
      
              return first;
          }
      
          public T getLast() {
      
              return last;
          }
      
          public void setFirst(T first) {
              this.first = first;
          }
      
          public void setLast(T last) {
              this.last = last;
          }
      
          public static void main(String[] args) {
              Pair<?> pair = new Pair<>();
               //pair.setFirst(1); // 添加失败
              // pair.setLast(2); // 添加失败
      
              pair = new Pair<Integer>(); // 编译通过
              Pair<?> pair1 = new Pair<>(1,2); // 编译通过
          }
      
      }
      • 大多数情况下,可以引入泛型参数<T>消除<?>通配符
      • 实际上<?>等价于<? extends Object>
泛型限定符的使用
  • 以下边的代码为例,理解泛型限定符的使用

    public static void GenericWithLimitTest() {
            // 首先声明三个依次继承的类
            List<Animal> animals = new ArrayList<>();
            List<Cat> cats = new ArrayList<>();
            List<Garfield> garfields = new ArrayList<>();
    
            animals.add(new Animal());
            cats.add(new Cat());
            garfields.add(new Garfield());
    
            // 测试赋值操作
            // 下行编译出错,List<? extends Cat>类型的集合只接受Cat或者其子类的集合
            List<? extends Cat> extendsCat = animals;
            List<? super Cat> superCat = animals;
    
            extendsCat = cats;
            superCat = cats;
    
            extendsCat = garfields;
            // 下行编译出错,List<? super Cat>类型的集合只接受Cat或者其父类的集合
            superCat = garfields;
    
    
            // add方法测试
            // 泛型中使用extends关键字的集合,只允许写null
            extendsCat.add(new Animal());
            extendsCat.add(new Cat());
            extendsCat.add(new Garfield());
            extendsCat.add(null);
    
            // 泛型中使用super关键字的集合,只允许写Cat类型数据或者其子类
            superCat.add(new Animal());
            superCat.add(new Cat());
            superCat.add(new Garfield());
    
    
            // get方法测试,使用extends关键字时,读出的数据会被转型为Cat类型
            Cat cat = extendsCat.get(0);
            Object cat1 = extendsCat.get(0);
            // 下行编译出错,Cat类型的值向其子类引用赋值会出现类型强转失败
            Garfield cat2 = extendsCat.get(0);
    
            // 使用super关键字时,可以读数据,但是数据类型丢失
            Object object = superCat.get(0);
            // Object类型的对象不能被赋值到其子类的引用
            Cat cat3 = superCat.get(0);
    
        }
反射API对泛型的支持
  • Java的部分反射API支持泛型

    // compile warning:
    Class clazz = String.class;
    // 必须强转,否则会编译失败
    String str = (String) clazz.newInstance();
    
    // no warning:
    Class<String> clazz = String.class;
    String str = clazz.newInstance();
    • 调用ClassgetSuperclass()方法返回的Class类型是Class<? super T>

      Class<? super String> sup = String.class.getSuperclass();
    • 构造方法Constructor也支持泛型:

      Class<Integer> clazz = Integer.class;
      Constructor<Integer> cons = clazz.getConstructor(int.class);
      Integer i = cons.newInstance(123);
带泛型数组
  • 可以声明带泛型的数组,但不能用new操作符创建带泛型的数组:

    Pair<String>[] ps = null; // ok
    Pair<String>[] ps = new Pair<String>[2]; // compile error!
  • 必须通过强制转型实现带泛型的数组

    Pair<String>[] ps = (Pair<String>[]) new Pair[2];
    • 因为需要强制转换,所以我们不得不首先创建一个普通的数组,如果不是直接使用new来创建这个普通的数组,而是使用了一个变量来引用这个普通数组的话,可能会导致错误,这是因为编译器会检查带泛型的数组,但是不会检查普通的数组,但是通过强制转型之后,两个数组引用实际上指向同一个数组,不被检查的普通数组可能会引入不符合泛型的成员,从而导致错误

      Pair[] arr = new Pair[2];
      Pair<String>[] ps = (Pair<String>[]) arr;
      
      ps[0] = new Pair<String>("a", "b");
      arr[1] = new Pair<Integer>(1, 2);
      
      // ClassCastException:
      Pair<String> p = ps[1];
      String s = p.getFirst();
  • 带泛型的数组在泛型擦除后实际上就是普通的数组类型

    Pair[] arr = new Pair[2];
    Pair<String>[] ps = (Pair<String>[]) arr;
    System.out.println(ps.getClass() == Pair[].class); // true
  • 在支持泛型的方法中创建泛型数组时不能直接new T[],因为这样实际上创建的是Object[]类型的数组,正确的创建方法与创建泛型实例的方法一致:引入Class实例

    public T[] create(Class<T> tClass) {
      return (T[])Array.newInstance(tClass, 5);
    }
泛型可变参数
  • 除了使用newInstance方法之外还可以使用泛型可变参数创建泛型数组

    public class ArrayHelper {
        @SafeVarargs
        static <T> T[] asArray(T... objs) {
            return objs;
        }
    }
    
    String[] ss = ArrayHelper.asArray("a", "b", "c");
    Integer[] ns = ArrayHelper.asArray(1, 2, 3);
  • 使用泛型可变参数存在一定的风险

    public class ArrayHelper {
    
        static <K> K[] pickTwo(K k1, K k2, K k3) {
            return asArray(k1, k2);
        }
    
        static <T> T[] asArray(T... objs) {
            return objs;
        }
    
        public static void main(String[] args) {
            String[] arr = asArray("one", "two", "three");
            System.out.println(Arrays.toString(arr));
            // ClassCastException:
            String[] firstTwo = pickTwo("one", "two", "three");
            System.out.println(Arrays.toString(firstTwo));
        }
    }
    • 直接调用asArray(T...)似乎没有问题,但是在另一个方法中,我们返回一个泛型数组就会产生ClassCastException,原因还是因为类型擦除pickTwo()方法内部,编译器无法检测K[]的正确类型,因此返回了Object[]
    • 实际上IDEA会有警告,除非确认完全没有问题,才可以用@SafeVarargs消除警告

      image-20220402202621518

  • 考虑到上述出现的关于泛型数组的问题,如果在方法内部创建了泛型数组,最好不要将它返回给外部使用
类型转换
  • 要明白一点:泛型只是提供一种编译时的类型检查机制,它并不是类、接口、方法的一部分,除此之外,确定的泛型类型(即未使用通配符的泛型类型)是没有协变性的

    // 创建ArrayList<Integer>类型:
    ArrayList<Integer> integerList = new ArrayList<Integer>();
    // 添加一个Integer:
    integerList.add(new Integer(123));
    // “向上转型”为ArrayList<Number>:赋值失败
    ArrayList<Number> numberList = integerList;

泛型的定义

Class

ArrayList为例

// 类名后面要加一个<T>,其作用就是先声明这个泛型的符号(本质上就是个类型参数),此后类中就可以使用此泛型符号作为一般的类型标志
public class ArrayList<T> {
  private T[] array;
  private int size;
  public void add(T e) {...}
  public void remove(int index) {...}
  public T get(int index) {...}
}
  • 使用Java类中的泛型的时候注意

    • 静态方法中不能使用类上定义的泛型,类上定义的泛型似乎与实例对象this有关系,所以不能在静态语境中使用,如果非要使用,可以使用定义在方法上的泛型(与类或者方法本身无关,只与泛型定义的位置有关),下边看实例代码

      public class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last) {
          this.first = first;
          this.last = last;
        }
        public T getFirst() { ... }
        public T getLast() { ... }
      
        // 对静态方法使用<T>: 编译不通过 this cannot be referenced from a static contex
        public static Pair<T> create(T first, T last) {
          return new Pair<T>(first, last);
        }
      }
      
      
      class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last) {
          this.first = first;
          this.last = last;
        }
        public T getFirst() {
          return (T)first;
        }
        public T getLast() {
          return (T)last;
        }
      
        // 对静态方法使用<K>:对于普通的函数也是适用的
        public static<K> Pair<K> create(K first, K last) {
          return new Pair<K>(first, last);
        }
      }
      
      
      // 修改为使用方法上声明的泛型
      public class Demo {
        public static void main(String[] args) {
          Pair<String> pair = Pair.create("li","jia");
          Pair<Integer> pair1 = pair.create(1,2);
          System.out.println(pair.getFirst());
        }
      }
    • 一个不带泛型的类可以继承自带泛型的类

      class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last) {
          this.first = first;
          this.last = last;
        }
        public T getFirst() {
          return (T)first;
        }
        public T getLast() {
          return (T)last;
        }
        public <K> Pair<K> create(K first, K last) {
          return new Pair<K>(first, last);
        }
      }
      class IntPair extends  Pair<Integer> {
      
        public IntPair(Integer first, Integer last) {
          super(first, last);
        }
      }
      • 对于一般的泛型类型的实例,无法通过其Class实例获取泛型,但是对于子类继承带泛型父类的场景或者是方法中使用了泛型等场景,均可以通过反射获取对应的泛型信息,以上边的IntPair类为例

        Class<IntPair> clazz = IntPair.class;
        Type t = clazz.getGenericSuperclass();
        if (t instanceof ParameterizedType) {
          ParameterizedType pt = (ParameterizedType) t;
          Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
          Type firstType = types[0]; // 取第一个泛型类型
          Class<?> typeClass = (Class<?>) firstType;
          System.out.println(typeClass); // Integer
        }

Interface

  • 一个典型的案例就是Comparable接口

    interface Comparable<T> {
      int compareTo(T o);
    }
  • 对于带泛型的类、接口的继承与实现可以有两种形式,父类或者是接口可以是带通配符泛型的,也可以是具体类型的

    class IntPair<T> extends Pair<T>{
        // ...
    }
    
    class IntPair extends Pair<Integer>{
        // ...
    }
    
    class Person implements Comparable<Person> {
        String name;
        int score;
        Person(String name, int score) {
            this.name = name;
            this.score = score;
        }
        public int compareTo(Person other) {
            return this.name.compareTo(other.name);
        }
        public String toString() {
            return this.name + "," + this.score;
        }
    }
    • 实现带泛型接口的类必须实现正确的泛型类型

Function

  • 定义在函数上的泛型优先被被所在函数使用

    public static <E> void printArray( E[] inputArray )
    {         
      for ( E element : inputArray ){        
        System.out.printf( "%s ", element );
      }
      System.out.println();
    }
    • 泛型声明的位置在函数修饰符后,返回类型前

多泛型的使用

  • 以上三种位置的泛型都可以使用多泛型,典型的例子就是Map的定义

    public class Pair<T, K> {
      private T first;
      private K last;
      public Pair(T first, K last) {
        this.first = first;
        this.last = last;
      }
      public T getFirst() { ... }
      public K getLast() { ... }
    }
    
    Pair<String, Integer> p = new Pair<>("test", 123);

嵌套泛型的使用

  • 所谓嵌套泛型的含义就是泛型代表的类型也使用了泛型

    public class Subset {
    
        public List<List<Integer>> subsets(int[] nums) {
            // 对于包含泛型的类型,保证内层泛型一致即可
            LinkedList<List<Integer>> res = new LinkedList<>();
            LinkedList<Integer> track = new LinkedList<>();
            backtrack(0, nums, res, track);
            return res;
        }
    
        private void backtrack(int i, int[] nums, LinkedList<List<Integer>> res, LinkedList<Integer> track) {
            res.push(new ArrayList<>(track));
            for (int j = i; j < nums.length; j++) {
                track.push(nums[j]);
                backtrack(j + 1, nums, res, track);
                track.pop();
            }
        }
    }
    • 注意res的类型,只有外部的实际类型可以考虑转型,即LinkedList转到List,内部的泛型保持完全的一致(必须是完全的一致,泛型不存在什么转型)即可
    • 对于LinkedList<List<Integer>>内部的泛型在res.push起作用,ArrayList可以升级为List

      • res.push(new ArrayList<>(track))这一句中,会检查track的泛型类型,如果使用的是ArrayList的空参数构造函数也是允许的,如果track泛型的类型不一致,IDE会提示无法猜测ArrayList的泛型类型

参考


demoli
16 声望3 粉丝

bug创建者


« 上一篇
Go异常处理