Java泛型语法,原理,高级用法和局限

2020-02-22  本文已影响0人  alonwang

前言

泛型是现代编程语言很重要的组成部分,它的出现有两个目的:

Java从JDK5开始添加了对泛型支持,本文将详述Java(JDK8)中泛型的语法,原理,高级用法和局限.

正文

泛型类和方法的定义

    //非严谨定义,仅表示基本结构
    /**
    *泛型类
    */
    public class class_name<T1, T2, ..., Tn> { /* ... */ }
    
    /**
    *泛型方法
    */
    public <T1,T2,...,Tn> void method_name(T1 t1,T2 t2,...Tn){
     /* ... */ }
    
    //eg.
    
    //创建单Parameter的泛型类
    class A<T>{ /* ... */ }
    //创建多Parameter泛型类
    class B<T,V>{ /* ... */ }


​    
    class A<T> {
            //使用类Type Parameter的方法
            public void method1(T t) {
                /* ... */
            }
        //泛型方法
            public <R> void method2(R r){
                /* ... */
            }
            //混合类Type Parameter和方法Type Parameter的方法
            public <R> void method3(R r, T t) {
                /* ... */
            }
        }

T1,T2,...Tn被称为 Type Parameter(类型参数,详见Type Parameter 和 Type Argument )

泛型类和方法的使用

Parameterized Types和Raw Types

如果生成泛型类对象时指定了具体的Type Argument,就称之为Parameterized Types,例如下面这句就生成了Parameterized Types为Integer的ArrayList.

new ArrayList<Integer>();

如果生成时没有指定Type Argument,就称为 Raw Types,例如下标这句生成了Raw Types的ArrayLis

//编译时会提示  Unchecked assignment: 'java.util.ArrayList' to 'java.util.List<java.lang.Integer>'
List<Integer> list=new ArrayList();

为什么会有Raw Types呢? 假设现在有一个JDK5之前编写的方法,在Raw Types的支持下,老代码无需修改就能获得泛型的便利,我们可以直接这样写,一方面减轻了用户更新JDK版本的难度,另一方面保证了JDK的兼容性.

public List getList(){/*...*/}

List<String> result=getList()

基本用法

    /**
    *创建泛型类对象
    */
    new class_name()<>();
    
    /**
    *手动指定Type Argument的泛型方法
    */
    obj.<Type argument>method_name(args)


​    
    class A<T> {
            //使用类Type Parameter的方法
            public void method1(T t) {}
        //泛型方法
            public <R> void method2(R r){}
            //混合类Type Parameter和方法Type Parameter的方法
            public <R> void method3(R r, T t) {}
        }
    
    //创建泛型类对象
    A<String> a = new A<>();
    //1 Parameterized Types在泛型类对象创建的时候已经确定了,A的  Parameterized Types是String
    a.method1("abc");
    //2 Parameterized Types会根据传入argument的类型自动推导为String
    a.method2("abc");
    //3 显式指定了Parameterized Types,不再使用自动推导.签名为method1(Long t)
    a.<Long> method2(0L);
    //4 来自泛型类的Type Parameter由泛型类确定,方法自身的泛型参数由传入的argument决定
    a.method3(0, "abc");

Java中的泛型实现原理

Java中的泛型使用类型擦除方式实现,JVM并不知道泛型,所有的泛型在编译阶段就已经被处理成了普通类和方法,并体现在生成字节码上

擦除原理代码演示

public static void eraseTest() {
        List l1 = new ArrayList();
        l1.add("abc");
        l1.add(Integer.valueOf(123));
        String s1 = (String) l1.get(0);
        Integer i1 = (Integer) l1.get(1);
        List<String> l2 = new ArrayList();
        l2.add("abc");
        // IDE报错提示: 类型不符
        // l2.add(Integer.valueOf(123));
        String s2 = l2.get(0);
        List<String> l3 = new ArrayList<>();
        l3.add("abc");
        // IDE报错提示: 类型不符
        // l3.add(Integer.valueOf(123));
        String s3 = (String) l1.get(0);
    }

