Java语言基础(SE)-第六节 泛型(Generics)

泛型简介

  1. 从 Java 5 开始,增加了泛型技术
  2. 什么是泛型?
    1. 将类型变为参数,提高代码复用率
  3. 建议的类型参数名称

     T :Type 
     E :Element 
     K :Key 
     N :Number 
     V :Value 
     S、U、V :2nd, 3rd, 4th types
    

泛型类型(Generic Type)

  1. 什么是泛型类型?
    1. 使用了泛型的类或者接口
    2. 比如java.util.Comparatorjava.util.Comparable
  2. 举例使用
    1. 单个类型参数

       public class Student<T> {
           private T score;
           public T getScore() {
               return score;
           }
           public void setScore(T score) {
               this.score = score;
           }
       }
       //main函数
       Student<String> stu = new Student<>();
       stu.setScore("A");
       String score1 = stu.getScore();
       Student<Double> stu2 = new Student<>();
       stu2.setScore(98.5);
       double score2 = stu2.getScore();
      
    2. 多个类型参数

       public class Student<N,S> {
           private N no;
           private S score;
           public Student(N no, S score) {
               this.no = no;
               this.score = score;
           }
       }
              
       //main函数
       Student<String, String> s1 = new Student<>("E925", "A+");
       Student<Integer, Double> s2 = new Student<>(18, 96.5);
      

泛型类型的继承

  1. JDK中的继承

     public interface Iterable<T>
     public interface Collection<E> extends Iterable<E>
     public interface List<E> extends Collection<E>
     public class ArrayList<E> implements List<E>
    
  2. 示例

     Iterable<String> it = null;
     Collection<String> col = null;
     List<String> li = null;
     ArrayList<String> al = null;
        	
     //多态,父类指向子类
     it = col;
     col = li;
     li = al;
        
     //二者不存在继承关系
     List<Object> list = null;
     ArrayList<String> all = null;
     //错误
     //list = all;
    
  3. 示例2:

     package com.zh;
     import java.util.List;
     public interface MyList<E,T> extends List<E> {
         void setNo(T no);
     }
        
     //main函数
     List<String> li = null;
     MyList<String, Integer> ml1 = null;
     MyList<String, Integer> ml2 = null;
     MyList<String, Integer> ml3 = null;
     li = ml1;
     li = ml2;
     li = ml3;
    

原始类型(Raw Type)

  1. 什么是原始类型?
    1. 没有传递具体的类型给泛型的类型参数 创建的对象。
     //Box称为是B<E>的原始类型(Raw Type)
     Box rawBox = new Box();//warning :rawtypes
     Box<String> strBox = new Box<>();
     rawBox = strBox;//ok
     strBox = rawBox;//warning :unchecked
    
  2. 当使用了原始类型时,编译器会给出 警告(可以用@SuppressWarnings消除)
  3. 将非原始类型赋值给原始类型时,编译器没有任何警告和错误
  4. 将原始类型赋值给非原始类型时,编译器会给出 警告(可以用@SuppressWarnings消除)
  5. Box 是原始类型,Box<Object> 是非原始类型

泛型方法(Generic Method)

