Java泛型详解

第一次接触泛型 完成

使用容器类时

ArrayList<Integer> list = new ArrayList<>();

list.add(123);

Integer i = list.get(0);

定义 <> 内为 Integer,写代码时就只能向 list 传入 Integer 类型的数据。

并且可以直接通过 Integer 类型变量接收 list 的元素。

`

ArrayList<Test> list = new ArrayList<>(); Test test = list.get(0);

也定义为自定义类型。

`

ArrayList list = new ArrayList(); String str = (String) list.get(0);

也可以不定义类型,默认为 Object。

ArrayList 类是怎么做到能够插入任意相同类型的数据呢?通过多态吗?

如果是多态,那又是怎么做到 Integer i = list.get(0); 不需要 Object 强转为 Integer 呢?

实际上是通过泛型。

JDK 1.5 引入了泛型。所有容器类的定义都加上了泛型。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable 
    
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

ArrayList::add(e)、ArrayList::get(index)

public boolean add(E e) {...}
public E get(int index) {...}

而在 JDK 1.5 之前,ArrayList 只能强制转换。

public void add(Object obj){...}
public Object get(int index){...}
JDK1.5...
ArrayList list = new ArrayList();
list.add(123);
list.add("haha");
Integer o1 = (Integer) list.get(0);
//ClassCastException 
Integer o2 = (Integer) list.get(1);

这样很容易造成 ClassCastException 类型转换异常。

·

泛型是什么?

泛型

概念

即广泛的类型,类型可以作为参数传递。

Java 中的类型分为基本数据类型和引用数据类型。泛型只能使用引用数据类型(数组同样适用)。不存在 ArrayList< int >,ArrayList< double >

好处

  • 将确定类型延迟到使用类/方法/接口时。对应泛型类、泛型方法、泛型接口

  • 类型安全,将类型转换检查提前到编译期,禁止一切违反类型安全的操作,有效避免 ClassCastException。

Integer i = list.get(0);
//开发环境和编译器会提示类型错误
list.add("haha");
String str = list.get(1);
  • 简化代码,提高可读性。
ArrayList list = new ArrayList();
list.add(123);
Integer o1 = (Integer) list.get(0);
----------------------------
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
Integer i = list.get(0); //不需要冗余的强制转换代码
  • 集合类中的元素类型一定相同,可以使用增强 for 循环;for-each
for(Integer i:list){
	...
}

泛型与多态无关

泛型:编译后即可确定实际类型。

多态:编译期间无法确定,运行时才能确定实际类型。多态是基于继承和调用虚方法的

泛型类

在类名前可以定义类中通用的泛型 < T>,当使用 UseGeneric 类时需要传入类型参数实例化 T 的类型,成员变量 key 的类型也同时确定。

public class UseGeneric<T>{
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

测试类 GenericTest ,将泛型 T 实例化为 String,同时 key 也确定类型为 String。

public class GenericTest {
    public static void main(String[] args) {
        UseGeneric<String> useGeneric = new UseGeneric<>();
        useGeneric.setKey("123");
        String key = useGeneric.getKey();
    }
}

基本原理

对 GenericTest::main 方法 javap 反编译后:

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
0: new           #2                  // class generic/UseGeneric
3: dup
4: invokespecial #3                  // Method generic/UseGeneric."<init>":()V
7: astore_1
8: aload_1
9: ldc           #4                  // String 123
11: invokevirtual #5                  // Method generic/UseGeneric.setKey:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #6                  // Method generic/UseGeneric.getKey:()Ljava/lang/Object;
18: checkcast     #7                  // class java/lang/String
21: astore_2
22: return
	LocalVariableTable: 局部变量表
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 useGeneric   Lgeneric/UseGeneric;
           22       1     2   key   Ljava/lang/String;
	LocalVariableTypeTable: 字段特征签名表
        Start  Length  Slot  Name   Signature
            8      15     1 useGeneric   Lgeneric/UseGeneric<Ljava/lang/String;>;

在方法表的 Code 属性中看到:

15:invokevirtual 调用虚方法 getKey() 可以看到方法返回值为 java/lang/Object。

说明编译后将 T 被泛型擦除为 java/lang/Object 类型

18:checkcast 类型转换检查,检查 key 的实际类型是否为 String,如果不是则抛出强转异常 ClassCastException。(实际上,这里的类型转换操作因为泛型的存在,checkcast 一定成功)

T 被泛型擦除后,编译器还帮助我们加上了类型转换的代码:

String key = useGeneric.getKey(); —> String key = (String) useGeneric.getKey();

`

main 方法表除了 Code 属性还有;

1.LocalVariableTable 2.LocalVariableTypeTable

其中 1. 是局部变量表,用于存储变量名与变量描述符等信息 2.是字段特征签名表,存储记录泛型信息的 Signature 属性等信息。

《深入理解Java虚拟机》:

在 JDK 5 增加泛型后,Signature 属性增加到 Class 文件规范之中,可出现于类、字段表、方法表结构的属性中。在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量 (TypeVariable)或参数化类型(Parameterized Type),则 Signature 属性会为它记录泛型签名信息。

因为泛型信息被存储与 Signature 属性中。所以即使编译期间将泛型擦除,程序运行期间还是可以通过 Class 文件 中的字段表和方法表中可以动态获取泛型信息的。

`

泛型方法

getKey 方法不是泛型方法,而是泛型类中的方法

public class UseGeneric<T>{
    private T key;

