美文网首页
Java 泛型

Java 泛型

作者: Mobile_Joy | 来源:发表于2020-09-19 16:43 被阅读0次

    Java 泛型

    泛型的概念

    JDK1.5引入的一种参数化类型特性,它提供编译时类型安全检测机制,使编译器能够在编译时检测到非法的类型。

    泛型的好处

    1. 代码更健壮,将类型检查提前到编译期,避免了运行时类型转换错误
    2. 代码更简洁,避免了强制类型转换
    3. 代码更灵活,便于复用

    参数化类型

    把类型当作参数一样传递

    Box<T>T 称为类型参数类型变量,整个被称为泛型类型

    Box<Apple> 中的 Apple 称为实际类型参数,整个被称为参数化的类型 ParametrizedType

    泛型的原理

    JDK1.5 引入泛型特性,Jvm 其实是不支持泛型,为了向下兼容,所以 Java 的泛型实现是一种伪泛型机制,也就是在编译期擦除了所有的泛型信息,这样就不会产生新的类型被编译成字节码,所有的泛型类型仍然是原始类型,运行时根本就不存在泛型信息,自然也不会影响以前编写类库的运行,实现了向下兼容

    泛型使用

    泛型类

        //泛型类的定义语法
        class 类名<泛型标识,泛型标识,...>{
           private 泛型标识 变量名;
        }
        //栗子
        public class Box<T>{
           //T是在实例化类时指明泛型参数的具体类型
           private T t;
           public void setT(T t){
               this.t = t;
           }
           public T getT(){
               return t;
           }
        }
    
    

    泛型类继承

    1. 父类是泛型类型(泛型参数T没有传实际的类型参数),子类也要是泛型类型
    class Child<T> extends Father<T>
    
    1. 父类是参数化的类型(泛型参数传了实际的类型参数),子类的实际类型参数可以不传
    class Child extends Father<String>
    

    泛型参数存在继承关系,并不代表泛型类型有继承关系
    如:Integer 继承自 Number,而 List<Number> 和 List<Integer> 并没有任何关系


    泛型的继承关系1
    泛型的继承关系1

    泛型接口

    泛型接口和泛型类类似,这里就忽略了代码

    泛型方法

    泛型方法与仅仅使用的泛型参数的普通方法的区别就是,返回值前是否有声明泛型,栗子:

    //泛型方法,返回值前<T>声明了泛型参数,调用时指明类型参数的具体类型
    public <T> void setT(T t){
       this.t = t;
    }
    
    //仅使用了泛型参数的普通方法
    public void setT(T t){
       this.t = t;
    }
    

    泛型擦除机制

    Java 的泛型是在 JDK1.5 引入的,虚拟机并不支持泛型,所以 Java 实现的是一种伪泛型机制,即在编译器擦除所有的泛型信息,这样 Java 就不需要产生新的类型到字节码,所有的泛型类型都是原始类型。

    编译器是如何擦除的

    1. 检查泛型类型,获取目标类型
    2. 擦除类型变量,并替换为限定类型
    • 如果泛型类型的泛型变量没有限定 (<T>),则用 Object作为替换类型
    • 如果有限定(<? extends XClass>,<? super XClass>),则用 XClass作为替换
    • 如果有多个限定(<? extend XClass1 & XClass2>),则用第一个边界XClass1作为替换类型
    1. 在必要时插入类型转换以保证安全
    2. 生成桥方法以在扩展的泛型类中保留多态

    泛型擦除的副作用

    1. 任何基础类型不能作为实际类型参数
    2. 无法创建类型参数的实例
    3. 不可直接创建具体泛型类型的数组
    4. 无法对参数化类型使用转换或 instanceof
    5. 无法使用类型参数声明静态变量
    6. 泛型类型无法直接或间接基础Throwable
    7. 当一个的所有重载方法的形参类型擦除后,如果他们具有相同的原始类型,那么此方法是不可重载的

    桥方法

    类型擦除的影响

    下面的代码片段中,声明了一个泛型类型和他的一个扩展类,并在扩展类中传入了实际类型参数

    public class Node<T> {
    
        public T data;
        
        public Node(T data) { this.data = data; }
        
        public void setData(T data) {
                System.out.println("Node.setData");
                this.data = data;
        }
    }
    
    public class MyNode extends Node<Integer> {
        public MyNode(Integer data) { super(data); }
     
        public void setData(Integer data) {
                System.out.println("MyNode.setData");
                super.setData(data);
        }
    }
    
    //编译后泛型擦除
    public class Node {
    
        public Object data;
        
        public Node(Object data) { this.data = data; }
        
        public void setData(Object data) {
                System.out.println("Node.setData");
                this.data = data;
         }
    }
    
        public class MyNode extends Node {
        
        public MyNode(Integer data) { super(data); }
         //与父类中的方法签名不同,这里并没有重写父类中的方法
        public void setData(Integer data) {
                System.out.println("MyNode.setData");
                super.setData(data);
        }
    }
    
    

    看看使用时的两段代码

    MyNode mn = new MyNode(5);
    Node n = mn;            // A raw type - compiler throws an unchecked warning
    n.setData("Hello"); //运行时,这里会抛出java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number   
    Integer x = mn.data;    
    
    

    既然扩展类中看上去并未重写父类中的方法,那么n.setData("Hello")应该是在调用从Note<T>类中继承的方法,根据泛型擦除机制,T 会被擦除替换成 Object,代码应该能够正确执行才对啊?

    但是,实际上为解决这样的类型擦除后方法重写失败,并保持泛型的多态性,编译器会自动生成一个桥方法

    class MyNode extends Node {
    
    // 编译后生成的桥方法
    //
    //    public void setData(Object data) {
    //        setData((Integer) data);
    //    }
    
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    
    // ...
    }
    

    桥方法和类型擦除后 Node 中的 setData 方法有着同样签名。而这个桥方法也在委托调用子类方法时,对参数类型进行了强制类型转换。所以,ClassCastExcption 异常就是在这里抛出来的

    泛型参数不能显式的用于运行时类型的操作。《Java编程思想》

    受限的类型参数

    作用

    对泛型变量的范围作出限制

    格式

    单一限制:<T extends Number>

    多种限制:<T extends A & B & C>
    多种限制,语法要求如果上限类型是一个类,必须放到第一个位置,否则编译错误

        interface A{}
        interface B{}
        class C{}
        //C上限是类,必须放在第一个位置
        class D<T extends C & A & B>{}
    

    通配符

    在通用代码中,称为通配符的 (?) 表示未知类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时作为返回值类型。通配符从不用作泛型方法的调用,泛型类实例创建或超类型的类型参数。

    上界通配符

    <? extends T> 表示,泛型参数类型范围是 T 或其子类型

    下界通配符

    <? super T>表示,泛型参数的类型范围是T或其父类型

    无界通配符

    <?>来表示,泛型参数是一种未知类型,也可以称为类型通配符,相当于List<? extends Object>,运行时和原始类型 List 没啥区别,但是在编译时List<?>会进行类型安全检查,而原始类型 List 不会。有两种情况,无界通配符是非常有用的:

    • 如果正在编写一个可以使用 Object 类中提供的功能实现的方法
    • 当代码使用通用类中不依赖于类型参数的方法时。如:List.size或List.clean。阅读源码时,你可能经常见到 Class<?> 类型,Class 中之所以经常使用,是因为 Class 中的大部分方法都不依赖于 T

    简而言之,就是代码中没有用到类型参数

    考虑以下方法,printlist:

    // printList 的目标是打印任何类型的列表,但未能实现该目标(它仅打印 Object 实例的列表)
    public static void printList(List<Object> list) { 
         for (Object elem : list) {
              System.out.println(elem + " "); 
              System.out.println();
         }
     }
    // printList 方法可以传入任意类型元素的List
    public static void printList(List<?> list) { 
         for (Object elem : list) {
              System.out.println(elem + " "); 
              System.out.println();
         }
     }
    

    通配符和子类型

    通配符类继承关系1
    通配符类继承关系2

    通配符捕获和帮助方法

    在某些情况下,编译器会推断通配符的类型。例如,可以将列表声明为 List<?>,但是在评估表达式是,编译器会从代码中推断出特定的类型(CAP#1),这种情况称为通配符捕获。

    //这里也无法通过编译,错误: 不兼容的类型: Object无法转换为CAP#1(捕获变量),
    //编译器只能知道 i.get(0) 可以是一个 Object 作为 ?的上界,而编译器将 ?当作一种 CAP#1 的新的类型,扩展自 Object。
    //例如,即便 B 和 C 是 A 的子类,你也不能往 List<? extends A> 里面添加 B 类后又添加 C 类,来个混搭。
    pulic void foo(List<?> i){
        i.set(0,i.get(0);
    }
    //为了解决这个问题,采用以下方法,显式指出参数 T,强制指出前后匹配性,这样就完成了编译检查。
    public class WildcardFixed {
        void foo(List<?> i) {
            fooHelper(i);
        }
    
        // Helper method created so that the wildcard can be captured
        // 编译器能够推断出 T 是 CAP#1(捕获变量)
        private <T> void fooHelper(List<T> l) {
            l.set(0, l.get(0));
        }
    }
    

    PESC原则

    什么是 PESC

    如果参数化的类型表示一个T的生产者,就用<? extends T>;如果他表示一个T的消费者,就用<? super T>

    productor -- extends
    上界通配符限制的泛型类型,可以作为生产者,安全取元素,但不能add

    consumer -- super
    下界通配符,可以作为消费者,安全add元素(必须是下界及其派生类),不能取元素

    通配符使用准则

    为了便于讨论,变量视为提供以下两个功能之一:
    输入变量:将数据提供给代码,作为数据源。如 copy(src,dest) 参数复制方法,要将 src 中的数据复制到 dest 中,则 src 就是输入参数。
    输出参数: 保存要在其他地方使用的数据。在上面的 dest 参数就是接受数据,因此它是输入参数。
    在考虑使用通配符以及哪种类型的通配符时,可以使用"输入"和"输出"原理,下面提供了要遵循的原则:

    • 使用上界通配符 extends 定义输入变量
    • 使用下界通配符 super 定义输入变量
    • 如果可以使用 Object 类中定义的方法访问输入变量,使用无界通配符 ?
    • 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符

    数据类型变化

    Type Variance形式化定义

    假设 A、B 是类型,f(·) 表示类型转换,表示继承关系,如A ≤ B,表示 A 继承 B

    • f(·)协变的,若 A ≤ B,则有 f(A) ≤ f(B)
    • f(·)逆变的,若 A ≤ B,则有 f(B) ≤ f(A)
    • f(·)不变的,若A ≤ B,则f(A) ≤ f(B)f(B) ≤ f(A)都不成立,即f(A)f(B) 没有关系
    • f(·)双变的,若 A ≤ B,则有 f(A) ≤ f(B)f(B) ≤ f(A)都成立

    Java 数组是协变的

    String 是 Object 的子类,String[] 是 Object[] 的子类

    class A{}
    class B extends A{}
    class C extends B{}
    
    //则
    public void test(){
        B[] array1 = new B[1];
        array1[0] = new B();
        A[] array2 = array1;
        try{
            // 编译时ok,运行时 error,编译看声明类型,运行时看实际类型,所以 B 类型数组里面,无法放父类 A
            array2[0] = new A();
            //数组协变,B[] arrayB = new C[1],所以可以当作子类的数组来用
            array2[0] = new C();
            
        }catch(Exception ex){
            
        }
        
    }
    

    明确泛型参数的泛型是不变的

    明确泛型参数的泛型是不变的,如List<String>List<Object> 并没有任何关系

    具有受限类型参数的泛型是可变的

    class A{}
    class B extens A{}
    //协变
    ArrayList<? extends A> listA = new ArrayList<B>();
    //逆变
    ArrayList<? super B> listA = new ArrayList<A>();
    
    
    • JDK1.4 重写的方法参数和返回值要求一样
    • JDK1.5以后,重写的方法,参数要求一样的,返回值可以是协变的,即如果重写方法时返回值是被重写方法返回值的子类也可以

    泛型与反射

    泛型参数虽然会在编译时被擦除,但是泛型的类型信息会保留类的常量池内,所以在运行时,仍然可以通过发射获取泛型的类型信息。

    ParameterizedType 泛型类型,如Map<String,Integer>类型的抽象,可以通过这个类提供的方法获取实际类型参数的类对象(Class对象)

    public class GenericDemo {
        private Map<String,Integer> map;
    
        public static void main(String[] args) throws NoSuchFieldException {
            Field f = GenericDemo.class.getDeclaredField("map");
            //获取泛型类型
            System.out.println(f.getGenericType());//java.util.Map<java.lang.String, java.lang.Integer>
            ParameterizedType parameterizedType = (ParameterizedType) f.getGenericType();
            //获取原始类型
            Type rawType = parameterizedType.getRawType();//interface java.util.Map
            System.out.println(rawType);
            //获取实际类型参数的类类型对象数组,即Class<T>的数组,这里数组的元素分别是String.class,Integer.class
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            System.out.println(actualTypeArguments[1]);//class java.lang.Integer
        }
    }
    

    编译后,泛型类型的泛型信息会被保留到 signature 注释中

    / class version 51.0 (51)
    // access flags 0x21
    public class com/nd/android/xx/java/demo/generic/GenericDemo {
    
      // compiled from: GenericDemo.java
    
      // access flags 0x2
      // signature Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;
      // declaration: map extends java.util.Map<java.lang.String, java.lang.Integer>
      private Ljava/util/Map; map
      ...
    }
    

    小结

    以上介绍了下面几个方面的内容:

    1. 泛型的概念
    2. 泛型带来的好处
    3. 泛型分别可以使用在类、接口和方法中
    4. 介绍了泛型的实现原理
    5. 泛型擦除机制及其带来的问题
    6. 了解了受限类型参数及其意义
    7. 协变和协变相关的概念,以及泛型如何来支持协变的
    8. 介绍了如何通过反射获取实际的泛型参数

    相关文章

      网友评论

          本文标题:Java 泛型

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