//对应的字节码
public static void eraseTest();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=7, args_size=0
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_0
         8: aload_0
         9: ldc           #4                  // String abc
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_0
        18: bipush        123
        20: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        23: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        28: pop
        29: aload_0
        30: iconst_0
        31: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        36: checkcast     #8                  // class java/lang/String
        39: astore_1
        40: aload_0
        41: iconst_1
        42: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        47: checkcast     #9                  // class java/lang/Integer
        50: astore_2
        51: new           #2                  // class java/util/ArrayList
        54: dup
        55: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
        58: astore_3
        59: aload_3
        60: ldc           #4                  // String abc
        62: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        67: pop
        68: aload_3
        69: iconst_0
        70: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        75: checkcast     #8                  // class java/lang/String
        78: astore        4
        80: new           #2                  // class java/util/ArrayList
        83: dup
        84: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
        87: astore        5
        89: aload         5
        91: ldc           #4                  // String abc
        93: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        98: pop
        99: aload_0
       100: iconst_0
       101: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
       106: checkcast     #8                  // class java/lang/String
       109: astore        6
       111: return

使用绑定的类型(或Object)替换泛型中的Type Parameter,这样生成的字节码中,只包含原始的类,接口和方法

首先看 0~4,51~55,80~84 三种创建泛型类对象的方式生成的字节码是相同的,都是

// Method java/util/ArrayList."<init>":()V

再看11,62,93 ,add方法的签名也都相同,参数都是Object而非具体的Type Argument

InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

为了保证类型安全,在必要的地方加入类型转换

36,75,106中 36对应的字节码checkcast 是由源代码编译出的,而75,106都是编译器对泛型处理后自动生成的

在有继承的泛型类中使用桥接方法(bridge methods)保证多态

class A {
        public <T> void method(T t) {

        }

    }

    class B extends A {
        @Override
        public <T> void method(T t) {

        }
    }
//类A对应字节码
public void method(T);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 15: 0
    Signature: #14                          // (TT;)V