    public T getKey() {
        return key;
    }
}

一个方法是不是泛型方法,与它所在的类无关。只有声明并使用类型参数的方法才是泛型方法。

public <T> T compareAndReturn (T t1,T t2){
        return t1.equals(t2) ? t1 : null;
}

`

public <H> void test(ArrayList<H> list){
        list.add(123); //compile error
        list.add(list.get(0));
    }

T 在编译期间类型未知,会被擦除为 Object ,我们除了调用 Object 类的方法就做不了什么。

而且因为 T 的类型未知,例如 test() 方法的情况,我们无法做出可能会违背类型安全的操作。

通过在定义类型参数时限制上界 < T extends xx> 表示 T 只能为 xx 的子类。T 在编译后被擦除为 xx

public class Test{
	
	public <T extends Number> T compareAndReturn (T t1,T t2){
        //T 被擦除为 Number
		return t1.doubleValue() > t2.doubleValue() ? t1 : t2;
	}
    
}

类型参数的限定

泛型的上界

上界可以是实体类

< T extends Son>

在声明泛型时可以指定泛型的上界,表示在泛型擦除后,T 将被修改为 Son。

那么 compareAndReturn 方法在编译后长这个样子:

public Number compareAndReturn (Number t1,Number t2){
		return t1.doubleValue() > t2.doubleValue() ? t1 : t2;
}

使用该方法时,传入的类型参数被限定为 Number 及其子类,返回值的实际类型也是 Number 及其子类。

Double aDouble = compareAndReturn(100.1, 100.2);

上界可以是接口

< T extends Comparable>

public <T extends Comparable<T>> T compareAndReturn (T t1,T t2){
        return t1.compareTo(t2) > 0 ? t1 : t2;
}

<T extends Comparable> 表示传入的类型参数被限定为 Comparable 的实现类、并且只能与相同类型进行比较。

上界可以是其他泛型

//自定义方法
public class EasyArrayList<E> {
	public <T> void put(ArrayList<T> list){
        for(T t:list){
            add(t); // need cast to E
        }
	}
}

public static void main(String[] args) {
	EasyArrayList<Number> list = new EasyArrayList<>();
	EasyArrayList<Integer> integers = new EasyArrayList<>();
    //不允许插入不同类型的元素
	list.put(integers);//compile error
}

将 put 方法修改后可以添加 Number 的子类的集合。

public <T extends E> void put(ArrayList<T> list){
        for(T t:list){
            add(t);
        }
	}

泛型接口

面向接口和能力编程

先有接口,再有实现类。接口将能力进行抽象,具体的类实现接口来获得能力。例如 Comparator 比较器接口、Comparable 可比性接口,一个谁都能实现的接口,实现 compare 方法即代表该实体类具有比较的能力;实现 Comparable 接口则表示实体类可以被比较。

没有泛型前
public interface Comparator{
	boolean compare(Object o1,Object o2);
}

class A implements Comparator{
    int key;
    int compare(Object o1,Object o2){
        ...
    }
}

有了泛型以后,提高了可读性,能够让实现类更加清晰的了解到如何实现该方法。

public interface Comparator<T>{
	boolean compare(T o1,T o2);
}

class A implements Comparator{
    int key;
    int compare(A o1,A o2){
        if(o1.key == o2.key) 
            return 0;
        return o1.key > o2.key ? 1 : -1;
    }
}

泛型通配符

无限定通配符 <?>

为什么要通配符

实例化参数类型时,如果不能确定,可以使用通配符 ? 表示任意类型。

public void test(ArrayList<?> list){
      ...
}

test 方法可以接收各种类型参数的 List 对象

test(new ArrayList<Integer>());
test(new ArrayList<String>());

使用通配符可以做到更简洁的类型参数限定:

public <T extends E> void put(ArrayList<T> list){
        for(T t:list){
            add(t);
        }
	}

-------------------------------
    //限定传入的 ArrayList 泛型为 E 的子类
public void put(ArrayList<? extends E> list){
        for (E e : list) {
            add(e);
        }
}

这样看起来无限定通配符很强大。但实际上,它很蛋疼。

public void test(ArrayList<?> guys){
	Object o = guys.get(0);//compile success
}

获取 list 中的数据竟然还要用 Object 来接收。Object o = list.get(0) 这与泛型的初衷背道而驰。

有限定通配符和超类型通配符给予了?存在的意义。

`

了解这两个通配符前,先知道 PECS 原则。

PECS(Producer Extends Consumer Super)

“PECS” is from the collection’s point of view. If you are only pulling items from a generic collection, it is a producer and you should use extends; if you are only stuffing items in, it is a consumer and you should use super. If you do both with the same collection, you shouldn’t use either extends or super.

如果当前容器只用来获取元素,容器就相当于一个 producer,应该使用< ? extends T>

如果当前容器只用来填充元素,容器就相当于一个 consumer,应该使用 < ? super T>

有限定通配符 <? extends >

public void test(ArrayList<? extends Father> guys){
	Father guy = guys.get(0);//compile success
	guys.add(new Son()); // compile error
}

该"生产者"集合被实例化为 Father 及其某个子类的集合。可能是 Son 儿子类,也可能是 Daughter 女儿类

ArrayList<Son> guys = new ArrayList<>();
ArrayList<Daughter> guys = new ArrayList<>();

编译期间无法确定是以上哪两种,那么如果向该集合中填充数据的话,编译器是不能保证类型安全的。所以编译器直接禁止向"生产者"集合中填充数据,避免以下情况发生的类型错误:

ArrayList<Son> sons = new ArrayList<>();
sons.add(new Daughter());
ArrayList<Daughter> daughters = new ArrayList<>();
daughters.add(new Sons());

但编译器允许以下操作:

ArrayList<? extends Father> guys = new ArrayList<>();
ArrayList<Son> sons = new ArrayList<>();
sons.add(new Son());
guys = sons;

参数类型为 Father 的子类的集合 sons 可以赋值给 fathers。

编译器知晓 sons 是 Son 儿子类的集合,能够在保证类型安全的前提下,将 guys 指向一个子类集合对象。相当于在编译期间就通知编译器,当前集合中只可能存在 Son 类型的数据:

ArrayList<? extends Father> guys ... -> ... -> ... -> new ArrayList<Son>();

"生产者"集合能够保证集合当中的元素都是 Father 的子类。所以获取集合当中数据时可以以 Father 静态类型统一接收。

Father guy = guys.get(0);

通过反射可以看出,泛型为 <? extend Father> 被擦除后静态类型为 Father

Class<? extends Father> clazz = ...编译期间不能确定;
Father genericFather = clazz.newInstance();

超类型通配符< ? super >

ArrayList<? super Father> guys = new ArrayList<>();
guys.add(new Father());//compile success
guys.add(new Son());//compile success
guys.add(new GrandFaher());//compile error
guys.add(new Object())//compile error

< ? super Father> 是指类型参数被实例化为 Father 及其父类。

编译器只知道当前"消费者"集合可能是 Father 类或其父类的集合

ArrayList<Father> guys = new ArrayList<>();
ArrayList<GrandFather> guys = new ArrayList<>();

编译器无法确定,所以干脆禁止向当前集合中加入不能保证类型安全的数据。但是!编译器能够保证加入 Father 及其子类数据是类型安全的。所以"消费者"集合可以任意加入限定类型的子类的数据。

ArrayList<Father> fathers = new ArrayList<>();
ArrayList<GrandFather> grandFathers = new ArrayList<>();
fathers.add(new Son());
grandFathers.add(new Daughter());

以下方式创建"消费者"集合也是被编译器允许的:

ArrayList<? super Father> guys = new ArrayList<>();
ArrayList<GrandFather> fathers = new ArrayList<>();
guys = fathers;

guys 指针重新指向新的集合后(grandFather 对象集合 -> father 对象集合),当前集合中只有 grandFather 一种数据。该集合还可以任意添加子类对象:

guys.add(new Son());
guys.add(new Daughter());
guys.add(new Father());

所以获取当前集合中的数据只能用所有类的统一父类 Object 来接收。因为编译期间不能确定 guys 被初始化成了什么类型的集合,集合当中存在哪几种数据。为了保证类型安全,只能让 Object 老大出山。

Object o = guys.get(0);

从反射可以看出 泛型 < ? super Father> 被擦除后静态类型为 Object

Class<? super Father> clazz = ...编译期间不能确定;
Object guy = clazz.newInstance()

泛型擦除

Java 编译器将代码通过类型转换的方式来实现泛型擦除的。

泛型只作用于编译期间,运行期间只是存在于类型信息中。

为什么进行泛型擦除?

向后兼容,例如低版本的集合类没有泛型

ArrayList list = new ArrayList();

更新版本后,低版本的java代码也能继续跑

ArrayList list = new ArrayList();
相当于
ArrayList<Object> list = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
擦除
ArrayList list = new ArrayList();
Integer i = list.get(0);

运行时获取泛型信息

类文件信息中的 Signature 常量存储泛型信息。

例如:字段中带有泛型信息

public class GenericTest {
    UseGeneric<String> useGeneric = new UseGeneric<>();
}    

javap 查看类文件信息

generic.UseGeneric<java.lang.String> useGeneric;
descriptor: Lgeneric/UseGeneric;
flags: ACC_STATIC
Signature: #26                          // Lgeneric/UseGeneric<Ljava/lang/String;>;

例如:方法的局部变量或参数带有泛型信息, Signature 常量与局部变量的关系存在于 LocalVariableTypeTable 中

public void test(ArrayList<String> list){
    UseGeneric<Integer> useGeneric = new UseGeneric<>();
}

javap 查看类文件信息

