Java泛型原理浅析

一、泛型定义

泛型,即参数化类型,在使用的时候可以传入具体的类型,泛型仅应用于类型之上,可以使代码应用于多种类型,使类和方法具有更广泛的表达能力。

二、泛型意义

为了使代码具有更广泛的灵活性,一般会采取以下几种方式编程:
1.重载方法,此编程方式可以增加程序的灵活性和适用性,但是编写繁琐
2.方法参数为基类,方法可以接受从这个基类导出的任何类作为参数,可以提供更大的灵活性,由于Java是单继承体系,因此程序受限还是比较多,容器类需要手动向下转型
3.方法参数为接口,接口提供了很大的编程灵活性,但是这会限制方法只能基于特定的接口。
为了使类或者方法具备最广泛的表达能力,为了解耦类或者方法与类型之间的约束,Java引入了泛型。也就是使类可以使用与很多很多类型。

三、泛型实现

1.擦除
JAVA泛型是通过擦除来实现的,目的是为了向后兼容。
当在使用泛型的时候,在运行期任何具体的类型信息都被擦除了,唯一知道的是在使用一个对象。因此ArrayList和ArrayList在运行时都是相同的类型,即擦除到他们的原生类型。示例代码:

package generic;
import java.util.ArrayList;
public class ErasedTypeEquivalence {

	public static void main(String[] args) {
		Class c1 = new ArrayList<String>().getClass();
		Class c2 = new ArrayList<Integer>().getClass();
		System.out.println(c1 == c2);
	}
}
//output true

运行期间返回的类型参数也只是个占位符,没有实际意义。因此在运行期只可以知道类型参数标识符和泛型边界这类信息,却无法知道用来创建某个特定实例的实际的类型参数。示例代码:

package generic;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class LostInfomation {

	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		Stream.of(list.getClass().getTypeParameters()).forEach(System.out::println);
	}
}
//output E

2.编译器层面实现泛型
在使用泛型的时候,例如将Integer类型的数据添加到ArrayList集合里,会产生编译器错误,示例代码:

package generic;
import java.util.ArrayList;
import java.util.List;
public class ErrorList {

	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		list.add(1);// error
	}
}

Java泛型在运行时期会将泛型擦除掉,但是在编译期又会有校验。那么我们猜测应该是Java在编译期为我们做了一些事情。
通过以下示例代码我们来验证一下。

package generic;
public class GenericHolder<T> {
	private T obj;
	public void set(T obj) {
		this.obj = obj;
	}
	public T get() {
		return this.obj;
	}
	public static void main(String[] args) {
		GenericHolder<String> holder = new GenericHolder<>();
		holder.set("item");
		String s = holder.get();
		System.out.println(s);
	}
}

用javap -c GenericHolder.class反编译一下,看会有什么结果
在这里插入图片描述
前两个红框说明参数类型T被擦除为Object,第三个红框说明是编译器帮我们做了类型强转。
以上我们发现Java泛型实现是编译器在编译的时候为我们插入了类型强制转换逻辑,因此我们也可以理解为什么Integer类型的元素无法添加到ArrayList列表了。

那么泛型通过类继承或者泛型接口实现的泛型具体化,Java编译器为我们做了什么操作呢?见示例代码:

package bridge;
public interface SuperClass<T> {
	public T apply(T t);
}

package bridge;
import java.util.stream.Stream;
public class SonClass implements SuperClass<String> {
	@Override
	public String apply(String t) {
		return t;
	}
	public static void main(String[] args) {
		Stream.of(SonClass.class.getDeclaredMethods()).forEach(method -> {
			System.out.println(method.getName() + ", returnType is " + method.getReturnType() + ": is Bridge " + method.isBridge());
		});
	}
}//output apply, returnType is class java.lang.Object: is Bridge true