泛型方法定义

  1. 使用了泛型的方法(实例方法、静态方法、构造方法),比如 Arrays.sort(T[], Comparator<T>)
  2. 方法中的泛型参数不是来自泛型类型
  3. 泛型方法的格式:

     //发型变量声明  方法返回值类型   方法名称(变量类型列表)
     <泛型变量名称> 方法返回值类型(也可以是泛型变量) 方法名称(参数类型/泛型变量 参数名){}
    
  4. 一个类可以不是泛型,但是这个类的某些方法可以支持泛型

     //Main类
     public static void main(String[] args) {
         Student<String, String> s1 = new Student<>();
         //set方法是Main这个类的类方法,所以可以Main.<String,String>set()调用
         //完整写写法
         Main.<String,String>set(s1,"k99","C++");
         Student<Integer, Double> s2 = new Student<>();
         //编译器可以自动推断出类型参数的具体类型
         set(s2,25,99.5);
     }
     //泛型方法,将参数stu的类型的泛型不要写死,也弄成泛型T1,T2
     //注意:泛型方法中使用的泛型参数要在方法的返回值类型签名写上,相当于声明
     static <T1,T2> void set(Student<T1,T2> stu,T1 no,T2 score) {
         stu.setNo(no);
         stu.setScore(score);
     }
    
  5. 示例

     public class Box<E> {
         private E element;
         public Box() {}
         public Box(E element) {
             this.element = element;
         }
     }
        
     public static void main(String[] args) {
         List<Box<Integer>> boxes = new ArrayList<>();
         addBox(11, boxes);
         addBox(22, boxes);
         addBox(33, boxes);
     }
     public static <T>  void addBox(T element,List<Box<T>> boxes) {
         Box<T> box = new Box<>(element);
         boxes.add(box);
     }
    

构造方法

  1. 举例

     public class Person<T> {
         private T age;
         //E代表泛型方法的泛型参数,T来自泛型类
         public <E> Person(E name,T age){
         }
     }
     //main函数
     Person<Integer>  p1 = new Person<>("jack", 20);
     Person<Double>  p2 = new Person<>(666, 20.6);
     Person<String>  p3 = new Person<>(12.34, "80后");
    
  2. 注意: 泛型类型带的泛型变量,只能用在构造方法或者实例方法中,不能用在类方法中(泛型类的类型参数使前提是有实例)

     public class Box<E> {
         private E element;
         public Box() {}
         public Box(E element) {
             this.element = element;
         }
         //错误,不能用在类方法中
         //public static void print(E element){}
         //泛型方法,这个可以
         public static <T> void print(T element){}
     }
    

类型推断

```
//类不是泛型类
public class Collections {
    @SuppressWarnings("unchecked")
    //包含一个泛型方法:<T> 泛型变量声明,List<T>返回值类型
    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }
}
//根据赋值的变量类型,编译器推断出emptyList的return类型
List<String> list1 = Collections.emptyList();
List<Integer> list2 = Collections.emptyList();
```

限制类型参数

  1. 可以通过 extends 对类型参数增加一些限制条件,比如
    1. extends 后面可以跟上类名、接口名,代表 T 必须是 A 类型,或者继承、实现A
  2. 可以同时添加多个限制,比如 <T extends A & B & C>,代表 T 必须同时满足 A、B、C
    1. 注意A、B、C,只能最多有一个是类,因为Java不支持多继承。
    2. 如果有一个是类,那个类必须放在最前面

泛型类使用extends限制

  1. 举例

     //<T extends Number>:T变量限制必须是Number或者其子类
     public class Person<T extends Number> {
         private T age;
         public Person(T age) {
             this.age = age;
         }
         public int getAge() {
             return (age ==null) ? 0 : age.intValue();
         }
     }
        
     Person<Double>  p1 = new Person<>(18.7);
     System.out.println(p1.getAge()); //18
     Person<Integer> p2;//ok
     //String不是Number的子类型
     Person<String> p3;//error
    

泛型方法使用extends限制

  1. 泛型方法要在泛型参数声明处使用extends进行限制。
  2. 示例

     //获取数组中的最大值
     //<T extends Comparable<T>> 泛型变量声明,同时限制表示T变量类型必须是继承或实现Comparable<T>接口的接口或类
     //实现Comparable接口的类能够使用compareTo方法进行比较
     static <T extends Comparable<T>> T getMax(T[] array) {
         if (array == null || array.length == 0) return null;
         T max = array[0];
         for (int i = 0; i < array.length; i++) {
             if (array[i] == null) continue;
             //这句话会报错,array[i]是T类型,max也是T类型,不支持比较
             //if(array[i] <= max) continue;
             //实现Comparable接口的对象可以使用compareTo比较
             if(array[i].compareTo(max)<= 0) continue;
             max = array[i];
         }
         return max;
     }
     //main函数
     //Double、Integer默认实现了Comparable接口
     //Double声明:public final class java.lang.Double extends java.lang.Number implements java.lang.Comparable {...}
     Double[] ds = {5.6,3.4,8.8,4.6};
     System.out.println(getMax(ds));//8.8
     Integer[] is = {4,19,3,28,56};
     System.out.println(getMax(is));//56
    

