一、泛型定义
泛型,即参数化类型,在使用的时候可以传入具体的类型,泛型仅应用于类型之上,可以使代码应用于多种类型,使类和方法具有更广泛的表达能力。
二、泛型意义
为了使代码具有更广泛的灵活性,一般会采取以下几种方式编程:
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编程思想第四版