反编译结果如下:javap -c SonClass.class
在这里插入图片描述
SuperClass由于泛型擦除,方法擦除为Object apply(Object t);
SonClass实现了SuperClass,按照Java实现接口规则,那么SonClass应该也要有一个Object apply(Object t)实现方法,但SonClass却是String apply(String t)的实现方法,明显违反了Java实现接口原则。看反编译结果,编译器为我们添加了一个Object apply(Object t)实现方法,并且对String进行向上造型。
编译器添加的方法我们称之为桥接方法,泛型的接口实现是编译器为我们添加了桥接方法来完成的。

泛型类的继承原理类似,请读者自行验证。
3.擦除补偿
由于泛型擦除丢失了泛型在运行期的某些消息,导致任何在运行时需要知道确切类型信息的操作都无法工作。如下:
a.instance of
b.new
c.cast
d.原始类型不能用于泛型
e.一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体成为相同的接口
f.catch语句不能捕获泛型的异常,泛型类也不能直接或者间接集成Throwable类

一个类不能实现同一个泛型接口的两种变体示例代码:

package generic;
public interface Payable<T> {}

package generic;
public class Employee implements Payable<Employee> {}

package generic;
public class Hourly extends Employee implements Payable<Hourly>{} //编译异常

如果从Payable的两种用法中都移除掉泛型参数,则可以编译

既然泛型不能直接进行new操作,那么创建实现类应该如何创建呢。

package generic;

public class ClassAsFactory<T> {
	T x;
	public ClassAsFactory(Class<T> kind) {
		try {
			x = kind.newInstance();
		} catch (InstantiationException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}
}

泛型数组只能声明,不能实例化。
static Generic[] gia = new Generic[SIZE];
以上语句会报编译异常。
泛型类型在运行时会擦除为Object类型,而Generic仅存在于编译期,因此数组创建的时候只能创建为Object类型数组,即new Generic[].成功创建泛型数组唯一的方式就是创建一个被擦除类型的数组,然后将其转型。
gia = (Generic[]) new Generic[SIZE];
看下以下代码示例

package generic;
public class GernericArray<T> {
	private Object[] array;
	public T[] rep() {
		return (T[]) array;
	}
	public GernericArray(int sz) {
		array = new Object[sz];
	}
	public static void main(String[] args) {
		GernericArray<Integer> ga = new GernericArray<>(3);
		Integer[] arr = ga.rep();
		System.out.println(arr);
	}
}
//output Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;at generic.GernericArray.main(GernericArray.java:17)

原因为何,因为底层的数组为Object[],而在Java中数组是一级对象,因此Object[]不能强转为Integer[],因此报错。

四、泛型边界与通配符

我们先看以下代码示例:

package generic;
public class HasF {
	public void f() {
		System.out.println("HasF.f()");
	}
}

package generic;
public class Manipulator<T> {
	private T obj;
	public Manipulator(T x) {
		this.obj = x;
	}
	public void manipulate() {
		obj.f();
	}
}// obj.f()会报编译异常

因为泛型擦除,Java编译器无法将f方法映射到HasF上有f方法这一事实上,因此该调用会出现编译异常。为了能调用f()方法,那么我们需要给泛型指定边界,告知编译器。如下代码示例。

package generic;
public class Manipulator2<T extends HasF> {
	private T obj;
	public Manipulator2(T x) {
		this.obj = x;
	}
	public void manipulate() {
		obj.f();
	}
}

通过以上代码示例,我们可以推测,泛型会擦除到边界,事实上泛型会擦除到第一个边界。

package generic;
import java.io.Serializable;
public class MultiBounds<T extends HasF & Serializable> {
	private T t;
	public void set(T t) {
		this.t = t;
	}
	public T get() {
		return this.t;
	}
}

对以上代码进行反编译javap -c MultiBounds.class
在这里插入图片描述
通过反编译可以看出,泛型擦除到HasF,第一个边界
另外,有个注意点,多边界中如果有一个是类,那么类必须放在第一边界,否则编译会报错。

数组示例代码:

package generic;
public class Fruit {}

package generic;
public class Apple extends Fruit {}

package generic;
public class Orange extends Fruit {}

package generic;
public class Jonathan extends Apple {}

package generic;
public class CovariantArrays {
	public static void main(String[] args) {
		Fruit[] fruit = new Apple[10];
		fruit[0] = new Apple();
		fruit[1] = new Jonathan();
		
		fruit[0] = new Fruit();
		fruit[1] = new Orange();
	}
}//output Exception in thread "main" java.lang.ArrayStoreException: generic.Orange at generic.CovariantArrays.main(CovariantArrays.java:11)

fruit[0] = new Fruit();实际这个赋值语句也会报同样的错误。
我们分析一下原因,看为什么会报以上异常。
Fruit[]引用作为水果类的引用,Java编译器是允许将Fruit或者其子类的类型元素添加到Fruit数组中的,但是运行时候会报错误,因为Fruit[]引用实际是Apple[],因此Apple[]数组只能添加Apple或者其子类元素。这个错误只有在运行时才会产生,那么如果是利用泛型呢?是不是会在编译期就会产生异常呢。请看以下示例代码:

package generic;
import java.util.ArrayList;
import java.util.List;
public class NonConvariantGenerics {