综合举例

  1. 上面的Double、Integer是默认实现了Comparable接口,才可以使用getMax,那么我们是否可以自定义一个类型,也能使用getMax方法呢?
  2. 那个么这个自定义的类型,需要实现Comparable接口
//Comparable<T>比较的类型是Student<T>,所以是Comparable<Student<T>>
public class Student<T extends Comparable<T>> implements Comparable<Student<T>> {
    //如果score也具备可比较性,因此T也要实现Comparable,所以T extends Comparable<T>
    private T score;
    public Student(T score) {
        this.score = score;
    }
    @Override
    public int compareTo(Student<T> s) {
        if(s == null) return 1;
        if (score != null && s.score != null) return score.compareTo(s.score);
        if (score == null && s.score ==null) return 0;
        return s.score == null ? 1 : -1;
    }
    @Override
    public String toString() {
        return "[score=" + score + "]";
    }
}
    
//main函数
Student<Integer>[] stus = new Student[3];
stus[0] = new Student<Integer>(18);
stus[1] = new Student<Integer>(18);
stus[2] = new Student<Integer>(18);
//[score = 38]
System.out.println(getMax(stus));

心得总结

  1. 泛型T使用了extends之后,会让代码更加难以读懂
  2. 无论是泛型类还是泛型方法中使用extends限制泛型参数T,可以把T extends A就看成是T,先不考虑限制来读取代码。然后真正使用这个T参数时再考虑T需要满足什么限制。

通配符(Wildcards)

  1. 在泛型中,问号(?)被称为是通配符
  2. 通常用作变量类型、返回值类型的类型参数
  3. 一般是用于定义一个引用变量,以便实现“多态”调用(非真正意义上的多态)。
    1. 定义为引用变量,可指向不同类型的变量

       //使用?
       SuperClass<?> sup = new SuperClass<String>("lisi");
       sup = new SuperClass<People>(new People ()) ;
       sup = new SuperClass<Animal>(new Animal()) ;
              
       //使用具体类型
       SuperClass<String> sup1new SuperClass<String>(ulisin) :
       SuperClass<People> sup2new SuperClass<People>;
       SuperClass<Animal> sup3 = new SuperClass<Animal>:
      
    2. 或者方法形参用于接收不同参数,实际也是一个引用变量,如下:

       List<String> l1 = new ArrayList<String>();
       List<Integer> l2 = new ArrayList<Integer>();
       l1.add("AAA");
       l2.add(222);
        public static void test1(List<?> l1){
            String join = Joiner.on(",").join(l1);
            System.out.println(join);
       }
      
  4. 不能用作泛型方法调用、泛型类型实例化、泛型类型定义的类型参数

     //泛型类型实例化,不能出现
     //Box<Integer> p1 = new Box<?>();
     //泛型方法调用,不能出现
     //Main2.<?>set(s1,"k99","C++");
     //类型定义,不能出现
     //public class Person<? extends Number> {}
    

上界

  1. 可以通过 extends 设置类型参数的上界

     //类型参数必须是Number类型或者是Number的子类型
     static void testUpper(Box<? extends Number> box) {}
     Box<Integer> p1 = null;
     Box<Number> p2 = null;
     //可以不指定类型,用?代替
     Box<? extends Number> p3 = null;
     //Integer是Number子类型,所以可以
     Box<? extends Integer> p4 = null;
     testUpper(p1);
     testUpper(p2);
     testUpper(p3);
     testUpper(p4);
    
  2. 示例代码

     static double sum(List<? extends Number> list) {
         double s = 0.0;
         for (Number n : list) {
             s += n.doubleValue();
         }
         return s;
     }
     //Arrays.asList创建一个List集合
     List<Integer> is = Arrays.asList(1,2,3);
     //6.0
     System.out.println(sum(is));
        	
     List<Double> ds = Arrays.asList(1.2,2.3,3.5);
     //7.0
     System.out.println(sum(ds));
    

