在JDK5之前,还没有泛型。在使用集合时,需要构建一个元素类型为Object的集合,集合能够存储任意的数据类型对象,在使用该集合的过程中,需要明确知道存储每个元素的数据类型,否则很容易引发ClassCastException异常。
JDK5中引入Java泛型这个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。
在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
参数化类型
泛型的本质就是参数化类型。在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。
参数化类型,顾名思义就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
简单理解:泛型就是把类型当作是参数一样进行传递,数据类型只能是引用类型。
泛型的使用需要先声明,声明通过<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。
一般约定的类型符号:
常见声明方式:
需要注意:通配符?不能在泛型类(接口)的声明上使用。
Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。
官方描述:
泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。
为了实现泛型,Java编译器将类型擦除应用于:
类型擦除确保了不会为参数化类型创建新类;所以泛型是没有运行时开销的。
对官方描述的理解:
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());
}
}
打印结果为true,因为list1和list2的Class对象是同一个Class。
特别注意:对于编译后生成的字节码class文件不包含泛型类型信息这句话,网上普遍都说这个说法,但都没去仔细解释,理解的时候可能会有不解,因为你发现现实中的class文件中时是包含了泛型类型信息的。下文会解释这个问题。
桥接方法是JDK1.5引入泛型后,为使java泛型方法生成的字节码与JDK1.5版本之前的字节码兼容由编译器自动生成的。
子类继承父类(实现接口)实现泛型方法的情况,父类经过编译后方法的泛型类型入参和返回值类型都为Object(或上边界类型),而子类的实现方法的入参和返回值类型为具体的泛型类型(如String),此时子类并没有重写父类的方法了(返回值和形参与父类完全相同才是重写方法),所以需要编译器生成一个桥接方法达到重写父类方法的目的。
因此当子类继承父类(实现接口)实现泛型方法的时候,编译器会为子类自动生成桥接方法。
举例说明:
public interface TestInterface<T> {
T get();
void set(T t);
}
class Test3 implements TestInterface<Integer> {
@Override
public Integer get() {
return null;
}
@Override
public void set(Integer s) {
}
}
class MainClass {
public static void main(String[] args) {
Method[] methods = Test3.class.getDeclaredMethods();
for (Method method : methods) {
System.out.println((method.isBridge() ? "桥接方法:" : "普通方法:") + method.toGenericString());
}
}
}
打印结果:
桥接方法:public java.lang.Object com.joker.test.generic.Test3.get()
普通方法:public java.lang.Integer com.joker.test.generic.Test3.get()
桥接方法:public void com.joker.test.generic.Test3.set(java.lang.Object)
普通方法:public void com.joker.test.generic.Test3.set(java.lang.Integer)
以set方法作分析:
为什么Java不像C#一样实现真正的泛型呢?而要用类型擦除的方式实现了个伪泛型。
其实JDK1.5引入的泛型采用类型擦除式实现的根本原因是兼容性上的取舍,而不是因为实现不了真正意义上的泛型。
为了确保JDK1.5之前的和JDK1.5能使用同一个类加载器,所以Java通过类型擦除的方式实现的泛型支持。经过编译阶段的泛型类型擦除后,与JDK1.5之前是基本没有变动。
下面举例说明:
定义一个Demo泛型类和一个Test测试类。
public class Demo<T> {
private T id;
public T getId() {
return id;
}
}
public class Test {
public static void main(String[] args) {
Demo<Integer> demo = new Demo<>();
Integer id = demo.getId();
}
}
1. 查看IDEA编译后的class文件
Demo.calss
public class Demo<T> {
private T id;
public Demo() {
}
public T getId() {
return this.id;
}
}
Test.class
public class Test {
public Test() {
}
public static void main(String[] args) {
Demo<Integer> demo = new Demo();
Integer id = (Integer)demo.getId();
}
}
从class文件可见:
原因:在编译过程中,泛型信息是被擦除了,但是声明侧的泛型信息会被class文件以Signature的形式保留在Class文件的Constant pool中。
2. 使用javap命令反编译Demo.class文件和Test.class文件
javap -v Demo.class
Classfile /D:/work/my/springboot/target/classes/com/joker/test/generic/Demo.class
Last modified 2023-2-14; size 610 bytes
MD5 checksum 4851ec541c05f1d29bee93edb79085f0
Compiled from "Demo.java"
public class com.joker.test.generic.Demo<T extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#24 // java/lang/Object."":()V
#2 = Fieldref #3.#25 // com/joker/test/generic/Demo.id:Ljava/lang/Object;
#3 = Class #26 // com/joker/test/generic/Demo
#4 = Class #27 // java/lang/Object
#5 = Utf8 id
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 Signature
#8 = Utf8 TT;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/joker/test/generic/Demo;
#16 = Utf8 LocalVariableTypeTable
#17 = Utf8 Lcom/joker/test/generic/Demo<TT;>;
#18 = Utf8 getId
#19 = Utf8 ()Ljava/lang/Object;
#20 = Utf8 ()TT;
#21 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#22 = Utf8 SourceFile
#23 = Utf8 Demo.java
#24 = NameAndType #9:#10 // "":()V
#25 = NameAndType #5:#6 // id:Ljava/lang/Object;
#26 = Utf8 com/joker/test/generic/Demo
#27 = Utf8 java/lang/Object
{
public com.joker.test.generic.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/joker/test/generic/Demo;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this Lcom/joker/test/generic/Demo<TT;>;
public T getId();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field id:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/joker/test/generic/Demo;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this Lcom/joker/test/generic/Demo<TT;>;
Signature: #20 // ()TT;
}
Signature: #21 // Ljava/lang/Object;
SourceFile: "Demo.java"
javap -v Test.class
Classfile /D:/work/my/springboot/target/classes/com/joker/test/generic/Test.class
Last modified 2023-2-14; size 739 bytes
MD5 checksum a82bd374c2bff99147acd130f3819415
Compiled from "Test.java"
public class com.joker.test.generic.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#28 // java/lang/Object."":()V
#2 = Class #29 // com/joker/test/generic/Demo
#3 = Methodref #2.#28 // com/joker/test/generic/Demo."":()V
#4 = Methodref #2.#30 // com/joker/test/generic/Demo.getId:()Ljava/lang/Object;
#5 = Class #31 // java/lang/Integer
#6 = Class #32 // com/joker/test/generic/Test
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/joker/test/generic/Test;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 demo
#20 = Utf8 Lcom/joker/test/generic/Demo;
#21 = Utf8 id
#22 = Utf8 Ljava/lang/Integer;
#23 = Utf8 LocalVariableTypeTable
#24 = Utf8 Lcom/joker/test/generic/Demo<Ljava/lang/Integer;>;
#25 = Utf8 MethodParameters
#26 = Utf8 SourceFile
#27 = Utf8 Test.java
#28 = NameAndType #8:#9 // "":()V
#29 = Utf8 com/joker/test/generic/Demo
#30 = NameAndType #34:#35 // getId:()Ljava/lang/Object;
#31 = Utf8 java/lang/Integer
#32 = Utf8 com/joker/test/generic/Test
#33 = Utf8 java/lang/Object
#34 = Utf8 getId
#35 = Utf8 ()Ljava/lang/Object;
{
public com.joker.test.generic.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/joker/test/generic/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/joker/test/generic/Demo
3: dup
4: invokespecial #3 // Method com/joker/test/generic/Demo."":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method com/joker/test/generic/Demo.getId:()Ljava/lang/Object;
12: checkcast #5 // class java/lang/Integer
15: astore_2
16: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 args [Ljava/lang/String;
8 9 1 demo Lcom/joker/test/generic/Demo;
16 1 2 id Ljava/lang/Integer;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 9 1 demo Lcom/joker/test/generic/Demo<Ljava/lang/Integer;>;
MethodParameters:
Name Flags
args
}
SourceFile: "Test.java"
由反编译的文件可知:
对于Java中泛型类型的获取可参考:Java中如何获取泛型类型信息
泛型类型用于类的定义中,被称为泛型类。用户在使用该类的时候,才把类型明确下来。
在类上定义的泛型,在实例方法中可以直接使用,不需要定义,但不能在静态方法中使用。
原因:Java中泛型只是一个占位符,必须在传递类型后才能使用。类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了。
语法:
public class 类名<泛型表示符号> {
}
示例:
public class Test<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
如果使用时没指定具体的泛型类型,则泛型类型为Object。
public static void main(String[] args) {
Test<String> test1 = new Test<>();
String data1 = test1.getData();
Test test2 = new Test();
Object data2 = test2.getData();
}
泛型接口和泛型类的声明方式一致。泛型接口的具体类型需要在实现类中进行声明。
语法:
public interface 接口名<泛型标识符号> {
}
示例:
public interface TestInterface<T> {
public T get();
public void set(T t);
}
public class Test2 implements TestInterface<String> {
@Override
public String get() {
return null;
}
@Override
public void set(String s) {
}
}
如果实现类未指定具体的泛型类型,则泛型类型为Object。
public class Test2 implements TestInterface{
@Override
public Object get() {
return null;
}
@Override
public void set(Object s) {
}
}
泛型类型声明在方法上,叫做泛型方法。
需要注意:只是在方法中使用类定义的泛型,该方法不是泛型方法。
语法:
public <泛型表示符号> void 方法名(泛型表示符号 参数名){
}
public <泛型表示符号> 泛型表示符号 方法名(泛型表示符号 参数名){
}
等......
示例:
public class Test {
public static <T> void aaa(T t) {
}
public <T> void bbb(T t) {
}
}
泛型类型必须为指定类型的子类型。
格式: extends 类>
示例:
public class Test {
public void aaa(List<? extends Number> t) {
Number number = t.get(0);
}
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
Test test = new Test();
test.aaa(list);
}
}
泛型类型必须为指定类型的父类型。
格式: super 类>
示例:
public class Test {
public void aaa(List<? super Number> t) {
Object object = t.get(0);
}
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add(1);
Test test = new Test();
test.aaa(list);
}
}
使用方式:
// 泛型类上申明,约束泛型类变量
class WildcardTypeT<T extends Comparable<T> & List<T> & Serializable> {
}
// 方法上申明
public <R extends Enum<R> & Serializable> List<R> parse2Enums(){}
https://blog.csdn.net/tianzhonghaoqing/article/details/119705014