Java 泛型

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. 介绍了如何通过反射获取实际的泛型参数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270