Java 泛型

面向对象-泛型

1. 泛型

1.1 泛型介绍

1.1.1 泛型本质

(1)本质

  • 泛型本质是参数化类型,即指明使用的数据是什么类型

(2)参数化类型

  • 参数化类型是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)

1.1.2 泛型产生原因

(1)保证类型一致性

  • 假设一个链表保存的数据类型为Object,而保存的类型实例为Integer和String,如果都按String方式处理,必定会抛出ClassCastException
  • 当使用泛型对容器进行约束时,在编译阶段编译器便可发现类似问题,给予警告,即泛型将代码安全性检查提前到编译期

(2)使用泛型能写出更加灵活通用的代码

  • 泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨

(3)泛型能够省去类型强制转换

  • 在JDK1.5之前,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换
  • 加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,省去了转换步骤,使得代码更加简洁

1.2 泛型作用阶段

1.2.1 泛型擦除

(1)泛型擦除介绍

  • 泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码(.class文件)是完全相同的(编译器会自动进行类型转换并删除泛型信息),这个语法糖的实现被称为擦除

(2)泛型擦除的过程

1.2.2 泛型只在编译阶段有效

  • 编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段

  • 可以看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而经过编译后,生成的.class文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样

  • 测试:查看类的信息以及通过反射使用其他参数类型

    public class GenericTest {
        public static void main(String[] args) {
            List<String> stringArrayList = new ArrayList<String>();
            List<Integer> integerArrayList = new ArrayList<Integer>();
    
            Class strListClass = stringArrayList.getClass();
            Class intListClass = integerArrayList.getClass();
    
            System.out.println(strListClass);//class java.util.ArrayList
            System.out.println(intListClass);//class java.util.ArrayList
            System.out.println(strListClass == intListClass);//true
    
            Method add = strListClass.getMethod("add",Object.class);
            add.invoke(stringArrayList, Integer.valueOf(100));//[100]
        }
    }
    

1.3 类型边界

1.3.1 类型边界

  • 泛型在最终会擦除为Object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用Object自带的方法,所以需要使用类型边界来表明传入的对象到底是哪种类型或者是某一类类型

1.3.2 参数化类型

(1)类型参数

  • 使用符号代替特定的类型,如K,V,E,T等

(2)extend关键字

  • 泛型重载了extend关键字,可以通过extend关键字声明类或者方法中的参数类型是某一类或其子类类型或者是某一接口的实现类

    • 例:

      public class GenericTest {
          public <T extends Comparable<T>> int  compare(T o1,T o2){
              return o1.compareTo(o2);
          }
      }
      
      • T extend Comparable<T>表明T是实现了Comparable接口的类型
      List<? extends List> list = new ArrayList<>();
      
      • 表明list中元素为List接口类型,可以使用List l = list.get(1);但是只能存入List类型,其他类型都会报错

      • 一般在容器中只需要使用一般的泛型声明方式,即<T>就可以表明存储的类型为T或T的子类,比如:

        List<CharSequence> list5 = new LinkedList<>();
        list5.add("123");
        
  • 可以使用&连接extends后面的类,表示后面类的公共子类(当有表示实现的接口时需要将接口全部一到后面,例如<T extends String & Queue & List >,如果是这样<T extends Queue & List & String>会报错)

    public<T extends Queue & List> void print(T list){
        System.out.println(list);
    }
    
    
    public static void main(String[] args)  {
        GenericTest g1 = new GenericTest();
        LinkedList<String> list = new LinkedList<>();//LinkedList实现了List与Queue接口
        g1.print(list);//正常
        List<String> list2 = new ArrayList<>();
        g1.print(list2);//错误
    }
    

1.3.3 泛型通配符

