Java泛型详解

来源:互联网 发布:自学编程先看什么书 编辑:程序博客网 时间:2024/06/10 04:32

title:Java泛型详解

date:2017年10月27日01:16:39


Java泛型是JDK1.5出现的,在泛型出现之前,编写Java通用程序只能通过继承和实现接口实现,有很大的局限性。由于Java是单继承的,在很多时候都有很多限制,而使用接口会好一些,但是仍然会有很多约束。因为一但指明了接口,它就要求你的代码必须使用特定的接口。而我们希望达到的目的是编写更通用的代码,要是代码应用于“某种不具体的类型”,而不是一个具体的接口或者类。这种“不具体的类型”,属于参数化类型,也就是说:把类型当做参数。当定义的时候,我们把类型当做一个参数;在使用的时候,我们才将这个参数用具体的类型来代替。为了实现参数化类型的目标,从而出现了泛型的机制。

一.Java泛型实现的原理

擦除机制:
我们先看一个例子

    @Test    public void test(){        ArrayList<String> a1=new ArrayList<String>();        ArrayList<Integer> a2=new ArrayList<Integer>();        System.out.println(a1.getClass()==a2.getClass());    }

输出为 :true
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整型。最后,我们通过 a1对象和a2对象的getClass方法获取它们的类信息并比较,发现结果为true。
这是为什么呢,明明我们定义了两种不同的类型?因为,在编译期间,所有的泛型信息都会被擦除,List和List类型,在编译后都会变成List类型(原始类型)。Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。
原始类型:
原始类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。无论何时定义一个泛型类型,相应的原始类型都会被自动提供。原始类型的名字就是删去类型参数后的泛型类型的类名。擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)。
我们来看一段代码:

class Pair<T>{    public 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;        }    }    

这里要注意

    public T getValue(){    return value;}

​ 并不是一个泛型方法,这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才可以继续使用 T 这个泛型。

@Test    public void test(){        Pair<Integer> pair=new Pair<Integer> ();          pair.setValue(3);          Integer integer=pair.getValue();          System.out.println(integer);      }

在这个test()方法中。我们分析下getValue()方法,擦除getValue()的返回类型后将返回Object类型,编译器自动插入Integer的强制类型转换。也就是说,编译器把这个方法调用翻译为两条字节码指令:

1、对原始方法Pair.getValue的调用
2、将返回的Object类型强制转换为Integer
此外,存取一个泛型域时,也要插入强制类型转换。因此,我们说Java的泛型是在编译器层次进行实现的,被称为“伪泛型”,相对于C++。
泛型的实现便是编译阶段已经在代码中自动加入了类型的强制转化,将原始类型强制转化为我们实际代码中T填充的类型。我们可以这么理解,开篇我们说到我们把类型T当做一个参数,这里的T为Integer
,我们可以理解为这个T的形参在这里就是原始类型Object,实参为Integer:

上面的代码

        Pair<Integer> pair=new Pair<Integer> ();          pair.setValue(3);          Integer integer=pair.getValue();  `

等价于

    Object object=new Integer(3);    pair.setValue(object);      Object objint=pair.getValue();    Integer int=(Integer)objint;

我们要注意,Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。下面我们看一个例子

@Testpublic void test2() throws Exception{//      ArrayList<String> list=new ArrayList<String>();//      list.add("ad");//      Class clazz=list.getClass();//      Method method=clazz.getMethod("add", Object.class);//      method.invoke(list, new Integer(129));//      for(int i=0;i<list.size();i++){//          System.out.println(list.get(i).getClass().getName());//      }    ArrayList<Integer> array=new ArrayList<Integer>();            array.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer            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
因为使用反射,是运行时动态运行的,所以跳过了编译器检验的过程,导致不报错,但这样做很危险,违反了泛型的初衷。而且还有一个问题,若果上述代码中注释部分被打开,是会报错的,不知道具体是什么原因。

二.泛型方法

泛型类我们之前已经讲了,也比较好理解,接下来我们看看泛型方法,

package wangcc.generics;import org.apache.log4j.Logger;public class RealGenericMethod {private static Logger logger = Logger.getLogger(RealGenericMethod.class);//这个类是个泛型类,在上面已经介绍过   public class Generic<T>{             private T key;        public Generic(T key) {            this.key = key;        }        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。        //所以在这个方法中才可以继续使用 T 这个泛型。        public T getKey(){            return key;        }        /**         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。        public E setKey(E key){             this.key = keu        }        */    }    /**      * 这才是一个真正的泛型方法。     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T     * 这个T可以出现在这个泛型方法的任意位置.     * 泛型的数量也可以为任意多个      *    如:public <T,K> K showKeyName(Generic<T> container){     *        ...     *        }     */    public <T> T showKeyName(Generic<T> container){        System.out.println("container key :" + container.getKey());        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。        T test = container.getKey();        return test;    }    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。    public void showKeyValue1(Generic<Number> obj){        logger.info(obj.getKey());     //  log.info("泛型测试","key value is " , obj.getKey());    }    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类    public void showKeyValue2(Generic<?> obj){        logger.info(obj.getKey());    }     /**     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。    public <T> T showKeyName(Generic<E> container){        ...    }      */    /**     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。     * 所以这也不是一个正确的泛型方法声明。    public void showkey(T genericObj){    }    */    public static void main(String[] args) {        }}

说明:

  • 1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
  • 2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
  • 3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
  • 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
    泛型方法和泛型类的区别:泛型类的实现是在类实例化的时候实现的,而泛型方法是在调用该方法的时候实现的。

    1. 静态方法与泛型:
      如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
      无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

三.泛型数组

四.泛型统配符

五.关于泛型相关的一些问题

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

原创粉丝点击