Java中泛型的理解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中泛型的理解相关的知识,希望对你有一定的参考价值。

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(); 。

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

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

我们需要知道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>接口的情况

以上是关于Java中泛型的理解的主要内容,如果未能解决你的问题,请参考以下文章

java中泛型的使用

Java中泛型的使用

Java中泛型的介绍与简单使用

java中泛型的k t v e 分别代表啥

Java基础Java中如何获取一个类中泛型的实际类型

java中泛型List问题