Java泛型深入理解

来源:互联网 发布:大外交 知乎 编辑:程序博客网 时间:2024/06/15 11:05

一、泛型之前

在面向对象编程语言中,多态算一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。

在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 obj){        //这里的实现,只是为了演示,不具有任何参考价值        int length = elements.length;        Object[] newElements = new Object[length+1];        for(int i=0;i<length;i++){                newElements[i]=elements[i];            }            newElements[length]=0;            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    }}

这样的实现面临两个问题:
1、当我们获取一个值的时候,必须强制类型转换。
2、假定我们预想的是利用stringValues来存放String集合,因为ArrayList只是维护一个Object引用的数组,我们无法阻止将Integer类型(Object子类)的数据加入stringValues。然而,当我们使用数据的时候,需要将获取的Object对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如Integer),编译时我们不会收到任何的错误提示,但当我们运行程序时却会报异常;

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。

二、泛型

针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案:类型参数。例如,ArrayList类用一个类型参数来指出元素的类型。

public class GenericType{    public static void main(String []args){        ArrayList<String> stringValues = new ArrayList<String>();        stringValues.add("str");        stringValues.add(1);//编译错误    }}

现在,如果我们向ArrayList添加Integer类型的对象,将会出现编译错误。

编译器会自动帮我们检查,避免向集合中插入错误类型的对象,从而使得程序具有更好的安全性。

总之,泛型通过类型参数使得我们的程序具有更好的可读性和安全性。

三、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<String>泛型类型,只能存储字符串,一个是ArrayList<Integer>泛型类型,只能存储整型。最后,我们通过arrayString对象和arrayInteger对象的getClass方法获取它们的类信息并比较,发现结果为true.这是为什么呢?明明我们定义了两种不同的类型?因为,在编译期间,所有的泛型信息都会被擦除,,List<Integer>List<String>类型,在编译后都会变成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 obj){        this.value = value;    }}

因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。如果Pair<T extends Number>,擦出后,类型变量用Number类型替换。

public class ReflectlnGeneric{    public static void main(String[]args) throws IllegalArgumentException,SecurityException,IllegalAccessException.InvocationTargetException,NoSuchMethodException{        ArrayList<Integer> array = new ArrayList<Integer>();        //这样调用add方法只能存储整型,因为泛型类型的实例为Integer        array.add(1);        //利用反射调用add方法array.getClass().getMethod("add",Object.class).invoke(array,"asd");for(int i= 0;i<array.size();i++){            System.out.println(array.get(i));            }        }}
输出:1asd

为什么呢?我们在介绍 泛型时指出向ArrayList<Integer>,中插入String类型的对象;编译时会报错。现在为什么又可以了呢?

我们在程序中定义了一个ArrayList<Integer>泛型类型,如果直接调用add方法,那么只能存储整型的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。
这说明了ArrayList泛型信息在编译之后被擦除了,只保留了原始类型,类型数据(T)被替换为Object,在运行时,我们可以往其中插入任意类型的对象。

但是,并不推荐以这种方式操作泛型类型,因为违背了泛型的初衷(减少强制类型转换以及确保类型安全)。当我们从集合中获取元素时,默认会将对象强制转换成泛型参数指定的类型(这里是Integer),如果放入了非法的对象这个强制转换过程就会出现异常。

泛型方法的类型推断

在调用泛型方法的时候,可以指定泛型类型,也可以不指定。
在不指定泛型类型的情况下,泛型类型为该方法中的几种参数类型的共同父类的最小级,知道Object.

在指定泛型类型的时候,该方法的所有参数类型必须是该泛型类型或者其子类。

public class Test{    public static void main(String[]args){        /*在不指定泛型的时候*/        //这两个参数一个是Integer,另一个是Float,所以取同一父类的最小级,为Number.        int i = Test.add(a,1.2);        //这两个参数一个是Integer,另一个是String,所以取同一父类的最小级,为Object;        Object obj = Test.add(1,"asd");        /*指定泛型的时候*/        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.3);//指定为Number,所以可以为Integer和Float;    }//这是一个简单的泛型方法;public static<T> add(T x,T y){        return y;    }}

正确的运转
既然说类型变量会在编译的时候擦除掉,那为什么定义了ArrayList<List>泛型类型,而不允许向其中插入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的强制类型转换。也就是说,编译器把这方法调用翻译为两条字节码指令:

1、对原始方法Pair.getValue的调用;
2、将返回的Object类型强制转换为Integer

此外,存取一个泛型域时,也要插入强制类型转换。因此,我们说Java的泛型是在编译器层次上进行实现的,被称为“伪泛型”,相对于C++。

泛型相关问题
1、泛型类型引用传递问题
在Java中,像下面形式的引用传递是不允许的:

ArrayList<String> arrayList1 = new ArrayList<Object>();//编译错误ArrayList<Object> arrayList2=new ArrayList<String>();//编译错误

我们先看第一种情况,将第一种情况拓展成下面的形式:

ArrayList<Object> arrayList1 = new ArrayList<Object>();arrayList1.add(new Object());arrayList1.add(new Object());ArrayList<String> arrayList2 = arrayList1;//编译错误

实际上,在第4行代码处,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用arrayList2引用get()方法取值的时候,返回的都是String类型的对象,可是它里面实际上已经被我们存放了Object类型的对象,这样,就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷);

在看第二种情况,将第二种情况拓展成下面的形式:

ArrayList<String> arrayList1 = new ArrayList<String>();arrayList1.add(new String());arrayList1.add(new String());ArrayList<Object> arrayList2 = arrayList1;//编译错误

没错,这种情况比第一种情况好的多,最起码,在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢?
泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以Java不允许这么干。再说,你如果又用arrayList2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

所以,要格外注意泛型中引用传递的问题。

2、泛型类型变量不能是基本数据类型;

就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类中的类型变量(T)替换为Object,但Object类型不能存储double值;

3、运行时类型查询;
举个例子:

ArrayList<String> arrayList = new ArrayList<String>();/*因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。那么,运行时进行类型查询的时候使用下面的方法是报错的*/if(arrayList instanceof ArrayList<String>)//java限定了这种类型查询的方式,?为通配符,也即非限定符。if(arrayList instanceof ArrayList<?>)

4、泛型在静态方法和静态类中的问题;
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。

public class Test2<T>{    public static T one;//编译错误    public static T show(T one){//编译错误    return null;    } }

因为泛型类中的泛型参数的实例化是在定义泛型类型对象(例如ArrayList<Integer>)的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型所以当然是错误的。
但是要注意区分下面的一种情况:

public class Test2<T>{    public static <T> T show(T one){//这是正确的return null;    }}

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

泛型相关面试题
1、Java中的泛型是什么?使用泛型的好处是什么?
泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。

泛型一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免在运行时出现ClassCastException.

2、Java的泛型时如何工作的?什么是类型擦除?
泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。

编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么进行擦除了呢?这是为了避免类型膨胀。

3、什么是泛型中的限定通配符和非限定通配符?
限定通配符对类型进行了限制。有两种限定通配符,一种是

public V put(K key,V value){    return cache.put(key,value);}

4、可以把List<String>传递传递给一个接受List<Object>参数的方法吗?

对于任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为咋看起来String是一种Object,所以List<String>应当可以用在需要List<Object>的地方。但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做事有意义的,因为List<Object>,可以存储任何类型的对象包括String、Integer等等,而List<String>却只能用来存储String。

5、Java中List<Object>和原始类型List之间的区别?
原始类型和带参类型<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。这道题的考察点在与对泛型原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。

原创粉丝点击