 public void test(java.util.ArrayList<java.lang.String>);
    descriptor: (Ljava/util/ArrayList;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #21                 // class generic/UseGeneric
         3: dup
         4: invokespecial #22                 // Method generic/UseGeneric."<init>":()V
         7: astore_2
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lgeneric/GenericTest;
            0       9     1  list   Ljava/util/ArrayList;
            8       1     2 useGeneric   Lgeneric/UseGeneric;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       9     1  list   Ljava/util/ArrayList<Ljava/lang/String;>;
            8       1     2 useGeneric   Lgeneric/UseGeneric<Ljava/lang/Integer;>;
    Signature: #63       // (Ljava/util/ArrayList<Ljava/lang/String;>;)V

运行时获取泛型信息

字段 UseGeneric useGeneric = new UseGeneric<>();

//获取字段 useGeneric 的类型信息 Type 
Type useGeneric = GenericTest.class.getDeclaredField("useGeneric").getGenericType();
//强转为其子类, 参数化类型信息 ParameterizedType
ParameterizedType type = (ParameterizedType) useGeneric;
//调用 ParameterizedType 方法,getActualTypeArguments 获取泛型信息
System.out.println(type.getActualTypeArguments()[0]);

//output
class java.lang.String

方法参数

public void test(ArrayList<String> list, HashMap<String,Thread> map){
    UseGeneric<Integer> useGeneric = new UseGeneric<>();
}
//获取方法信息
Method test = GenericTest.class.getDeclaredMethod("test", ArrayList.class, HashMap.class);
//获取方法参数的泛型信息
for (Type genericParameterType : test.getGenericParameterTypes()) {
    //强转为参数化类型 ParameterizedType
    ParameterizedType pt = (ParameterizedType) genericParameterType;
    for (Type actualTypeArgument : pt.getActualTypeArguments()) {
        System.out.println(actualTypeArgument);
    }
}

//output
class java.lang.String
class java.lang.String
class java.lang.Thread

反射无法获取局部变量信息,也就无法获取局部变量的泛型信息。

`

虽然运行时能通过反射获取到泛型信息,但作用不大

泛型擦除与静态类型

泛型类C中的静态成员不能使用由当前类声明的泛型。

泛型类C 每次被创建并使用时,需要指定一个类型参数。如果静态成员的类型随之变化的话,就违背了 static 的语义 — 与类型绑定,类的所有实例对应唯一的静态成员。

例如以下代码:

public class UseGeneric<T>{
    @CompileError
    private static T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
	
    @ComplieError
    public static T test(){
        ...
    }
    
    @ComplieError
    public static <H extends T> void test1(){
        ...
    }
    
    @ComplieSuccess
    public static <D,J,W> J staticTest(ArrayList<D> list){
        ...
    }

}

静态成员不能使用类型的类型参数。但静态方法可以自定义类型参数。


参考:
老马说编程-泛型

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值