Java 泛型

作者: AaronSimon | 来源:发表于2019-01-15 20:31 被阅读111次

    一、概述

    1.1 什么是Java泛型

    Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法

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

    1.2 为什么使用Java泛型

    Java语言引入泛型的好处是安全简单。可以将运行时错误提前到编译时错误。

    在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

    二、泛型的使用

    2.1 泛型类

    泛型类用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。最典型的就是各种容器类,如:List、Set、Map。

    2.1.1 示例

    //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
    public class CustomMap<T> {
      //key这个成员变量的类型为T,T的类型由外部指定
      private T key;
      //泛型方法getKey的返回值类型为T,T的类型由外部指定
      public T getKey() {
        return key;
      }
    
      public void setKey(T key) {
        this.key = key;
      }
    }
    
    public static void main(String[] arg){
      //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
      CustomMap<String> a = new CustomMap <>();
      //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
      a.setKey("aas");
      CustomMap<Integer> b = new CustomMap <>();
      b.setKey(23);
      System.out.println(a.getKey());
      System.out.println(b.getKey());
    }
    

    在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

    public static void main(String[] arg){
      CustomMap a = new CustomMap <>();
      a.setKey("aas");
      CustomMap b = new CustomMap <>();
      b.setKey(23);
      System.out.println(a.getKey());
      System.out.println(b.getKey());
    }
    

    注意:

    1. 泛型的类型参数只能是类类型,不能是简单类型
    2. 不能对确切的泛型类型使用instanceof操作。如if( a instanceof CustomMap<String>)是非法的,编译时会出错

    2.2 泛型接口

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

    2.2.1 示例

    定义一个泛型接口

    public interface ICustomMap<T> {
      T getItem(T item);
    }
    

    实现泛型接口,传入泛型实参

    //在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
    public class CustomMap implements ICustomMap<String> {
      @Override
      public String getItem(String item) {
        return item;
      }
    }
    

    实现泛型接口,未传入泛型实参

    //未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
    //如果不声明泛型,如:class CustomMap implements CustomMap<T>,编译器会报错:"Unknown class"
    public class CustomMap<T> implements ICustomMap<T> {
      @Override
      public T getItem(T item) {
        return item;
      }
    }
    

    2.3 泛型方法

    泛型方法相对于泛型类,泛型接口比较复杂。泛型方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

    /**
     * 泛型方法
     * @param tClass 传入的泛型实参
     * @return T 返回值为T类型
     */
    public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
      IllegalAccessException{
            T instance = tClass.newInstance();
            return instance;
    }
    

    说明:

    1. public 与返回值中间 <T> 非常重要,可以理解为声明此方法为泛型方法
    2. 只有声明了 <T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法
    3. <T> 表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T
    4. 与泛型类的定义一样,T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型

    2.3.1 简单示例

    class test {
      //这是一个泛型类
      public class Custom<T>{
        private T key;
    
        public T getKey() {
          return key;
        }
        //虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        public void setKey(T key) {
          this.key = key;
        }
      }
      /**
       * 这才是一个真正的泛型方法
       *
       * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
       * 这个T可以出现在这个泛型方法的任意位置.
       * 泛型的数量也可以为任意多个
       *    如:public <K,V,T> T showValue1(Map<K,V> map,List<T> container){
       *        ...
       *        }
       */
      public <T> T showValue1(List<T> list){
        return list.get(0);
      }
      //这不是一个泛型方法,只是使用了List<String>这个泛型类做形参而已
      public void showValue2(List<String> list){
    
      }
    
      //这不是一个泛型方法,只不过使用了泛型通配符?
      //?是一种类型实参,可以看做为String等所有类的父类
      public void showValue3(List<?> list){
    
      }
    }
    

    2.3.2 泛型类中使用泛型方法

    class test {
      public class Custom<T>{
        private T key;
    
        public T getKey() {
          return key;
        }
    
        public void setKey(T key) {
          this.key = key;
        }
        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void showValue1(E t){
          System.out.println(t.toString());
        }
        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void showValue2(T t){
          System.out.println(t.toString());
        }
      }
    }
    

    2.3.3 泛型方法的可变参数

    public <T> void showValue( T... args){
        for(T t : args){
            System.out.println(t.toString());
        }
    }
    

    2.3.4 静态方法中使用泛型

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

    public class CustomMap<T> {
        /**
         * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
         * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
         * 如:public static void show(T t){..},此时编译器会提示错误信息:
              "StaticGenerator cannot be refrenced from static context"
         */
        public static <T> void showValue(T t){
    
        }
    }
    

    2.4 泛型通配符

    通配符有三种使用方式:

    • 上限通配符:? extends扩展类型
    • 下限通配符:? super超级类型
    • 无限通配符:?

    2.4.1 上限通配符

    问号(?)表示通配符,代表未知类型的泛型。有时候可能希望限制允许传递给类型参数的类型。 例如,对数字进行操作的方法可能只希望接受Number类或其子类的实例。要声明一个上限通配符参数,首先列出问号 ? 然后跟上extend关键字,之后再指定其上限。
    示例

    class test {
      public static double sum(List <? extends Number> numberlist) {
        double sum = 0.0;
        for (Number n : numberlist)
          sum += n.doubleValue();
        return sum;
      }
    
      public static void main(String args[]) {
        List<Integer> integerList = Arrays.asList(10, 12, 23);
        System.out.println("sum = " + sum(integerList));
    
        List<Double> doubleList = Arrays.asList(11.2, 32.3, 53.7);
        System.out.println("sum = " + sum(doubleList));
      }
    }
    

    2.4.2 无界通配符

    问号(?)表示通配符,代表未知类型的泛型。 当可以使用Object类中提供的功能或当代码独立于类型参数来实现方法时,这样的参数可以使用任何对象。要声明无限通配符参数,只需要列出问号(?)。
    示例

    class test {
      public static void printAll(List<?> list) {
        for (Object item : list)
          System.out.println(item + " ");
      }
    
      public static void main(String args[]) {
        List<Integer> integerList = Arrays.asList(10, 20, 30);
        printAll(integerList);
        List<Double> doubleList = Arrays.asList(11.2, 12.3, 13.5);
        printAll(doubleList);
      }
    }
    

    2.4.3 下限通配符

    问号(?)表示通配符,代表未知类型的泛型。 有时候可能希望限制允许传递给类型参数的类型。 例如,对数字进行操作的方法可能只需要接受Integer或其超类的实例,例如Number类的实例。要声明一个下限通配符参数,在问号?后跟super关键字,最后跟其下界。
    示例

    class test {
      public static void addCat(List<? super Cat> catList) {
        catList.add(new RedCat());
        System.out.println("Cat Added");
      }
    
      public static void main(String[] args) {
        List<Animal> animalList = new ArrayList<Animal>();
        List<Cat> catList = new ArrayList<Cat>();
        List<RedCat> redCatList = new ArrayList<RedCat>();
        List<Dog> dogList = new ArrayList<Dog>();
    
        // add list of super class Animal of Cat class
        addCat(animalList);
    
        // add list of Cat class
        addCat(catList);
    
        // 编译错误
        // addCat(redCatList);
    
        //编译出错
        // addCat.addMethod(dogList);
      }
    }
    

    2.5 Java 为什么不支持泛型数组

    2.5.1 分析

    对于Java数组,数组必须明确知道内部元素的类型,而且编译器会“记住”这个类型,每次往数组里插入新元素都会进行类型检查,类型不匹配会抛出java.lang.ArrayStoreException错误。如果我们直接创建一个泛型数组:

     List<String>[] ls = new ArrayList<String>[10];
    

    这样是没有办法通过的。原因就是Java的泛型是通过类型擦除(type erasure)来实现的。什么是类型擦除呢,简单来说Java在编译期间,所有的泛型信息都会被擦除掉。

    List<String> str = new ArrayList<>();
    List<Object> ob = new ArrayList<>();
    List list = new ArrayList();
    

    比如上面3个List,擦除类型参数以后,List中的实际元素都是Object。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。

    Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。所以由于类型擦除的原因,Java是禁止直接创建泛型数组实例的。

    2.5.2 如何创建真正的泛型数组

    我们不能直接创建泛型数组,饭可以吧数组强制转换成泛型类型。

    List<String>[] ls = (List<String>[])new ArrayList[10];
    

    这样编译的时候并不会报错,因为Java虽然禁止直接创建泛型数组实例,但并没有禁止声明一个泛型数组引用。所以仍然可以通过强制转型原生类型数组的方式,绕过限制。

    虽然编译期不会报错,但是这样做仍然有潜在的风险,因为类型擦除的存在,遇到下面这种情况运行期间就挂了:

    List<String>[] ls = (List<String>[])new ArrayList[10];
    ls[0] = new ArrayList(Arrays.asList(new Integer[]{1}));
    

    Java 中不允许直接创建泛型数组,所以并不建议通过各种方式绕过编译器的限制,这样会带来在代码出错时,编译器也有可能无法及时发现错误,从而带来潜在的风险。

    参考文章:Java 为什么不支持泛型数组

    相关文章

      网友评论

        本文标题:Java 泛型

        本文链接:https://www.haomeiwen.com/subject/xyrydqtx.html