Java学习笔记-泛型

本文详细介绍了Java中的泛型,包括泛型的概念、使用泛型的好处、参数类型化、泛型类的本质,特别是重点讨论了类型擦除机制,解释了Java编译器如何擦除泛型信息,以及由此带来的影响,如桥接方法的生成、泛型与反射的关系,最后探讨了泛型在继承、子类型以及PECS原则中的应用。

一、泛型

1. 什么是泛型?

  • Java泛型 generics 是JDK5中引入的 一种参数化

2. 为什么使用泛型,使用它的好处?

  • 更健壮(只要编译器没有警告,那么运行期就不会出现ClassCastException)
  • 更简洁,不用类型强转
  • 更灵活,复用
// List.java 中排序对泛型的使用
default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

参数类型化

把数据当参数一样传递

​ <数据类型>只能是引用类型(泛型的副作用)

比如:

  • Plate<T>中的”T“成为类型参数
  • Plate<Apple>中的”Apple“成为实际类型参数
  • ”Plate<T>“ 整个成为泛型类型
  • ”Plate<Apple>“整个称为参数化的类型 ParameterizedType

泛型类的本质

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jVup81wG-1592823093440)(https://chitanda-blog.oss-cn-beijing.aliyuncs.com/img/image-20200620234539735.png)]

运行结果为true,由此可见运行时获取的类信息是完全一致的,泛型类型被擦除了,只留下原始类型——ArrayList

类型擦除-Type Erasure

Java语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型,Java编译器将类型擦除应用于:

  • 如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界(上下限)或 Object 。因
    此,产生的字节码仅包含普通的类,接口和方法。
  • 必要时插入类型转换,以保持类型安全。
  • 生成桥接方法以在扩展的泛型类型中保留多态。

类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

泛型类型的擦除-Erasure of Generic Types

在类型擦除过程中,Java编译器将擦除所有类型参数,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object:

class A<T> {
	private T data;

	public A(T data) {
		this.data = data;
	}

	public T getData() {
		return data;
	}
}

通过查看A.class以及字节码可发现Java编译器将参数类型T替换为Object:

image-20200620213715031

B类使用限定类型参数:

public class B<T extends Comparable<T>> {
	private T data;

	public B(T data) {
		this.data = data;
	}

	public T getData() {
		return data;
	}
}

Java编译器将绑定类型参数T替换为第一个绑定类 Comparable:

桥接方法

在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,Java编译器可能需要创建一个称为桥接方法的综合方法.

举个例子:

interface Box<T> {
	void set(T t);
}

class MyBox<T extends Comparable<T>> implements Box<T> {
	private T data;

	public MyBox() {

	}

	@Override
	public void set(T t) {
		data = t;
		System.out.println(data);
	}
}

查看MyBox类字节码可以发现不仅有一个set方法,还有一个桥接方法

image-20200620223509421 image-20200620223913910

类型擦除后,方法签名不一致.MyBox的方法变为set(Comparable),Box的方法变为set(Object).MyBox的方法不能覆盖Box的方法.为了解决这个问题,并保留泛型类型的多态性,Java编译器生成了一个桥接方法来确保能够按预期工作.在类型擦除后,MyBox类的桥接方法set(Object)会使用CHECKCAST进行类型转换,在调用原来的set(Comparable)方法执行.

泛型擦除的残留

左边class文件里还是T,其实是签名而已.保留了定义的格式,便于分析字节码.右边的才是对应的参数类型.方框中的是泛型类独有的标记,普通类没有,标记了定义时的成员签名(泛型参数列表,参数类型,返回值等)

泛型与反射

public class TestType {
	private Map<String, Integer> map;

	public static void main(String[] args) throws Exception {
		Field f = TestType.class.getDeclaredField("map");
		System.out.println(f.getGenericType()); //java.util.Map<java.lang.String, java.lang.Integer>
		System.out.println(f.getGenericType() instanceof ParameterizedType); //true
		ParameterizedType parameterizedType = (ParameterizedType) f.getGenericType();
		for (Type type : parameterizedType.getActualTypeArguments()) {
			System.out.println(type); //1.class java.lang.String 2.class java.lang.Integer
		}
		System.out.println(parameterizedType.getOwnerType()); //null
	}
}

通过反射依然能得到泛型的类型,在编译阶段不是已经被擦除了吗?为何还能获取到?

这是因为泛型的信息实际上被保留在了类常量池中,因此可以通过反射的getGenericType()getActualTypeArguments()方法拿到泛型的信息.


总结:

Java泛型的原理?什么是泛型擦除机制?

Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实并不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译器擦除了所有的泛型信息,这样Java就不需要产生新的类型字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息.

Java编译器具体是如何擦除泛型的?
  1. 检查泛型类型,获取目标类型

  2. 擦除类型变量,并替换为限定类型

    • 如果泛型类型的类型变量没有限定(<T>),则用Object作为原始类型
    • 若果有限定类型(<T extends XClass>),则用XClass作为原始类型
    • 如果有多个限定类型(<T extends XClass&XClass2>),则使用第一个边界XClass1作为原始类
    1. 在必要时插入类型转换以保持类型安全
    2. 生成桥方法一再扩展时保持多态性
使用泛型以及泛型擦除带来的影响(副作用)
  1. 泛型类型变量不能使用基本数据类型

    比如:没有ArrayList<int>,只有ArrayList<Integer>.当类型擦除后,ArrayList的原始类中的类型变量T替换成了Object,但Object类型不能存放int值

  2. 不能使用instanceof运算符

    擦除之后,ArrayList<list>只剩下原始类型,泛型信息String不存在了.所以没有办法使用instanceof

  3. 泛型在静态方法和静态类中的问题

    因为泛型类中的泛型参数的实例化是在定义泛型类型对象(如ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,连所有对象都没创建,如何确定这个泛型参数是什么

  4. 泛型类型中的方法冲突

    因为擦除后两个equals()变成一样的了

  5. 无法创建泛型实例

    public static <E> void append(List<E> list){
    	E elem = new E();//无法创建一个类型参数的实例,这样会引起编译错误
    	list.add(elem);
    }
    public static <E> void append(List<E> list,Class<E> clazz)throws Exception{
    	E elem=clazz.newInstance();
    	list.add(elem);//可以通过反射创建一个参数化类型的实例
    }
    
  6. 没有泛型数组

    public static <T> void test() {
    	Apple[] apples = new Apple[10];
    	Fruit[] fruits = new Fruit[10];
    	System.out.println(apples.getClass());
    	//class [Lcn.chitanda.java.demo02.Apple
    	System.out.println(fruits.getClass());
    	//class [Lcn.chitanda.java.demo02.Fruit
    	fruits = apples;
    	System.out.println(fruits.getClass());
    	/*
    	fruits[0] = new Banana(); //编译可以通过,运行报ArrayStoreException
    	Fruit是Apple的父类,Friut[]是Apple[]的父类,这称作数组的协变
    	Plate<Apple> applePlate =new Plate<Apple>[10];
    	T[] arr = new T[10];
    	由于擦除机制,运行时无法知道数组的类型,因此以上的两种写法是不允许的
    	 */
    	Plate<?>[] plate = new Plate<?>[10];
    }
    

泛型,继承和子类型

给定两种具体的类型A和B(如Fruit和Apple),无论A和B是否相关,MyClass<A>与MyCLass<B>都无关,他们的公共父对象是Object

interface Plate <T>{}
class AIPlate <T> extends Plate <T>{}
class BigPlate <T> extends AIPlate <T>{}
class ColorPlate <T> extends AIPlate <T>{}
void test(){
    Plate<Apple> applePlate = new AIPlate<Apple>();
	Plate<Apple> applePlate1 = new BigPlate<Apple>();
    Plate<Apple> applePlate2 = new ColorPlate<String,Apple>();
    //以上三种写法都正确
}

extends 上界通配符:Plate<? extends Fruit>表达的意思是一个能放水果以及一切是水果派生类的盘子.Plate<? extends Fruit>Plate<Fruit>以及Plate<Apple>的基类,直接的好处是可以使用苹果盘子给水果盘子赋值了.

super 下届通配符:Plate<? super Fruit>表达的意思是一个能放水果以及一切水果基类的盘子.Plate<? super Fruit>Plate<Fruit>的基类但不是Plate<Apple>的基类

Plate<?>非限定通配符是一个泛型类型等价于Plate<? extends Object>

List不进行类型安全检查;List<?>进行类型安全检查.

Java泛型PECS原则

如果你只需要从集合中获得类型T,使用<? extends T>通配符.

如果你只需要将类型T放到集合中,使用<? super T>通配符.

如果你既要获取又要放置元素,则不使用任何通配符.如List<Apple.

PECS即Producer extends Consumer super

为何要使用PECS原则?:提升API的灵活性

<?>既不能存也不能取

public class PECS {
	public static void main(String[] args) {
		List<Apple> dest = new ArrayList<>(10);
		List<Apple> src = new ArrayList<>();
		src.add(new Apple(1));
		dest.add(new Apple(2));
		System.out.println(dest);
		copy(dest, src);
		System.out.println(dest);

		List<Banana> dest1 = new ArrayList<>(10);
		List<Banana> src1 = new ArrayList<>();
		src1.add(new Banana(1));
		dest1.add(new Banana(2));
		System.out.println(dest1);
		copy1(dest1, src1);
		System.out.println(dest1);

		List<Fruit> dest2 = new ArrayList<>(10);
		dest2.add(new Banana(3));
		System.out.println(dest2);
		//  copy1(dest2,src1);
		copy2(dest2, src1);
		System.out.println(dest2);

		List<Fruit> dest3 = new ArrayList<>(10);
		dest3.add(new Banana(2));
		System.out.println(dest3);
		// PECS.<Fruit>copy2(dest3, src1);
		copy3(dest3, src1);
		System.out.println(dest3);
	}

	private static <T> void copy3(List<? super T> dest, List<? extends T> src) {
		Collections.copy(dest, src);
	}

	private static <T> void copy2(List<? super T> dest, List<T> src) {
		Collections.copy(dest, src);
	}

	private static <T> void copy1(List<T> dest1, List<T> src1) {
		Collections.copy(dest1, src1);
	}

	public static void copy(List<Apple> dest, List<Apple> src) {
		Collections.copy(dest, src);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值