携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情
精通Java泛型的使用
泛型之前
在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。
通过继承设计通用程序
在Java增加泛型类型之前,通用程序的设计就是利用继承实现的,例如,ArrayList类只维护一个Object引用的数组,Object为所有类基类。
public class BeforeGeneric {
/**
* 泛型之前的通用程序设计
*/
static class ArrayList{
private Object[] elements=new Object[0];
public Object get(int i){
return elements[i];
}
/**
* 这里的实现,只是为了演示,不具有任何参考价值
* */
public void add(Object o){
int length = elements.length;
Object[] newElements = new Object[length+1];
for(int i=0; i<length; i++){
newElements[i] = elements[i];
}
newElements[length] = o;
elements = newElements;
}
}
public static void main(String[] args) {
ArrayList stringValues = new ArrayList();
//可以向数组中添加任何类型的对象
stringValues.add(1);
//问题1——获取值时必须强制转换
String str = (String) stringValues.get(0);
//问题2——上述强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
System.out.println(str);
}
}
面临的问题
- 当我们获取一个值的时候,必须进行强制类型转换。
- 当我们插入一个值的时候,无法约束预期的类型。假定我们预想的是利用stringValues来存放String集合,因为ArrayList只是维护一个Object引用的数组,我们无法阻止将Integer类型(Object子类)的数据加入stringValues。然而,当我们使用数据的时候,需要将获取的Object对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如Integer),编译时我们不会收到任何的错误提示。但当我们运行程序时却会报异常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at generic.BeforeGeneric.main(BeforeGeneric.java:24)
这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。
泛型
针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案:类型参数。例如,ArrayList类用一个类型参数来指出元素的类型。
ArrayList<String> stringValues=new ArrayList<String>();
这样的代码具有更好的可读性,我们一看就知道该集合用来保存String类型的对象,而不是仅仅依赖变量名称来暗示我们期望的类型。
public class GenericType {
public static void main(String[] args) {
ArrayList<String> stringValues=new ArrayList<String>();
stringValues.add("str");
stringValues.add(1); //编译错误
}
}
现在,如果我们向ArrayList添加Integer类型的对象,将会出现编译错误。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method add(int, String) in the type ArrayList is not applicable for the arguments (int)
at generic.GenericType.main(GenericType.java:8)
编译器会自动帮我们检查,避免向集合中插入错误类型的对象,从而使得程序具有更好的安全性。
总之,泛型通过类型参数使得我们的程序具有更好的可读性和安全性。
Java泛型的实现原理
擦除
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString=new ArrayList<String>();
ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
System.out.println(arrayString.getClass()==arrayInteger.getClass());
}
}
输出:
true
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整型。最后,我们通过arrayString对象和arrayInteger对象的getClass方法获取它们的类信息并比较,发现结果为true。
这是为什么呢,明明我们定义了两种不同的类型?因为,在编译期间,所有的泛型信息都会被擦除,List和List类型,在编译后都会变成List类型(原始类型)。Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。
原始类型
原始类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。无论何时定义一个泛型类型,相应的原始类型都会被自动提供。原始类型的名字就是删去类型参数后的泛型类型的类名。擦除 类型变量,并替换为 限定类型(T为无限定的 类型变量,用Object替换)。
//泛型类型
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//原始类型
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因为在Pair中,T是一个无限定的类型变量,所以用Object替换。如果是Pair,擦除后,类型变量用Number类型替换。
突破泛型约束
public class ReflectInGeneric {
public static void main(String[] args) throws IllegalArgumentException,
SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
ArrayList<Integer> array=new ArrayList<Integer>();
// 这样调用add方法只能存储整形,因为泛型类型的实例为Integer
array.add(1);
// 通过泛型可以突破泛型类型约束
array.getClass().getMethod("add", Object.class).invoke(array, "asd");
for (int i=0;i<array.size();i++) {
System.out.println(array.get(i));
}
}
}
输出:
1
asd
为什么呢?我们在介绍泛型时指出向ArrayList中插入String类型的对象,编译时会报错。现在为什么又可以了呢?
- 我们在程序中定义了一个ArrayList泛型类型,如果直接调用add方法,那么只能存储整形的数据。
- 不过当我们利用反射调用add方法的时候,却可以存储字符串。
这说明ArrayList泛型信息在编译之后被擦除了,只保留了原始类型,类型变量(T)被替换为Object,在运行时,我们可以行其中插入任意类型的对象。
再次应证:Java中的泛型基本上都是在编译器这个层次来实现的“伪泛型”。
但是,并不推荐以这种方式操作泛型类型,因为这违背了泛型的初衷(减少强制类型转换以及确保类型安全)。当我们从集合中获取元素时,默认会将对象强制转换成泛型参数指定的类型(这里是Integer),如果放入了非法的对象这个强制转换过程就会出现异常。
泛型方法的类型推断
在调用泛型方法的时候,可以指定泛型类型,也可以不指定。
在不指定泛型类型的情况下,泛型类型为该方法中的几种参数类型的共同父类的最小级,直到Object。
在指定泛型类型的时候,该方法中的所有参数类型必须是该泛型类型或者其子类。
public class Test {
public static void main(String[] args) {
/**不指定泛型的时候*/
int i=Test.add(1, 2); //这两个参数都是Integer,所以T替换为Integer类型
Number f=Test.add(1, 1.2);//这两个参数一个是Integer,另一个是Float,所以取同一父类的最小级,为Number
Object o=Test.add(1, "asd");//这两个参数一个是Integer,另一个是String,所以取同一父类的最小级,为Object
/**指定泛型的时候*/
int a=Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
int b=Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float
Number c=Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
正确的运转
既然说类型变量会在编译的时候擦除掉,那为什么定义了ArrayList泛型类型,而不允许向其中插入String对象呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存放别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
java是如何解决这个问题的呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。以如下代码为例:
Pair<Integer> pair=new Pair<Integer> ();
pair.setValue(3);
Integer integer=pair.getValue();
System.out.println(integer);
擦除getValue()的返回类型后将返回Object类型,编译器自动插入Integer的强制类型转换。也就是说,编译器把这个方法调用翻译为两条字节码指令:
- 对原始方法Pair.getValue的调用
- 将返回的Object类型强制转换为Integer
此外,存取一个泛型域时,也要插入强制类型转换。因此,我们说Java的泛型是在编译器层次进行实现的,被称为“伪泛型”,相对于C++。