文章目录
第一次接触泛型 完成
使用容器类时
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 usesuper
. If you do both with the same collection, you shouldn’t use eitherextends
orsuper
.
如果当前容器只用来获取元素,容器就相当于一个 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){
...
}
}
静态成员不能使用类型的类型参数。但静态方法可以自定义类型参数。
参考:
老马说编程-泛型