//类B对应字节码
public void method(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 23: 0
  //桥接方法
  public void method(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method method:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0

泛型擦除后,A中method签名为 method(Object) 而B继承Parameterized Types为String的A后,重写的method签名为method(String),这就导致多态出现问题, 为了解决这个问题,编译器会自动生成一个桥接方法, 签名为 method(Object),这个方法内部去调用B重写的method. 保证多态.

高级用法

定义时限定Type Parameter上界

限定上界,extends后如果跟随多个,第一个必须为类或抽象类,后面的必须为接口,使用时Type Argument 必须同时满足所有限定条件

/**
*限定上界的类定义
*/
class class_name <T extends class_or_ abstract_class_name&interface_name&interface_name>
/**
* 限定上界的方法定义
*/
public <T extends class_or_ abstract_class_name&interface_name&interface_name> void method2(T t)

//eg.
class C<T extends Number> extends ArrayList<T> {

}

public <T extends Number & Serializable> void method2(T t) {

}

没有限定下届的语法,个人认为是意义不大,所以没有.

通配符?在泛型中的应用

PECS(Producer Extends Consumer Super)规则

  1. 如果一个对象频繁向外读取数据的,就被称为 "in" ,适合用上界 extends
  2. 如果一个对象经常向里插入数据,就被称为"out"适合用下界super

场景设定: Plate可以装载物品, Food,Fruit,Apple,Banana有继承关系.

static class Plate<T> {
        private T t;

        public T getT() {
            return t;
        }

        public void setT(T t) {
            this.t = t;
        }
    }

    static class Food {

    }

    static class Fruit extends Food {

    }

    static class Apple extends Fruit {

    }

    static class Banana extends Fruit {

    }

限定上界

Image-extend.png
Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<? extends Fruit> p = applePlate;
// 编译错误,类型不符
// p.setT(new Apple());
Fruit f = p.getT();
f.printName();

不能存,只能取, 且取出的是上界对应的对象. <? extends Fruit>只知道里面存的是Fruit或Fruit的子类,标记为#A,具体是什么不知道,想插入数据时不知道和#A是否匹配,所以存不进去,取出时同理,只能当做Fruit取出.

限定下界

Image-super.png
Plate<? super Fruit> plate = new Plate<>();
plate.setT(new Fruit());
Object obj = plate.getT();
((Fruit) obj).printName();

能存,存的必须是Fruit或Fruit的直接/间接父类, 可以取,因为Object是所有类的基类,因此只能当做Object取

无界

Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<?> plate = applePlate;
Object obj=plate.getT();
((Apple) obj).printName();

不能存,只能取,且取出时只能为Object.它有两个用途

<?>和<Object>是有差别的,<?>只能取,不能存, 而<Object>是可以作为Object存的.

为什么用了通配符?某些场景下不能存,某些可以

上面讲过类型擦除为了保证类型安全,在必要的地方加入类型转换,这就要有有一个确定的类型,<? extends X> 可以确保是X或X的子类,由于类的继承原理: X的子类可以强转为X,添加强转是使用X即可.因此可以存,而<? super X>和<?> 都没有确定的类型,都表示一个范围,因此不能存.

泛型的继承关系

泛型不会继承普通类的继承关系

Untitled.png

要完成泛型中的继承,需要使用 ?, ?在继承中对泛型的作用类似于Object.

Untitled 1.png

擦除的弊端

上面说过,擦除法是在编译进阶擦除类型信息,这也就导致运行时获取不到类型信息.

泛型弊端演示

下面这段代码是无法编译通过的,原因上面的完全wildcard讲过

void foo(List<?> i) {
        i.set(0, i.get(0));
    }

要解决这个问题,可以添加一个辅助方法

void foo(List<?> i) {
        forHelp(i);
    }

    <T> void forHelp(List<T> i) {
        i.set(0, i.get(0));
    }
    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
            Number temp = l1.get(0);
            l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
            // got a CAP#2 extends Number;
            // same bound, but different types
            l2.set(0, temp);        // expected a CAP#1 extends Number,
            // got a Number
        }

这边这段代码从语法上就是错误的,但是报错信息可能会让人迷惑,现在假设有这些数据

    List<Integer> l1 = Arrays.asList(1, 2, 3);
    List<Double>  l2 = Arrays.asList(10.10, 20.20, 30.30);
    swapFirst(l1, l2);

很明显, l2中的数据是Double类型,而l1中的数据时Integer类型,当然无法set.

    //compile error!
    List<int,int> il
    public static <E> void append(List<E> list) {
        E elem = new E();  // compile-time error
        list.add(elem);
    }

一种可行的解决方案

    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E elem = cls.newInstance();   // OK
        list.add(elem);
    }
    //compile error
    public class MobileDevice<T> {
        private static T os;

        // ...

    }
    //ArrayList<Integer>, ArrayList<String> ,LinkedList<Character>都可能被传递进去,而runtime并不会追踪Type Parameters
    public static <E> void rtti(List<E> list) {
        if (list instanceof ArrayList<Integer>) {  // compile-time error
            // ...
        }
    }
    
    // compile-time error
    List<Integer> li = new ArrayList<>();
    List<Number>  ln = (List<Number>) li;  
    
    
    //OK
    List<String> l1 = ...;
    ArrayList<String> l2 = (ArrayList<String>)l1;
    Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
    stringLists[0] = new ArrayList<String>();   // OK
    stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                                // but the runtime can't detect it.
    // Extends Throwable indirectly
    class MathException<T> extends Exception { /* ... */ }    // compile-time error
    
    // Extends Throwable directly
    class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
    
    public static <T extends Exception, J> void execute(List<J> jobs) {
        try {
            for (J job : jobs)
                // ...
        } catch (T e) {   // compile-time error
            // ...
        }
    }
    
    
    class Parser<T extends Exception> {
        public void parse(File file) throws T {     // OK
            // ...
        }
    }
    // compile error
    public class Example {
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }

后记

写这篇文章的起因是在做一个需求的时候,同事用泛型很好的封装了一个基础组件,优雅简洁,我却完全没有想到,还在傻傻的强转类型转换. 很早之前我就研究过泛型并且做了笔记, 趁这次机会回顾实践一下并写了这篇文章.


官方文档

JVM如何理解Java泛型类(转)

上一篇 下一篇

猜你喜欢

热点阅读