(1)引入泛型通配符的意义

  • 请看以下问题:

    public class GenericTest {
    
        public <T extends CharSequence> void printValue(List<CharSequence> list) {
            System.out.println(list);
        }
        
        public <T extends CharSequence> void printValue2(List<? extends CharSequence > list) {
            System.out.println(list);
        }
        
    
        public static void main(String[] args)  {
            GenericTest g1 = new GenericTest();
            List<String> list = new ArrayList<>();
            g1.printValue(list);//编译器报错
            g1.printValue2(list);//正常
        }
    }
    
    • 虽然String类型是CharSquence的实现类,但是编译器去不允许实现类(子类)作为参数(两者此处在编译器看来并无关联),只能用声明的类型,所以需要使用通配符来表示传入的泛型的范围

(2) 通配符的使用

  • ?表示可以持有任何类型,但是与泛型类型如T,V,E等,?不能用于定义类和泛型方法

    public void print(List<?> list){
        System.out.println(list);
    }
    
    
    public static void main(String[] args)  {
        GenericTest g1 = new GenericTest();
        List<String> list = new ArrayList<>();
        g1.print(list);//正常
        List<Integer> list2 = new ArrayList<>();
        g1.print(list2);//正常
    }
    
  • 上界通配符 < ? extends E>表示参数化的类型可能是所指定的类型,或者是此类型的子类或实现类

    public void print(List<? extends CharSequence> list){
        System.out.println(list);
    }
    
    
    public static void main(String[] args)  {
        GenericTest g1 = new GenericTest();
        List<String> list = new ArrayList<>();
        g1.print(list);//正常
        List<StringBuffer> list2 = new ArrayList<>();
        g1.print(list2);//正常
    }
    
  • 下界通配符 < ? super E>表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

    public void print(List<? super String> list){
        System.out.println(list);
    }
    
    
    public static void main(String[] args)  {
        GenericTest g1 = new GenericTest();
        List<String> list = new ArrayList<>();
        g1.print(list);//正常
        List<CharSequence> list2 = new ArrayList<>();
        g1.print(list2);//正常
    }
    

(3)使用场景

  • 上界通配符主要用于读数据,下界通配符主要用于写数据。

    •     public void printValue2(List<? extends CharSequence > list) {
              System.out.println(list);
              list.add("String");//报错
        		CharSequence s = list.get(1);//正常
          }
      
      • 虽然我指定传入的list存储的元素为CharSequence实现类,但是本身CharSequence的实现类不止String一种,只有在方法外部我才知道list具体存储元素是什么,所以编译器就不允许存入确定的类型;
      • 但是我却知道list存储元素为CharSequence或其子类,因此可以用CharSequence或其父类引用指向其中的元素(多态)
    •     public void printValue2(List<? super CharSequence > list) {
              System.out.println(list);
              list.add("123");//正常
              CharSequence s = list.get(1);//报错
          }
      
      • 因为指定了list中存储元素都为CharSequence实现类的父类,所以将子类插入集合,即作为其本身或父类来使用(多态)是允许的
      • 但是当去取元素,子类(CharSequence)取指向其父类是不被允许的(比如:CharSeQuence c = new Object();是不允许的)

1.4 泛型的使用

1.4.1 泛型类

(1)泛型类介绍

  • 泛型类型用于类的定义中,该类可以使用所指定的泛型类型作为属性,该类被称为泛型类

    • 例:链表的节点类:

      private static class Node<E> {
              E item;
              Node<E> next;
              Node<E> prev;
      }
      

(2)泛型类作用

  • 通过泛型可以完成对一组类的操作对外开放相同的接口
  • 最典型的就是各种容器类,如:List、Set、Map
  • 如果创建泛型类对象时不声明泛型,则默认为Object类型,即传入参数可以为任意类型(所有类皆为Object子类,基本类型可以使用自动封箱机制封装成Object类),返回的类型为Obejct类型,需要强制转换

(3)泛型类使用约束

  • 如果该类为某一泛型类的子类时,如果extends关键字父类使用泛型,则在子类的声明中也至少应该加入对应的泛型声明

  • 子类的泛型声明范围必须不超出父类的泛型范围

    class S<E extends CharSequence> extends F<E > {
    
    }//正确
    
    class S<E> extends F<E extends CharSequence> {
    
    }//编译器报错
    
  • 泛型的类型参数只能是对象类型(包括自定义类),不能是简单类型

    • 例: 使用整数时只能使用Integer而不能使用int
  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错

    System.out.println(integerArrayList instanceof List<Integer>);//报错
    

