Java中泛型的理解

时间:2024-01-18 13:29:08

Java中的泛型,本质上来说,就是是参数化类型,就是说所操作的数据类型被指定为一个参数,而不是确定的某种类型。这种数据类型可以用在类、接口和方法创建中。即泛型类、泛型接口、泛型方法。这样说可能不够生动,来举些例子说明一下。

例子一


我们通过 例子一 来简单看看泛型的应用场景。下面的情况中,没有使用泛型:

public class FanXingTest {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
Integer integer1 = 1;
arrayList.add(integer1);
//你记得这个元素的类型,那么没问题,通过index取出来
Integer integer2 =(Integer) arrayList.get(0);
System.out.println(integer2);
//假如你记错了arraylist里面存的类型了,这时编译器也不会报错,但是运行时会报错
String string =(String) arrayList.get(0);
System.out.println(string);
}
}

ArrayList里存放的是Object类。这样的好处是:通过对类型Object的引用来实现可以存放任意类(Java的多态机制),但也带来了缺点:需要进行强制类型转换。

可以看到,在没有使用泛型的情况下,你若记得 arrayList 里面存了什么类型的东西,那么最好不过了,可以直接通过index来取出来,并进行类型转换即可。

而假如你在不知道的情况下强制转换写错,编译器也不会提示错误,在运行时才会发现,会报一个错: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 。实际情况中程序员不可能对每个元素都记得其类型,所以这样有明显的不合理性。

泛型的引入可以解决上述问题,先来看看针对上述情景使用泛型的情况:

public class FanXingTest {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
Integer integer1 = 1;
arrayList.add(integer1);
//直接取出来,无需强制类型转换
Integer integer2 = arrayList.get(0);
System.out.println(integer2);
//编译器会检查出错误,根本不允许你把类型搞混了,下面两种情况编译器都直接提示不能类型转换
String string1 =(String) arrayList.get(0);
String string2 = arrayList.get(0);
}
}

上面的代码中我们使用了泛型,就是在创建 arrayList 的时候,就限定了里面存放的只能是 Integer 类型的数据,那么编译时就可以检查类型安全,所有的强制类型转换是自动和隐式的,提高代码重用率。我们在取出数据的时候,就无需进行强制类型转换,而且你把类型搞错,编译器会提示错误。

例子二


那么泛型是如何实现的呢?自己如何创建泛型类?我们通过 例子二 来看一看。先自己来创建一个泛型类,如下所示:

//自己新建一个泛型类:MyFan
class MyFan<T>{
private T t1;
//构造函数
public MyFan(T t1){
this.t1=t1;
}
//测试泛型的操作数据类型
public void showInfo(){
System.out.println("所操作数据类型是:"+t1.getClass().getName());
//通过反射机制,可以知道T的各种成员变量和方法的信息
Method m[]=t1.getClass().getDeclaredMethods();
for (int i = 0; i < m.length; i++) {
System.out.println(m[i].getName());
}
}
}

自己创建泛型类的时候,先假设这样一个处境:我希望我的这个类可以处理多种类型的数据。这就需要把类型"参数化",也就是当成一个"参数"传进来。所以定义类的时候,在类名的后面跟了一个<T>: class MyFan<T> ,这个T只是一个象征意义的符号,可以是任意符号。T 就是个类型"参数",我们之后可以传任意类型的类进来。上面的 showInfo() 方法中,我们可以利用反射机制,来得到传进来的这个数据类型是什么,也可以得到这个类里面有些个什么方法。接下来,我们利用我们自己创建的泛型类 MyFan<T> 来做个试验:

public class FanXingTest {
public static void main(String[] args) {
// TODO Auto-generated method stub MyFan<String> ft1=new MyFan<String>("黄黄");
ft1.showInfo();
MyFan<Integer> ft2=new MyFan<Integer>(1);
ft2.showInfo(); //输出两个实例对象的类型,结果是一样的
System.out.println(ft1.getClass());
System.out.println(ft2.getClass()); }
}

通过创建我们自己的泛型类的实例, ft1 里面的 t1 变量指向一个String类实例, ft2 里面的 t1 变量指向一个Integer类实例,那么上述程序的输出如下:

