Java泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,它与C++中的模板templates比较类似。但是有一点,Java的泛型在编译期有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。
类型参数使程序具有更好的可读性和安全性。
- 类型擦除
- 泛型类
- 泛型方法
- 类型通配符(类型限定)
- 运行时获取泛型信息
一、类型擦除
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); //内存中只有一个Class对象
//类型擦除:编译时有效,编译生成的class文件中:类型被擦除
//因为JVM运行时已经不包含类型信息,所以下面通过反射在运行时插入不同类型的数据
List<Integer> list3 = new ArrayList<>();
list3.add(1);
// list3.add("asd"); //类型参数在编译时有效
try {
list3.getClass().getMethod("add", Object.class).invoke(list3, "asd"); //获取公共方法add
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
for (int i = 0; i < list3.size(); i++) {
System.out.println(list3.get(i));
}
// list3.forEach(item -> {
// System.out.println(item);
// });
}
}
输出结果:
true
1
asd
上面的例子,说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型,即Object。
当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型Object。
Java 之所以要避免在创建泛型实例时而创建新的类,从而避免运行时的过度消耗。
二、类型擦除后保留的原始类型 & 类型通配符
编译后,类型变量被擦除,并使用其限定类型(无限定的变量用Object)替换。
下面是一个最简单的泛型类:
class Box<T> {
private T data;
public Box() {
}
public Box(T data) {
setData(data);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 泛型之间没有父子关系
* 在逻辑上Box<Number>不能视为Box<Integer>的父类
* 但是ArrayList<E>可以继承AbstractList<E>
*
* 类型通配符一般是使用 ? 代替具体的类型实参。
* 注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>、Box<Number>...等所有Box<具体类型实参>的父类。
*
* 泛型方法
* 无论何时,只要你能做到,你就应该尽量使用泛型方法。
* 也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
* 另外,对于一个static的方法而言,无法访问泛型类的类型参数。所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
*
* 不能创建泛型数组。一般的解决方案是任何想要创建泛型数组的地方都使用ArrayList.
*/
public class Test2 {
public static void main(String[] args) {
ArrayList list = new ArrayList(); //在泛型类中,不指定泛型的时候,这个时候的泛型类型为Object
list.add("adc");
list.add(1.0);
Box<Integer> a = new Box<>(712);
// Box<Number> b = a;
Box<Float> f = new Box<>(3.14f);
// getData(f);
Box<Number> n = new Box<>(12);
Box<String> s = new Box<>("asd");
getData2(a);
getData2(f);
getData2(s);
getUpperNumberData(a);
getUpperNumberData(f);
getUpperNumberData(n);
// getUpperNumberData(s);
// getLowerNumberData(a);
// getLowerNumberData(f);
// getLowerNumberData(s);
getLowerNumberData(n);
}
public static void getData(Box<Number> data) {
System.out.println("data :" + data.getData());
}
public static void getData2(Box<?> data) {
System.out.println("data :" + data.getData());
}
//类型通配符上限
//需要定义一个功能类似于getData2()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类
public static void getUpperNumberData(Box<? extends Number> data){
System.out.println("data :" + data.getData());
}
//类型通配符下限
// Box<? super Number>形式,其含义与类型通配符上限正好相反: 只能是Number类及其父类
public static void getLowerNumberData(Box<? super Number> data){
System.out.println("data :" + data.getData());
}
//泛型方法
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
//static的方法无法访问泛型类的类型参数,所以必须将其定义为泛型方法
public static <T> List<T> makeList(T... args){
List<T> result = new ArrayList<T>();
for(T item:args)
result.add(item);
return result;
}
}
有时,类或方法需要对类型变量加以约束。
如下,获取数组中最小的元素:
class ArrayAlg {
public static <T> T min(T[] arr) {
if (arr == null || arr.length <= 0)
return null;
T smallest = arr[0];
for (int i = 1; i < arr.length; i++) {
if (smallest.compareTo(arr[i]) > 0) //编译错误
smallest = arr[i];
}
return smallest;
}
}
不能保证 T 中有compareTo方法,所以必须对类型参数加以限定:
class ArrayAlg {
public static <T extends Comparable<T>> T min(T[] arr) {
if (arr == null || arr.length <= 0)
return null;
T smallest = arr[0];
for (int i = 1; i < arr.length; i++) {
if (smallest.compareTo(arr[i]) > 0)
smallest = arr[i];
}
return smallest;
}
}
如果存在类,类必须位于extends或者super限定符后的第一个位置,如果是接口(Comparable),就无所谓。
三、泛型方法
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。
在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
public static void main(String[] args) {
/**不指定泛型的时候*/
int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f=Test2.add(1, 1.2);//这两个参数一个是Integer,以风格是Double,所以取同一父类的最小级,为Number
Object o=Test2.add(1, "asd");//这两个参数一个是Integer,以风格是String,所以取同一父类的最小级,为java.io.Serializable
/**指定泛型的时候*/
int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
int b=Test2.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Double
Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Double
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型类型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList中可以放任意类型的对象。
四、运行时泛型信息获取
尝试通过下面的方法获取泛型信息:
List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
/* Output
[E]
[K, V]
*/
我们可能期望能够获得真实的泛型参数,但是仅仅获得了声明时泛型参数占位符。
getTypeParameters方法的Javadoc也是这么解释的:仅返回声明时的泛型参数。所以,通过 getTypeParamters方法无法获得运行时的泛型信息。
可以通过如下方法获取泛型信息:
Map<String, Integer> map = new HashMap<String, Integer>() {};
Type type = map.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
System.out.println(typeArgument.getTypeName());
}
/* Output
java.lang.String
java.lang.Integer
*/
其中最关键的差别是本节的变量声明多了一对大括号。其实是创建了一个匿名内部类。这个类是 HashMap 的子类,泛型参数限定为了 String 和 Integer。
Java 引入泛型擦除的原因是避免因为引入泛型而导致运行时创建不必要的类。那我们其实就可以通过定义类的方式,在类信息中保留泛型信息,从而在运行时获得这些泛型信息。
简而言之,Java 的泛型擦除是有范围的,即类定义中的泛型是不会被擦除的。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
/**
* 运行时泛型信息获取
*/
public class Test3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println("--------------");
Map<String, Integer> map2 = new HashMap<String, Integer>() {}; //匿名内部类
Type type = map2.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
System.out.println(typeArgument.getTypeName());
}
System.out.println("--------------");
Student st = new Student();
Class clazz = st.getClass();
//getSuperclass()获得该类的父类
System.out.println(clazz.getSuperclass());
//getGenericSuperclass()获得带有泛型的父类
//Type是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
Type type2 = clazz.getGenericSuperclass();
System.out.println(type2);
//ParameterizedType参数化类型,即泛型
ParameterizedType p = (ParameterizedType)type2;
//getActualTypeArguments获取参数化类型的数组,泛型可能有多个
Class c = (Class) p.getActualTypeArguments()[0];
System.out.println(c);
}
}
class Person<T> {
}
//类定义中的泛型是不会被擦除的
class Student extends Person<Student> {
}
运行结果:
[E]
[K, V]
--------------
java.lang.String
java.lang.Integer
--------------
class Generic.Person
Generic.Person<Generic.Student>
class Generic.Student
很多情况下我们又需要在运行时获得泛型信息,那我们可以通过定义类的方式(通常为匿名内部类,因为我们创建这个类只是为了获得泛型信息)在运行时获得泛型参数,从而满足例如序列化、反序列化等工作的需要。
五、约束与局限性
1、不能用基本类型实例化类型参数
2、运行时类型查询只适用于原始类型
JVM不认识泛型:
Pair<String> a = new Pair<>();
// System.out.println(a instanceof Pair<String>);
System.out.println(a instanceof Pair);
3、不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10]; //ERROR