	List<Fruit> list = new ArrayList<Apple>();//编译异常
}

以上示例说明泛型在编译期解决了数组遇到的类似问题。
Apple的List不是Fruit的List,Apple的List持有Apple或者Apple子类元素,Fruit持有Fruit或者Fruit子类的元素,但是Fruit的子类不一定是Apple,因此Fruit的List不是Apple的List。因此无法完成赋值。

有时候,想要在两个类型之间建立某种类型的向上转型关系,而这正是通配符所允许的,如下示例代码:

package generic;
import java.util.ArrayList;
import java.util.List;

public class GenericAndConvaraint {
	public static void main(String[] args) {
		List<? extends Fruit> flist = new ArrayList<>();
		flist.add(new Apple());//编译异常
		flist.add(null);
		flist.add(new Object());//编译异常
		Fruit f = flist.get(0);
	}
}

引入了通配符,List<? extends Fruit> flist = new ArrayList<>();代表flist持有从Fruit继承的类型列表,但是不意味着flist可以持有任何Fruit类型,而是代表flist持有某一种具体Fruit类型。而Apple是Fruit的一个具体子类,因此编译器允许,不会报错。
为什么flist.add(new Apple()),flist.add(new Object())会编译异常呢,因为flist可以指向任意的Fruit类型列表,比如指向List,那么这个时候,向flist添加任何元素,编译器都不会允许,除非是null对象。
Fruit f = flist.get(0)这个为什么可以正常呢,因为flist中元素无论如何都至少是Fruit类型。
具有<?extends T>形式的泛型成为上界,上界只能取值,不能存值。

如下示例代码

package generic;
import java.util.List;
public class SuperTypeWildCard {