所操作数据类型是:java.lang.String...String的各个成员方法略去
所操作数据类型是:java.lang.Integer...Integer的各个成员方法略去
class MyFan
class MyFan

最后两行的输出,我们是想来通过 ft1/ft2.getClass() 来看看创建的这两个实例的类型会不会不一样,结果可以看到,两个都是属于 MyFan类的实例变量,并没有因为传入类型的不同而引起不同。

例子三


现在应该知道了泛型是如何运作的,我们在自己定义泛型类或者泛型接口的时候,<T>都是跟在类名的后面: class MyFan<T>{...} ,然后实例化的时候<T>也跟在类名的后边即可: MyFan<T> fan1 = new MyFan<T>()。

有些时候我们还会遇到更为复杂的情况,这几天我在工具类Collections里面,看到了sort方法,如下:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}

我们需要知道extends后面跟的类型表示泛型的上限(含自己),super表示的是泛型的下限(含自己)。如<T extends 类/接口>,表示T必须是该指定类/接口的子类(实现了某接口或继承自某类,都算"子类")。

我们来看 <T extends Comparable<? super T>> ,首先规定了T必须是 Comparable<? super T> 的一个子类,也就是说,T 必须是实现了 Comparable<? super T> 这个接口的。然后<? super T>表示Comparable<>中的类型下限为T(就是至少得是T的父类,包括T)!我们可以通过 GregorianCalendar 类和 Calendar 类来加深理解。先看下两个类的定义:

//Calendar类实现了Comparable<Calendar>接口
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar>{
...
@Override
public int compareTo(Calendar anotherCalendar) {
return compareTo(getMillisOf(anotherCalendar));
}
}
//GregorianCalendar类继承自Calendar类
public class GregorianCalendar extends Calendar{...}

那我们现在假设有个ArrayList,调用上面的sort方法:

ArrayList<GregorianCalendar> arrayList = new ArrayList<GregorianCalendar>();
Collections.sort(arrayList);

此时不会报错,因为相当于 <GregorianCalendar extends Comparable<Calendar>> 是成立的,我们结合两个类的定义可以知道:Calendar为GregorianCalendar 的父类,并且Calendar实现了Comparable<Calendar>,于是GregorianCalendar 也实现了Comparable<Calendar>接口。所以泛型的限制范围 <T extends Comparable<? super T>> 是满足的。接下来我们自己定义一个sort函数:

public static <T extends Comparable<T>> void sort(ArrayList<T> list) {
list.sort(null);
}

此时调用我们的sort函数 sort(new ArrayList<GregorianCalendar>()); ,编译器就会报错,因为GregorianCalendar类只是实现了Comparable<Calendar>接口,并没有实现Comparable<GregorianCalendar>接口。于是传入的参数不满足泛型的限制范围,所以会报错

自己的一些思考

先看看下面的Collections类里面的两个sort方法

public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}

注意到上面除了方法参数(比如List<T> list)之外,都是在返回类型(void)之前对方法参数类型进行了限定(比如static <T extends Comparable<? super T>> void)。

其实泛型就是”可有可无”,它的存在就是限制了你将要使用的参数类是什么范围内,extends/super分别是泛型的上下界(含)。就比如上面第一个sort,限定了<T extends Comparable<? super T>>,第二个就是没限定,任意的<T>;但是还有参数这块,第一个的参数就是之前限定好的参数T即可;第二个的参数呢,在Comparator<? super T>这里限定了一个下届,得是T的父类。

再往下说,为什么是<T extends Comparable<? super T>>而不是<T extends Comparable<T>>呢。这其实考虑到了子类若是继承自父类,而父类已经实现了Comparable接口,子类可以拿来用,所以就有<T extends Comparable<? super T>>的效果,举个例子就是上面的GregorianCalendar类和Calendar类,等效于子类GregorianCalendar也实现了Comparable接口。这时候若是<T extends Comparable<T>>就不合适了。

其实一般自己实现的Comparable接口的话,都是 class MyTest implements Comparable<MyTest>这种,估计也不会class MyTest implements Comparable<MyTest的父类>,上面的<T extends Comparable<? super T>>只是考虑到了比如ManmanTest继承自MyTest,那么ManmanTest也就实现了Comparable<MyTest>接口的情况。