java泛型小结

一、概述
泛型是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");
		}
	}
	
}


猜你喜欢

转载自blog.csdn.net/yzb808/article/details/80751433
今日推荐