	static void writeTo(List<? super Apple> apples) {
		apples.add(new Apple());
		apples.add(new Jonathan());
	}
}

利用了super关键字,可以添加Apple和Apple子类元素了。
形如 List<? super Apple>,具体哪一种不能确定,既可以是 Fruit,也可以是 Fruit的父类,直至 Object类。在尝试执行 add() 方法时,虽然 List 的具体类型不能确定,但是根据多态, Apple 类及其子类的对象肯定都可以被赋值给 Apple 的对象,所以只能添加 Apple 类及其子类的对象。在尝试执行 get() 方法时,List 中的类型是 Apple 类或者其父类的具体一种,向上直至 Object 类,所以只能将获取的元素赋值给 Object 对象。
具有<?super T>形式的泛型成为下界,下界只能存值,不能取值。

五、泛型动态类型安全

package generic;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CheckedList {
	@SuppressWarnings({ "unchecked", "rawtypes" })
	static void oldStyleMethod(List probableDogs) {
		probableDogs.add(new Cat());
	}
	
	public static void main(String[] args) {
		List<Dog> dogs1 = new ArrayList<>();
		oldStyleMethod(dogs1);//1.运行正常
		
		List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
		try {
			oldStyleMethod(dogs2);//运行失败
		} catch (Exception e) {
			System.out.println(e);
		}
		
		List<Pet> pets = Collections.checkedList(new ArrayList<>(), Pet.class);
		pets.add(new Dog());
		pets.add(new Cat());
	}
}

oldStyleMethod(dogs1),将狗添加的猫的列表中是正确的,因为本行代码接受的是List原生类型,因此所有的对象都可以添加到此列表中。
oldStyleMethod(dogs2),运行时候会报
java.lang.ClassCastException: Attempt to insert class generic.Cat element into collection with element type class generic.Dog异常

六、泛型反射

java.lang.reflect.Type:所有类型的超接口
java.lang.Class:Java类API,例如String,List
java.lang.reflect.GenericArrayType:泛型数组,例如T[],List[]
java.lang.reflect.ParameterizedType:参数类型,例如List, ArrayList
java.lang.reflect.TypeVariable: 类型变量,例如E,K,V
java.lang.reflect.WildcardType:通配符类型,例如?,? extends T

如一下代码示例:

package generic;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class GenericDemo<T> {
	
	private List<String>[] listArr;
	
	private Map<String, Integer> map;
	
	private T[] tArr;
	
	private List<? extends T> list;
	
	private ArrayList<T> arrayList;

	public static void main(String[] args) throws Exception {
		//class
		Class<?> strClass = GenericDemo.class;
		System.out.println(strClass);
		//GenericArrayType
		Type genericArrayType = GenericDemo.class.getDeclaredField("listArr").getGenericType();
		System.out.println(genericArrayType);
		genericArrayType = GenericDemo.class.getDeclaredField("tArr").getGenericType();
		System.out.println(genericArrayType);
		//ParameterizedType
		Type paramerizedType = GenericDemo.class.getDeclaredField("map").getGenericType();
		System.out.println(paramerizedType);
		//WildcardType
		Type wildcardType = ((ParameterizedType)GenericDemo.class.getDeclaredField("list").getGenericType()).getActualTypeArguments()[0];
		System.out.println(wildcardType);
		//TypeVariable
		Type typeVariable = ((ParameterizedType)GenericDemo.class.getDeclaredField("arrayList").getGenericType()).getActualTypeArguments()[0];
		System.out.println(typeVariable);
	}

}
//output class generic.GenericDemo
//java.util.List<java.lang.String>[]
//T[]
//java.util.Map<java.lang.String, java.lang.Integer>
//? extends T
//T

七、ResolvableType

Spring在泛型解析上提供了很方便使用的工具,我们简单介绍一下Spring的一个底层API也是Spring泛型解析的一个核心API,ResolvableType。
该API采取了Immutable和fluent设计方式。
API提供了三大类方法:
1.for方法,此类方法,是将Field,Method,Constructor,Class等包装成ResolvableType,从而方便后续解析操作
2.as
方法,此类方法,获取传入类型的具体泛型信息,例如我们解析myMap属性,该属性为HashMap,那么我们调用asMap方法,就会返回Map的具体泛型信息,如果HashMap没有实现或者继承Map,那么该方法就会返回NONE。
3.resolve*方法,此类方法,用来具体解析泛型的信息。

ResolvableType使用的具体示例如下:

package com.spring.ioc.generic;

import java.util.HashMap;
import java.util.List;

import org.springframework.core.ResolvableType;

public class ResolvableTypeDemo {
	
	private HashMap<Integer, List<String>> myMap;

	public static void main(String[] args) throws Exception {
		
		ResolvableType t = ResolvableType.forField(ResolvableTypeDemo.class.getDeclaredField("myMap"));
		System.out.println(t.getSuperType());
		System.out.println(t.asMap());
		System.out.println(t.getGeneric(0).resolve());
		System.out.println(t.getGeneric(1).resolve());
		System.out.println(t.getGeneric(1));
		System.out.println(t.resolveGeneric(1, 0));
	}
}

以上为Java泛型的简单介绍,欢迎读着批评指正。

参考文档:
[1]Java编程思想第四版

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值