java 泛型基础
泛型类型:泛型类型是在类型上参数化的泛型类或接口
-
泛型使用场景
- 编译时前类型检查。定义为 Collection
类型的集合,add int时会有编译错误。 - 避免类型强转。如果只是定义 Collection,我们是不知道集合保存的是什么类型的,即便知道,在get之后也需要强制转换成需要的类型。
- 实现通用算法。如通用的二分查找,排序。
- 编译时前类型检查。定义为 Collection
-
泛型类型擦写
- java引入java,一遍在编译时提供更严格的类型检查并支持泛型变成。类型擦除是为了不会为参数化类型创建新的类。因此,泛型不会产生运行时开销。而CGLIB这种动态代理技术,会在运行时修改字节码,会产生一些运行时的消耗。为了实现泛型,编译器将类型擦除应用于:
- 将泛型类型中的所有类型参数替换为其边界,如果类型参数无边界,则将其替换为 Object。因此生成的字节码只包含普通类、接口和方法
- 必要时插入类型转换以保持类型安全
- 生成桥接方法以保留泛型类型中的多态性。
- java引入java,一遍在编译时提供更严格的类型检查并支持泛型变成。类型擦除是为了不会为参数化类型创建新的类。因此,泛型不会产生运行时开销。而CGLIB这种动态代理技术,会在运行时修改字节码,会产生一些运行时的消耗。为了实现泛型,编译器将类型擦除应用于:
以最常用的Collection为例看下泛型的例子
Collection list = new ArrayList<>();
list.add("hello");
// 编译错误
// list.add(1);
// 泛型擦除
Collection temp = list;
// 编译通过
temp.add(1);
System.out.println(list);
// ClassCastException
list.forEach(System.out::println);
从字节码看泛型
Java 泛型是在编译时实现的,jvm是没有泛型的概念的,也就是说泛型是java语言的特性。既然 jvm 没有泛型的特性,要让编译后的 .class 支持 Java 语言的泛型,字节码就是二者之间的桥梁。
public class Generic {
private T data;
public T get() {
return data;
}
public void set(T data) {
this.data = data;
}
}
从生成的字节码中可以看到,泛型 T 已经被擦除了:
()V
getData()Ljava/lang/Object;
setData(Ljava/lang/Object;)V
类型擦除与多态
GenericB 继承 Generic 并重写 getData、setData,从 Generic 的字节码知道参数类型是 Object,而 GenericB 定义的是 Number,正常应该是不能重写的,但是编译和运行时没有错误的。jvm为了解决这个问题,引入了桥接方法。
public class GenericB extends Generic {
private Number n;
@Override
public Number getData() {
return this.n;
}
@Override
public void setData(Number data) {
this.n = data;
}
}
字节码如下,我只举例 getData 方法。编译器自动生成了“java.lang.Object getData()”方法,在方法里调用“java.lang.Number getData()”方法。
public java.lang.Number getData();
descriptor: ()Ljava/lang/Number;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field n:Ljava/lang/Number;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lgeneric/GenericB;
public java.lang.Object getData();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method getData:()Ljava/lang/Number;
4: areturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lgeneric/GenericB;
再来看下没有写泛型的继承是如何实现的,GenericC继承自 Generic ,但是没有指定泛型类型。
public class GenericC extends Generic {
private Number n;
@Override
public Number getData() {
return this.n;
}
public void setData(Number data) {
this.n = data;
}
}
生成的字节码如下。父类中的方法为 getData()Object 和 setData(Object),GenericC 类中的 setData(Number) 与父类 set(Object) 方法参数不同,所以是是重载。我们知道,只有返回值不同不满足重载条件,所以对 GenericC 类的 getData()Number 方法来说,应该算是对父类方法 getData()T 的重写。
编译期自动生成了 getDate() Object 桥接方法来重写父类方法。我们发现字节码里存在了两个只有返回值类型不同的同名方法,不符合java语法。这是因为java和jvm方法签名定义不同:
- Java 方法签名 = 方法名 + 参数类型 + 参数顺序
- JVM 方法签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型 + 可能抛出的异常
在java反射的api里,Method有一个方法是 isBridge 就是用来判断是否桥接方法的。
{
private java.lang.Number n;
descriptor: Ljava/lang/Number;
flags: ACC_PRIVATE
public generic.GenericC();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method generic/Generic."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lgeneric/GenericC;
public java.lang.Number getData();
descriptor: ()Ljava/lang/Number;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field n:Ljava/lang/Number;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lgeneric/GenericC;
public void setData(java.lang.Number);
descriptor: (Ljava/lang/Number;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field n:Ljava/lang/Number;
5: return
LineNumberTable:
line 13: 0
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lgeneric/GenericC;
0 6 1 data Ljava/lang/Number;
public java.lang.Object getData();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #3 // Method getData:()Ljava/lang/Number;
4: areturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lgeneric/GenericC;
}
java 5 类型接口
- java 5 类型接口 - java.lang.reflect.Type
类或接口 | 说明 |
---|---|
java.lang.Class | Java 类API,如java.lang.Integer |
java.lang.reflect.GenericArrayType | 泛型数组类型,比如 T[] |
java.lang.reflect.ParameterizedType | 泛型参数类型 |
java.lang.reflect.TypeVariable | 泛型类型变量,如Collection |
java.lang.reflect.WildcardType | 泛型通配类型 |
- java 泛型反射API
类型 | API |
---|---|
泛型信息 Generics Info | java.lang.Class#getGenericInfo() |
泛型参数 Parameters | java.lang.reflect.ParameterizedType |
泛型父类 super classes | java.lang.Class#getGenericSuperclasses() |
泛型接口 interfaces | java.lang.Class#getGenericInterfaces() |
泛型生命 generics Declaration | java.lang.reflect.GenericDeclaration |
// 原生类型 primitive types : int long float
Class intClass = int.class;
System.out.println("intClass = " + intClass);
// 数组类型 array types : int[],Object[]
Class objectArrayClass = Object[].class;
System.out.println("objectArrayClass = " + objectArrayClass);
// 原始类型 raw types : java.lang.Integer
Class IntegerRawClass = Integer.class;
System.out.println("IntegerRawClass = " + IntegerRawClass);
System.out.println("ArrayList.class = " + ArrayList.class);
// 泛型参数类型 parameterized type
ParameterizedType arrayListParameterizedType = (ParameterizedType) ArrayList.class
.getGenericSuperclass();
System.out.println("ArrayListParameterizedType = " + arrayListParameterizedType);
System.out.println(
"arrayListParameterizedType.getRawType() = " + arrayListParameterizedType.getRawType());
System.out.println("泛型类型变量 Type Variable");
//
Type[] typeVariables = arrayListParameterizedType.getActualTypeArguments();
Stream.of(typeVariables)
.map(TypeVariable.class::cast) // Type -> TypeVariable
.forEach(System.out::println);
spring 4.2 泛型优化实现与局限性- ResolvableType
- 核心 API org.springframework.core.ResolvableType
- 工厂方法:forXX 方法
- 转换方法:asXX 方法
- 处理方法:resolveXX 方法
- GenericTypeResolver, GenericCollectionTypeResolver 替代者
ResolvableType 是一个 immutable 设计,通过forXX, asXX 返回一个新的 ResolvableType 实例,然后通过 resolveXX,getXX 等方法获取类型信息。
ResolvableType 设计的优点
- 简化Type API 开发;
- 不变设计,线程安全
- Fluent API 设计,builder模式,链式编程。
demo:
private HashMap> myMap;
public static void main(String[] args) throws NoSuchFieldException {
ResolvableType t = ResolvableType.forField(ResolvableTypeDemo.class.getDeclaredField("myMap"));
t.getSuperType(); // AbstractMap>
t.asMap(); // Map>
t.getGeneric(0).resolve(); // Integer
t.getGeneric(1).resolve(); // List
t.getGeneric(1); // List
t.resolveGeneric(1, 0); // String
t.resolveGeneric(1, 1); // null
}
一个案例
举一个例子,我们在定义一个支持泛型接口时,可以用不同的泛型类实现,当注入的时候指定具体的泛型类,spring 就会把具体类型的实现注入到 bean 中,这个过程就涉及到泛型的处理。代码如下:
spring 版本 5.2.7。
/**
* 泛型注入 demo
*/
public class GenericIocDemo {
@Bean
public UserMonitor userMonitor() {
return new UserMonitor();
}
@Bean
public PersonMonitor personMonitor() {
return new PersonMonitor();
}
private List> users;
@Autowired
public void setUsers(List> users) {
this.users = users;
}
@PostConstruct
public void init() {
// 注入的对象是 userMonitor
users.forEach(System.out::println);
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(GenericIocDemo.class);
context.refresh();
context.close();
}
public static class UserMonitor implements Consumer {
@Override
public void accept(User user) {
}
}
public static class PersonMonitor implements Consumer {
@Override
public void accept(Person user) {
}
}
}
demo 中注入 private List
通过 descriptor 的 ResolvableType 只获取了集合泛型,并没有获取泛型的泛型。这是为了在下一步获取候选 bean 做准备。因为 spring 不能通过泛型类型获取 bean,所以这里获取到 Consumer即可。
因为 requiredType 是 Consumer 接口,所以获取到两个候选 bean。
查看这部分代码的时候参照调用栈。第一行获取 实际的泛型类型,方法的最后会用来判断候选对象是否匹配(包括泛型)。
在比较之前,会获取候选 bean 的类型。descriptor.getDependencyType() 获取没有泛型的类型,然后和实际类型比较是否匹配,如果匹配则返回。
- 比较类型是否匹配,包括泛型
- 因为泛型不匹配。所以结果是false,不会添加注入的集合中。
总结
简单介绍了java语言和jvm对泛型的支持方式。
java 的泛型擦写是在运行时。其实从字节码的角度看并不存在擦写,因为编译后全部都是Object,擦写描述的其实是java代码编写的不规范,欺骗了编译器,从而导致运行时错误。
spring框架封装的 ResolvableType 实现原理。ResolvableType 可以在我们的项目中直接使用,api设计的也很友好。
使用框架,了解原理,事半功倍。
参考
- The Java™ Tutorials: generics
- Java Generics - Classes