下界

  1. 可以通过 super 设置类型参数的下界

     //类型参数必须是Integer类型或者是Integer的父类型
     static void testLower(Box<? super Integer> box) {}
     Box<Integer> p1 = null;
     Box<Number> p2 = null;
     Box<? super Integer> p3 = null;
     //Number是Integer的父类型
     Box<? super Number> p4 = null;
     testLower(p1);
     testLower(p2);
     testLower(p3);
     testLower(p4);
    
  2. 示例

     static void addNumbers(List<? super Integer> list) {
         for (int i = 1; i <= 10; i++) {
             list.add(i);
         }
     }
     List<Integer> is = new ArrayList<>();
     addNumbers(is);
     //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
     System.out.println(is);
     //Number是Integer的父类
     List<Number> ns = new ArrayList<>();
     addNumbers(ns);
     //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
     System.out.println(ns);
    

无限制

  1. 举例:

     //类型参数是什么类型都可以
     public static void test(Box<?> box) {};
     //main函数
     //什么类型都可以
     Box<Integer> p1 = null;
     Box<String> p2 = null;
     Box<Object> p3 = null;
     Box<? extends Number> p4 = null;
     Box<? super String> p5 = null;
     Box<?> p6 = null;
     test(p1);
     test(p2);
     test(p3);
     test(p4);
     test(p5);
     test(p6);
    
  2. 示例

     static void printList(List<?> list) {
         for (Object obj : list) {
             System.out.print(obj + " ");
         }
         System.out.println();
     }
     List<Integer> is = Arrays.asList(1,2,3);
     printList(is);//1 2 3 
     List<Double> ds = Arrays.asList(1.2,2.3,3.5);
     printList(ds);//1.2 2.3 3.5 
    

无限制通配符和有限制通配符的数据读写问题

1. 无限制通配符读写

  1. 读:可以读,尽管被无限制通配符修饰,但不管如何,容器里的元素永远是一个对象,也就是一个Object,所以可以用Object o这个引用变量来读取容器元素,即利用对象多态性。
  2. 写:不可以往容器里写入元素,因为写入的元素与容器的具体类型的关系不明确。比如:可以定义成接收变量,但无法向里面添另数据。

     public static void main(strings[] args) {
         ArrayList<?> list = new ArrayList<String>();
         list = new ArrayList<Integer>();
         list.add(1);
     }
    
    1. list可以指向不同的具体容器,相对应的能接收的元素类型也跟着改变。因此往容器里添加数据时,会因为无法确定所能添加的具体元素类型为何,导致的类型不安全而编译不通过。
    2. 注意: 可以添加null,因为null是所有类型的成员。

      2. 有限制通配符读写

  3. ? extends A:
    1. 读:往容器中添加的元素类型,一定是A或A的子类(如果是接口,则为实现类),因此读取出来的数据,一定可以用A类型来做引用(当然Object也可以)。
    2. 写:由于容器的具体类型未知,如果往容器添加元素,无法确保添加进去的具体数据是该容器具体类型的子类还是父类,因此存在类型不安全问题,所以是不允许往里添加数据的。null可以添加。

       static void swapFist(List<? extends Number> l1,List<? extends Number> l2) {
           Number temp = l1.get(0); //ok
           l1.set(0, l2.get(0));//error
           l1.set(0, temp);//error
       }
      
  4. ? super A:
    1. 读:不管容器里添加的元素是A还是A的父类的实例对象,它们始终都是Object对象,因此可以读取,用Object类型来做引用(不能用A类型来做引用)。
    2. 写:只能写入A类型及其子类的实例对象。因为具体容器的类型最低等级是到A,所以不管容器具体类型为何,它都能保证>= A,所以A的实例化对象可以被写入(对象多态性),当然,既然作为父类的A可以被写入,那么A的子类自然而然也可以被写入容器里了。null可以添加。

