JDK5提出了新特性:泛型。它允许我们在不知道变量类型的情况下,传入类型参数,在设计框架时,我们会大量的使用泛型,因为泛型的特性:动态,上下边界,编译检查等,特别适合架构设计
一、泛型上手
1.类属性使用泛型
定义泛型可以使用除关键字外的任意名字(遵循变量名的规则),使用"<泛型名>"来表示你需要使用泛型参数:
public class Data<T> {
private T data;
}
2.类方法使用泛型
上面的类中,我们可以直接使用泛型:T,来作为非静态方法的入参和返回参数:
public class Data<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
我们也可以直接在方法上定义一个或多个全新的泛型,为了和类定义的T作区别,使用R来定义泛型名:
public class Data<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public <R> R mapperData(T data) {
return (R) data;
}
}
3.泛型使用
实例化对象时,可以使用两种方式使用泛型,定义对象类型时使用和实例化时使用:
Data<String> data = new Data<>();
Data data1 = new Data<String>();
两者有区别:
- 定义对象类型时指定具体类型,那么类中使用到该泛型的地方,都会转变为具体类型,以在编译期就对变量类型安全检查
- 实例化时指定具体类型,没什么用,类中使用该泛型的地方,转变为Object类型
Data<String> data = new Data<>();
Data data1 = new Data<String>();
String data2 = data.getData();
Object data3 = data1.getData();
还可以使用"?"通配符,来表示任意类,其实就是Object:
Data<?> data4 = new Data<>();
Object data5 = data4.getData();
二、类型擦除
Java中的泛型为伪泛型,在编译时,会对泛型进行擦除,子类最终使用桥接来调用相应类的方法,类型擦除会导致在类型转换时报出异常
我们使用List时,指定其泛型为String,但是我们仍然可以利用反射,存入其他类型变量:
List<String> list = new ArrayList<>();
list.add("123");
try {
Method addMethod = list.getClass().getMethod("add", Object.class);
addMethod.invoke(list, 123);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(list.size());
结果:
2
反射获取的是类元信息,说明实际Class字节码中的add方法,并没有真正使用我们指定的泛型
当我们使用这个变量时,就会报错了:
System.out.println(list.get(1));
结果:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at GenericTest.main(GenericTest.java:34)
三、上下边界
先把类型擦除放一边,泛型也可以限定其上下边界,即它需要继承至某个父类,或者是它的某个父类
1.上边界
使用extends关键字:
public class Data<T extends Number> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
这样我们就只能使用Number的子类或Number本身作为泛型了
Data<Integer> data = new Data<>();
2.下边界
使用super关键字,只能在使用泛型时使用:
Class<? super Integer> clz = Number.class;
四、桥接
当我们自定义一个类继承至一个泛型类,并指定其泛型时,父类的方法会被重载:
public class DataImpl extends Data<Integer>{
@Override
public Integer getData() {
return super.getData();
}
@Override
public void setData(Integer data) {
super.setData(data);
}
}
而通过上面的了解,类型擦除会将父类的这两个方法的泛型擦除,在字节码中,使用Object代替。可以看到下面Data类反转义后的内容,其中Field data:Ljava/lang/Object;的注释,表示实际是使用了Object
public T getData();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field data:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LData;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this LData<TT;>;
Signature: #20 // ()TT;
public void setData(T);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field data:Ljava/lang/Object;
5: return
LineNumberTable:
line 9: 0
line 10: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LData;
0 6 1 data Ljava/lang/Object;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 6 0 this LData<TT;>;
0 6 1 data TT;
Signature: #23 // (TT;)V
将子类DataImpl反转义后的内容为:
Last modified 2021-10-5; size 761 bytes
MD5 checksum b30c50b7d1009fcd65b68b48e39aee97
Compiled from "DataImpl.java"
public class DataImpl extends Data<java.lang.Integer>
Signature: #25 // LData<Ljava/lang/Integer;>;
SourceFile: "DataImpl.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#28 // Data."<init>":()V
#2 = Methodref #8.#29 // Data.getData:()Ljava/lang/Object;
#3 = Class #30 // java/lang/Integer
#4 = Methodref #8.#31 // Data.setData:(Ljava/lang/Object;)V
#5 = Methodref #7.#32 // DataImpl.setData:(Ljava/lang/Integer;)V
#6 = Methodref #7.#33 // DataImpl.getData:()Ljava/lang/Integer;
#7 = Class #34 // DataImpl
#8 = Class #35 // Data
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LDataImpl;
#16 = Utf8 getData
#17 = Utf8 ()Ljava/lang/Integer;
#18 = Utf8 setData
#19 = Utf8 (Ljava/lang/Integer;)V
#20 = Utf8 data
#21 = Utf8 Ljava/lang/Integer;
#22 = Utf8 (Ljava/lang/Object;)V
#23 = Utf8 ()Ljava/lang/Object;
#24 = Utf8 Signature
#25 = Utf8 LData<Ljava/lang/Integer;>;
#26 = Utf8 SourceFile
#27 = Utf8 DataImpl.java
#28 = NameAndType #9:#10 // "<init>":()V
#29 = NameAndType #16:#23 // getData:()Ljava/lang/Object;
#30 = Utf8 java/lang/Integer
#31 = NameAndType #18:#22 // setData:(Ljava/lang/Object;)V
#32 = NameAndType #18:#19 // setData:(Ljava/lang/Integer;)V
#33 = NameAndType #16:#17 // getData:()Ljava/lang/Integer;
#34 = Utf8 DataImpl
#35 = Utf8 Data
{
public DataImpl();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method Data."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDataImpl;
public java.lang.Integer getData();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method Data.getData:()Ljava/lang/Object;
4: checkcast #3 // class java/lang/Integer
7: areturn
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this LDataImpl;
public void setData(java.lang.Integer);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #4 // Method Data.setData:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 9: 0
line 10: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LDataImpl;
0 6 1 data Ljava/lang/Integer;
public void setData(java.lang.Object);
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/Integer
5: invokevirtual #5 // Method setData:(Ljava/lang/Integer;)V
8: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LDataImpl;
public java.lang.Object getData();
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #6 // Method getData:()Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDataImpl;
}
以setData方法为例,可以观察发现,含有两个setData方法:
public void setData(java.lang.Integer);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #4 // Method Data.setData:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 9: 0
line 10: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LDataImpl;
0 6 1 data Ljava/lang/Integer;
public void setData(java.lang.Object);
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/Integer
5: invokevirtual #5 // Method setData:(Ljava/lang/Integer;)V
8: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LDataImpl;
入参为Object的setData方法中,会调用checkcast指令,检查类型是否为#3,即是否为Integer类型。然后又执行了invokevirtual #5,查看注释我们也可以知道,最终调用的是入参为Integer类型的setData方法,入参为Object的setData方法就是桥方法,重载只是假象,就是为了解决类型擦除和多态的冲突。这些概念实际就是为了兼容以前没有泛型的JDK版本
网友评论