Java 泛型

Java 泛型

泛型的概念

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

泛型的好处

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

参数化类型

把类型当作参数一样传递

BoxT 称为类型参数类型变量,整个被称为泛型类型

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

泛型的原理

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

泛型使用

泛型类

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

泛型类继承

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

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


泛型的继承关系1

泛型的继承关系1

泛型接口

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

泛型方法

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

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

//仅使用了泛型参数的普通方法
public void setT(T t){
   this.t = t;
}

泛型擦除机制

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

编译器是如何擦除的

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

泛型擦除的副作用

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

桥方法

类型擦除的影响

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

public class Node {

    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 {
    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 会被擦除替换成 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编程思想》

受限的类型参数

作用

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

格式

单一限制:

多种限制:
多种限制,语法要求如果上限类型是一个类,必须放到第一个位置,否则编译错误

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

通配符

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

上界通配符

表示,泛型参数类型范围是 T 或其子类型

下界通配符

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

无界通配符

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

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

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

考虑以下方法,printlist:

// printList 的目标是打印任何类型的列表,但未能实现该目标(它仅打印 Object 实例的列表)
public static void printList(List 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 里面添加 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  void fooHelper(List l) {
        l.set(0, l.get(0));
    }
}

PESC原则

什么是 PESC

如果参数化的类型表示一个T的生产者,就用;如果他表示一个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){
        
    }
    
}

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

明确泛型参数的泛型是不变的,如ListList 并没有任何关系

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

class A{}
class B extens A{}
//协变
ArrayList listA = new ArrayList();
//逆变
ArrayList listA = new ArrayList();

  • JDK1.4 重写的方法参数和返回值要求一样
  • JDK1.5以后,重写的方法,参数要求一样的,返回值可以是协变的,即如果重写方法时返回值是被重写方法返回值的子类也可以

泛型与反射

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

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

public class GenericDemo {
    private Map map;

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

小结

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

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

你可能感兴趣的:(Java 泛型)