1.4.2 泛型接口

(1)泛型接口介绍

  • 泛型接口与泛型类的定义及使用基本相同。

    public interface List<E> extends Collection<E> {
    	...
    }
    

(2)泛型接口的约束

  • 当未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类,如果类的声明中不加与实现的接口相对应的泛型信息,编译器会报错

    • 例:java.util.LinkedList

      public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
          ...
      }
      
  • 传入泛型实参时:

    • 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator,但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。

    • 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型,

      • 例:如果T为String类型,在方法、属性中的T都会被编译器认为成String类型。
      public interface Generictor<T> {
              T get();
      }
      
      public class GenericTest<T> implements Generictor<T> {
      
          T obj;
      
          public GenericTest(T obj){
              this.obj = obj;
          }
      
          @Override
          public T get() {
              return obj;
          }
      
          public static void showObj(GenericTest<Number> g){
              System.out.println(g.get());
          }
      }
      

1.4.3 泛型方法

(1)泛型方法

  • 泛型方法,是在调用方法的时候指明泛型的具体类型

    • 例如

      public class Displayer<E> {
          
          public<T> void display(T object){
              System.out.println(object);
          }
          
      }
      
      class Demo02{
          public static void main(String[] args) {
              new Displayer().display("你好,世界");
          }
      }
      
  • 说明:

    • public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
    • 只有在方法签名中声明了泛型的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法
    • <T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T
    • 上述方法中声明的T与类中声明的E为两种类型,实际上如果两者声明的泛型表示相同,则会判定为方法中声明的泛型类型

(2)静态泛型方法

  • 静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上,即如果静态方法要使用泛型的话,必须将静态方法也必须定义成泛型方法

  • 注意:

    1. 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)

    2. 即使静态方法要使用泛型类中已经声明过的泛型也不可以

      public static <T> void staticMethod(T t){
      	...
      }
      

1.4.4 泛型数组

(1)泛型数组不可被直接创建

  • 下述代码将会报错

    List<String>[] ls = new ArrayList<String>[10];  
    
  • 原因解释:

    //创建元素为存储Integer类型的数组表的泛型数组
    List<Integer>[] list = new ArrayList[10];
    
    //使用父类Object数组指向list
    Object[] objects = list;
    
    //创建存储元素为String类型的数组表
    List<String> stringList = new ArrayList<>();
    stringList.add("String");
    
    //Object类型可以指向任何子类,但是该stringList存储元素为String类型
    objects[0] = stringList;
    
    //但是此时我们可能依然以为objects[]数组中第一个存储着List<Integer>类型的表,当强制转换时会出错
    Integer s = ((List<Integer>)objects[0]).get(0);//java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    System.out.println(s);
    
    • 当使用Obejct类数组指向List类型的数组时,语法上并没有错误,而当转为Obejct数组类型后,因为元素为Object类型,便可以将其元素指向任意类型,此时该数组某些位置的类型已经不是List;因此,当之后需要使用Object类型数组转换为原本的类型时,便会出现强制转换异常

    参考

(2)使用通配符创建泛型数组

  • List<?>[] lists = new LinkedList<?>[10];//实际使用上与List<Object>没有太大区别
    
    List<? extends Comparable>[] lists = (List<? extends Comparable>[]) new LinkedList<?>[10];
    
  • 数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的

    • List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
      Object o = lsa;    
      Object[] oa = (Object[]) o;    
      List<Integer> li = new ArrayList<Integer>();    
      li.add(new Integer(3));    
      oa[1] = li; // Correct.    
      Integer i = (Integer) lsa[1].get(0); // OK
      

(3)在引用中声明泛型,而不在new中声明泛型

  • 该方法不能保证类型转换时的正确性,可能会出现ClassCastException,如下:

    List<Integer>[] list = new ArrayList[10];
    
posted @ 2020-04-06 00:31  NIShoushun  阅读(140)  评论(0编辑  收藏  举报