通配之间的继承关系

图1

T与?区别

  1. T代表一种类型,?是通配符,泛指所有类型
  2. 在编译成字节码calss文件时,T会进行替换成实际类型,?不会替换类型,可以认为?不属于泛型,跟泛型没有关系
    1. 泛型(T)本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。
    2. Java 编译器通过如下方式实现擦除:用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;在恰当的位置插入强制转换代码来确保类型安全;在继承了泛型类或接口的类中插入桥接方法来保留多态性。
  3. 泛型定义方法时需要在方法前声明泛型参数,通配符不需要,而且仅仅用来用作方法的参数类型。

     static void foo1(List<?> list) {}
     //声明泛型参数<T>
     static <T> void fooHelper(List<T> l) {}
    
  4. 使用范围不同:
    1. ? 通配符用作 参数类型、字段类型、局部变量类型,有时作为返回类型
    2. T 用作 声明类的类型参数、通用方法的类型参数
  5. 举例
    1. 例1;

       //编译器在解析 `List<E>.set(int index, E element) `时,无法确定E的真实类型,所以报错
       static void foo(List<?> list) {
           Object obj = list.get(0);//ok
           //list.set(0,obj);//error
           //list.set(0,list.get(0)); //error
       }
      
    2. 例2:解决上述问题

       static void foo1(List<?> list) {
           fooHelper(list);//ok
       }
       //使用泛型T,编译器编译的时候会进行全局替换,T直接被替换成Object
      static <T> void fooHelper(List<T> l) {
           //l.get(0)直接编译成Object类型实例,即一个指针
           l.set(0,l.get(0)); //ok
       }
      

泛型的使用限制

  1. 基本类型不能作为类型参数

     //error
     Map<int, char> map1 = new HashMap<>();
     //ok
     Map<Integer, Character> map2 = new HashMap<>();
    
  2. 不能创建类型参数的实例

     public class Box<E> {
         public void add(Class<E> cls)  throws Exception{
             //error,类型参数的实例
             E e1 = new E();
             //ok
             E e2 = cls.newInstance();
         }
     }
    
  3. 不能用类型参数定义静态变量

     public class Box<E> {
         //error
         private static E value;
     }
     Box<Integer>[] box1 = new Box();
     Box<String>[] box2 = new Box();
     //请问静态变量value是什么类型? Integer还是String?
    
  4. 泛型类型的类型参数不能用在静态方法上

     public class Box<E> {	
         public static void show(E value) {}
     }
    
  5. 类型参数不能跟 instanceof 一起使用

     ArrayList<Integer> list = new ArrayList<>();
     //error
     if (list instanceof ArrayList<Integer>) {}
    
  6. 不能创建带有类型参数的数组

     //error
     Box<Integer>[] boxes1 = new Box<Integer>[4];
     //OK
     Box<Integer>[] boxes2 = new Box[4];
    
  7. 下面的方法不属于重载

     //error
     void testX(Box<Integer> box) {}
     void testX(Box<String> box) {}
     //error
     void foo(Box<? extends Number > box) {}
     void foo(Box<String> box) {}
    
  8. 不能定义泛型的异常类

     //error,异常子类不能有类型参数
     public class MyException<T> extends Exception {}
    
  9. catch 的异常类型不能用类型参数

     public static <T extends Exception> void test(Box<T> box) {
         try {
         } catch (T e) { //error,catch右边不能有类型参数
         }
     }
    
  10. 下面的代码是正确的

    public class Parser <T extends Exception> {
        //OK,异常抛出可以写T
        public void parse(File file)  throws T{
        }
    }
    
Table of Contents