一、概述
泛型是java1.5引入的概念,用于规范用户输入类型,泛型使用的一个误区是将泛型当做类型或变量看待,从而尝试例如 T.class,new T() 等操作,实际上泛型只是一个契约,约定了在本类里用到相同泛型的地方类型一致,使用时,跨类之间契约可传递。
泛型的好处:
1. 实现java类型安全,相当于用户提供泛型信息,编译器帮用户做类型校验,减少用户错误编码造成的类型异常。
2. 使用泛型隐含类型转换,用户无需关心,代码简洁
例如:
void function() {
A<String> a = new A<>();
String b = a.getA();
}
public class A<T> {
private T a;
public void setA(T a) {
this.a = a;
}
public T getA() {
return (T) a;
}
}
从反编译后的字节码中看到,getA()方法返回结果赋值给b之前有一个字节码 checkcast,checkcast字节码相当于:
if(!(obj == null || obj instanceof <class>)) {
throw new ClassCastException();
}
这就是为什么使用泛型时可能出现 ClassCastException 的原因。
3. 带有上边界的泛型能预支用户方法,当然这里不使用泛型而是用面向接口编程的思路也可以。
例如:
private class Student<T extends Service> {
private T genericity;
public Student(T genericity) {
this.genericity = genericity;
}
public void say() {
System.out.println(genericity.sayHello());
}
}
private interface Service {
public String sayHello();
}
二、通配符、supser、extends、逆变、协变
package com.learn.genericity;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
/*
* 本例用于阐述泛型的协变、逆变和不变
* 协变表示接受本类及所有子类,逆变表示接受本类及所有父类,不变表示只接受本类
*/
public class SuperExtendsTest {
/*
* 数组是协变的,Object[]类型的引用能指向Integer[]类型的对象
* 常规java类型也是协变的
*/
@Test
public void test() {
Object[] objects = new Integer[10];
System.out.println(objects);
}
/*
* 这边会报错,因为常规泛型是不变的,也即只支持本类
*/
@Test
public void test1() {
// List<Object> list = new ArrayList<Integer>();
}
/*
* extends修饰的泛型会有协变效果,Integer是Object的子类,因此 List<? extends Object> 能指向 ArrayList<Integer>
*/
@Test
public void test2() {
List<? extends Object> list = new ArrayList<Integer>();
System.out.println(list);
}
/*
* super修饰的泛型有逆变的效果,Integer是Object的子类,因此 List<? super Integer> 能指向 ArrayList<Object>
*/
@Test
public void test3() {
List<? super Integer> list = new ArrayList<Object>();
System.out.println(list);
}
/*
* 使用形参存入数据时用协变,通过形参取出数据时用逆变
* putData方法是将datas中的数据拿出来放在'本地'
* getData方法是将'本地'的数据存入形参Datas
* Collections.copy(List<? super T> dest, List<? extends T> src)很好的阐述了这个概念,该方法将src的内容读出存放在dest里,因此要求src协变,dest逆变
*/
public void putData(List<? extends Object> datas) {
System.out.println(datas);
}
public void getData(List<? super Number> datas) {
System.out.println(datas);
}
}
三、泛型方法
泛型不仅能定义在类上,也能定义在方法上。
package com.learn.genericity;
import java.util.ArrayList;
import java.util.List;
/*
* 1. 泛型方法是一种特殊的泛型使用方式,泛型方法无需定义在泛型类中,如本例所示
* 2. 泛型方法能形成多个入参和返回值之前类型联动
* 3. 泛型方法帮用户实现潜在的类型检查和转化
* 4. 泛型和变量一样存在作用域,类上的泛型,作用域是整个类,方法上的泛型,作用域是整个方法,如果方法泛型同作用域泛型名称相同,方法泛型覆盖类泛型
* 5. 另外泛型方法可以在静态方法上使用(类泛型不能在静态方法上生效)
*/
public class GenericFunctionTest {
public static void main(String[] args) throws Exception {
A a = new A();
GenericFunctionTest instance = a.getInstance(GenericFunctionTest.class);
System.out.println(instance);
List<String> list = new ArrayList<>();
System.out.println(a.listSize(list));
System.out.println(a.listSize1(list));
List<Integer> list1 = new ArrayList<>();
a.say(list1);
a.say2(list1, 1);
}
public static class A {
/*
* <T> 是泛型方法的标志,同时声明了方法中被用到的泛型T,必须存在
*/
public <T> T getInstance(Class<T> class1) throws Exception {
return class1.newInstance();
}
public <T extends Number> int Transform(T number) {
return number.intValue();
}
/*
* 泛型方法和通配符泛型有相似的使用场景,都能解决适配多种输入的问题
*/
public int listSize(List<?> list) {
return list.size();
}
public <T> int listSize1(List<T> list) {
return list.size();
}
/*
* 但通配符方式定义的集合有只读的特点(extend修饰),而泛型方法定义的集合在一些场景下可写
* extend通配符的特点是只读,super特点是只可写,这一点在<T> Collections.copy(List<? super T> dest, List<? extends T> src)方法中有很好的体现 其中dest对象可读写,src对象只读
*/
public void say(List<? extends Integer> list) {
// list.add(new Integer(1)); // 会出错,list的实际泛型类型不清楚,因此写入会报错
}
/*
* 泛型方法中,形参集合可写,但也仅限于写入同样被泛型约束的形参
*/
public <T extends Number> void say11(List<T> list, T a) {
list.add(a); // 不会出错
}
/*
* 另外如果多个形参以及返回值之间存在类型依赖,就只能用泛型方法 本例返回值、两个形参都共用了泛型T,这种场景不能用通配符解决
*/
public <T extends Number> T say2(List<T> list, T a) {
list.add(a);
return a;
}
/*
* 统配符方式无法联动形参和返回值,用户调用say21方法时,能传入List<Integer>类型的形参,但返回float类型的值
*/
public Number say21(List<? extends Number> list, Number a) {
return 1.1;
}
/*
* 泛型方法可以作用在静态方法上,约束用户输入并在多个形参和返回值之间产生类型联动
*/
public static <T> T say3(List<T> list, T t) {
list.add(t);
return t;
}
}
}
四、泛型擦除和泛型获取
泛型在java中的对象用
java.lang.reflect.Type
表示,测试用例如下:
package com.learn.reflex;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;
/*
* java.lang.reflect.Type 类型是Class类型的父类,但Class源自java1.0,而Type来自java1.5
* Type的出现主要是为了兼容泛型,因为泛型并不是具体的类型,因此Class无法表示
* spring 提供的 ResolvableType 类型是Type的增强版,使用更加方便,详情见 com.learn.spring.source.ResolvableTypeTest
*
* TypeVariable类型会携带继承自GenericDeclaration的泛型,GenericDeclaration接口的子类型有 Class, Constructor, Executable, Method,
* GenericDeclaration 表示声明该泛型的类型
*/
public class TypeTest<T extends Number> {
// TypeVariable:常规泛型
public T a;
// GenericArrayType:泛型数组
public T[] b;
// 常规的class,数组类型
public Integer[] b1;
// ParameterizedType:以泛型做参数的对象
public List<T> c;
// ParameterizedType内嵌WildcardType:通配符类型
public List<? extends T> d;
public static void main(String[] args) throws Exception {
Field field = TypeTest.class.getDeclaredField("a");
Type type = field.getGenericType();
// getGenericDeclaration 返回该泛型在哪里被定义
System.out.println(((TypeVariable<?>) type).getGenericDeclaration());
System.out.println(field.getName() + ":" + type.getClass());
field = TypeTest.class.getDeclaredField("b");
type = field.getGenericType();
System.out.println(field.getName() + ":" + type.getClass());
field = TypeTest.class.getDeclaredField("b1");
type = field.getGenericType();
System.out.println(field.getName() + ":" + type.getClass());
field = TypeTest.class.getDeclaredField("c");
type = field.getGenericType();
System.out.println(field.getName() + ":" + type.getClass());
field = TypeTest.class.getDeclaredField("d");
type = field.getGenericType();
System.out.println(field.getName() + ":" + ((ParameterizedType) type).getActualTypeArguments()[0].getClass());
// 泛型声明来自方法(泛型方法)
Method method = TypeTest.class.getDeclaredMethod("function", Object.class);
TypeVariable<Method>[] params = method.getTypeParameters();
for (TypeVariable<Method> param : params) {
System.out.println(param.getGenericDeclaration());
System.out.println(param.getClass());
}
}
public <E> void function(E e) {
}
}
一些泛型会被擦除,另一些会被保留。哪些擦除哪些保留,为什么会这样,如何获取未被擦除的泛型?
package com.learn.genericity;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;
/*
* 在一些条件下,获取泛型
* 泛型为什么要擦除,哪些泛型会被擦除,两个问题可以被归结为一个问题,泛型信息在编译后应该放在哪里。
* 对于声明模糊泛型的类,类对象中不方便包含实际泛型类型,因为如果包含,则必然导致一个类在虚拟机中有多个类对象(每个类对象挂一种泛型,例如List<String>, List<Integer>),
* 这样java虚拟机运行负载会变重,因此类上的模糊泛型(例如 T,V extends Number)会被擦除到上边界,因为上边界必然被同一类不同泛型实现满足,所以可以放在类对象中.
* 类自身的定义上只能有模糊泛型(例如 A<T>,从来没有A<String>),但类的父类或父接口可以有明确泛型(例如 A<T> extends ArrayList<String>),对于父类有明确泛型的情况,
* 对类自身而言,父类的泛型信息是明确的,不会再改变,因此可以将父类泛型信息记录在本类里,java提供 getGenericSuperclass 方法让用户获取。当然父类也不是一定要明确泛型,父类不明确泛型的情况下,子类也无法获得。
* 对于类成员变量Field而言,如果Field上有明确泛型信息,能被记录到字节码,反射过程中表现在Field对象上,原因同上(明确不可变)。
* 对于类的构造函数入参和方法的入参和返回值而言,如果有明确的泛型信息,能被记录到字节码,反射过程中表现在Method对象上。
* 局部变量声明的泛型信息不会被记录,为什么成员变量可以局部变量却不行。这边我们做一个假设,如果将局部变量泛型信息编译到字节码里,例如在方法的字节码中,我们标定一个局部变量泛型关联到一种明确类型。
* 1. 一个方法中可以有多个局部变量,局部变量的泛型信息通常不会被使用到。
* 2. 成员变量泛型信息记录在java字节码字段表集合对应字段的属性表里,作为类信息的一部分;局部变量创建出来后存放在堆栈里,本身只是一个指向实际对象的引用(四字节,八字节),引用本身和引用指向的对象不含泛型信息。
* 3. 局部变量通常是私有的,现阶段的反射方法无法获取局部变量,因此要获取局部变量泛型信息也就无从入手。
* 4. 归根结底,局部变量是对象,而泛型信息要从类型中获取。成员变量是所属类信息的一部分,而局部变量只是运行过程中的一个对象。
*/
public class GetGenericTest {
@SuppressWarnings({ "unused", "serial" })
public static void main(String[] args) throws Exception {
/*
* getTypeParameters用于获取类上泛型
* 获取A和B的泛型信息,基于泛型擦除的机制,A的泛型T会被擦除成Object,B的泛型T会被擦除到上界Number(反射或反编译可以证明)
*/
TypeVariable<?>[] typeVariables = A.class.getTypeParameters();
for (TypeVariable<?> typeVariable : typeVariables) {
System.out.println(typeVariable.getClass());
Type[] bounds = typeVariable.getBounds();
for (Type type : bounds) {
System.out.println("A:" + type);
}
}
typeVariables = B.class.getTypeParameters();
for (TypeVariable<?> typeVariable : typeVariables) {
System.out.println(typeVariable.getClass());
Type[] bounds = typeVariable.getBounds();
for (Type type : bounds) {
System.out.println("B:" + type);
}
}
/*
* 这里a对象的泛型Integer拿不到,会被擦除,编译结果不含泛型Integer的信息。获取a对象的泛型,实际上通过A类来实现,但对A类来说并无泛型的分别。
*/
A<Integer> a = new A<>();
/*
* 局部变量的泛型信息不能获得,但成员变量的泛型信息A.a可以。
* 我个人的理解是,泛型擦除主要为了避免增加类型数量。对A.class来说,A.a的泛型一定是Integer,这是一个确定信息,可以被记录在类信息里。
* B.b不行(能拿到bound),因为B.b并没有明确表明泛型类型,这个模糊泛型可能有很多结果,因此无法在类中记录一个准确信息。
*/
Field field = A.class.getDeclaredField("a");
System.out.println("field a:" + ((ParameterizedType) (field.getGenericType())).getActualTypeArguments()[0].getTypeName());
field = B.class.getDeclaredField("b");
System.out.println("field b:" + ((TypeVariable<?>) (((ParameterizedType) (field.getGenericType())).getActualTypeArguments()[0])).getBounds()[0].getTypeName());
/*
* 方法入参和返回类型中的泛型信息可以被获取(无泛型入参返回 Class 对象)
*/
Method method = A.class.getDeclaredMethod("say", List.class);
for (Type type : method.getGenericParameterTypes()) {
System.out.println("method param:" + ((ParameterizedType) type).getActualTypeArguments()[0]);
}
System.out.println("method return:" + ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0]);
/*
* 构造函数入参类型能被获取
*/
Constructor<?> constructor = A.class.getConstructor(List.class);
for (Type type : constructor.getGenericParameterTypes()) {
System.out.println("construct param:" + ((ParameterizedType) type).getActualTypeArguments()[0]);
}
/*
* 父类有明确泛型时,该泛型信息能被获取
*/
System.out.println("GenericSuperclass:" + ((ParameterizedType) (A.class.getGenericSuperclass())).getActualTypeArguments()[0]);
/*
* 这里a1对象的泛型可以拿到,因为 'new A<Integer>() {}' 实际上构建了一个匿名内部类,该类以A<Integer>作为父类,通过javap反编译从字节码可以看到,父类泛型信息包含在字节码中
* 子类显示继承的父类泛型信息会被子类保留,究其原因,是因为显式指定后已经是明确的信息,不会有其他变化,且内部类相对与父类已经是一个全新的类,也没必要擦除泛型减少类数量
* 一些开源框架就是基于父类泛型声明来动态获取泛型信息(例如fastjson对map等集合类型做反序列化,需要先获取kv的泛型类型,才能进一步对kv做反序列化,参见 JSON.parseObject(String text, TypeReference<T> type, Feature... features))
*/
A<Integer> a1 = new A<Integer>() {};
Type[] types = ((ParameterizedType) (a1.getClass().getGenericSuperclass())).getActualTypeArguments();
for (Type type : types) {
System.out.println("a1:" + type);
}
}
@SuppressWarnings("all")
private static class A<T> extends ArrayList<String> {
public A<Integer> a;
public A() {
}
public A(List<Number> list) {
}
public List<Double> say(List<String> list) {
return null;
}
}
@SuppressWarnings("all")
private static class B<T extends Number & Comparable<T>> {
public B<T> b;
}
}
五、泛型和多态的冲突
泛型可能造成多态问题,java使用一些小技巧规避了这些问题。
package com.learn.genericity;
import java.lang.reflect.Method;
/**
* 泛型的桥接,解决多态问题
* 首先明确一下多态,当子类覆盖父类方法时,方法使用时实际调用对象方法,而不是声明方法(例如 A a = new B(); a.set(x); 实际调用B的set方法)
* 未被桥接的泛型可能破掉多态(桥接是java自身的一个技巧,无需用户特别编写)
* 1. 如本例所示,A.set(T t)方法会被编译成 A.set(Object t)
* 2. B继承A,并声明A的泛型类型是Integer,B实现set方法,且B.set(Integer t)方法上有@Override标签,表现的像是B.set覆盖了父类set方法。
* 3. 但基于第一条,A并没有A.set(Integer t)方法,那么B @Override 标签表示的覆盖,覆盖的是谁?覆盖只是语法糖。
* 4. 覆盖不是这次的主要问题,看看可能出现的多态问题。由于B继承自A,因此B对象也继承了A.set(Object o)方法,那么 A a = new B(); a.set(1.1); 是不是就不是调用B.set而是A.set
* 5. a.set(1.1); 没有被编译器报错,因为确实有set(Object t)方法,如果这时还调用到A.set上,多态的效果就被破除了(因为在用户的角度 B.set 已经 @Override 了)
* 6. 解决这个问题的方式是,编译器自动帮用户方法做桥接,桥接的方式有两个方式查看
* 7. 反射获取B类的所有方法,发现B自己生成了一个set(Object t)方法
* 8. 用jad反编译后可以看到B类中多了这样一个方法
* public volatile void set(Object obj)
* {
* set((Integer)obj);
* }
* 9. 编译器在B类中实现了set(Object t)方法,强行覆盖掉父类A的set方法,并在方法实现中回调B.set(Integer t),实现中做了强制类型转化。这也是为什么测试用例里抛出类型转化错误的原因(题外话,泛型的警告也很重要)
*/
public class GenericBridgeTest {
public static void main(String[] args) {
A a = new B();
// 这一句会抛出cast异常
a.set(1.1);
for (Method method : B.class.getDeclaredMethods()) {
if ("set".equals(method.getName())) {
System.out.println(method);
}
}
}
public static class A<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
System.out.println("a");
}
}
public static class B extends A<Integer> {
private Integer t1;
@Override
public Integer get() {
return t1;
}
@Override
public void set(Integer t) {
System.out.println("b");
}
}
}