Java泛型详解

来源:互联网 发布:宜信大数据 编辑:程序博客网 时间:2024/05/04 16:17

为什么要来再详究一遍泛型

当初学习Java时并没有觉得这个有多重要,又不像C++,我有现成的集合框架可以使用,我管你泛型干吗,(滑稽
现在慢慢的学到了JavaEE的一些知识,所起来,框架中的原理知识除了有Java的反射机制,还大量的用到了泛型的知识,随便点开一个方法的源码,很容易发现有泛型的痕迹。但是仔细一想,这点似乎并没有搞清楚,所以

正文

RT,本次讨论的主要目标,泛型。为了节省时间,一下的研究主要内容来自先驱者的博文指导,所以可以算是转载,侵删

为什么需要泛型

这个探讨的节奏深得我心啊,先说是不是,再问为什么(滑稽
先看一段代码:

/** * 主要是为了深入了解学习 泛型 * NewPrint类是写的简化输出的工具类 */public class TestGeneric extends NewPrint{    List list = new ArrayList();    @Test    public void test(){        list.add("hello");        list.add(100);        for(int i=0;i<list.size();i++){            // 再取第二个值时会出异常 java.lang.ClassCastException            String name = (String)list.get(i);            println("name: "+name);        }    }}

测试运行时会报出异常java.lang.ClassCastException,这是类型不匹配的异常。List默认的类型是Object类型的,什么类型的对象都可以往里面装。装入时是Integer类型的然后强制转为String类型自然会出错
从上面可以看出两个问题:

  • 当一个对象放入集合中时,集合并不会记住这个对象本来的类型;当该对象从集合中取出时,它的编译类型就变成了Object类型,但运行时还是会按照其本来的类型运算(这就是为什么编译时不会报错,允许强制转换,运行时却出异常的原因)
  • 当从一个集合中取出对象时,因为可能不知道其真实类型而去强制转换,这是很容易触发java.lang.ClassCastException

所以就有了这么 一个需求:如何可以使集合“记住”元素的类型,并在运行时不会出现java.lang.ClassCastException的异常呢?

什么是泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

简单说就是将一个类型当作参数传入另一个接口/类/方法的参数

于是将上面代码改为:

public class TestGeneric extends NewPrint{    List<String> newl = new ArrayList<String>();    @Test    public void testNewList(){        newl.add("hello");        // 这里会直接拒绝加入Integer类型的元素        //newl.add(100);        newl.add("scora");        for(int i=0;i<newl.size();i++){            // 再取第二个值时会出异常 java.lang.ClassCastException            String name = newl.get(i);            println("name: "+name);        }    }}

采用泛型写法后,当想插入非String类型的对象时就会直接提示出错,同时当从集合中取值时也没有必要强制类型转换。
可以得知在List中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。
看一看List的源码:

public interface List<E> extends Collection<E> {    int size();    boolean isEmpty();    boolean contains(Object o);    Iterator<E> iterator();    Object[] toArray();    <T> T[] toArray(T[] a);    boolean add(E e);    boolean remove(Object o);    boolean containsAll(Collection<?> c);    boolean addAll(Collection<? extends E> c);    boolean addAll(int index, Collection<? extends E> c);    boolean removeAll(Collection<?> c);    boolean retainAll(Collection<?> c);    void clear();    boolean equals(Object o);    int hashCode();    E get(int index);    E set(int index, E element);    void add(int index, E element);    E remove(int index);    int indexOf(Object o);    int lastIndexOf(Object o);    ListIterator<E> listIterator();    ListIterator<E> listIterator(int index);    List<E> subList(int fromIndex, int toIndex);}

在List接口中采用泛型化定义之后,中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
注意一下这两个常用的方法:

  • boolean add(E e);
  • E get(int index);

第一个方法一定需要类型的参数,第二个方法一定会返回一个类型的对象,这也就解释了上面为什么add加入一个非String类型的值会直接提示出错,为什么从集合中取值不再需要强制类型转换。
当然了,这只是List接口的定义,ArrayList实现类既然实现了List,那么一定会重写add()方法和get()方法,所以其也是需要类型的参数的

使用泛型的好处

谈完了什么是泛型,按照我的节奏,我一般都会去想一想使用它的好处都有什么

  • 类型安全:在使用时对一个对象进行了限制,只有约定类型的对象才能继续,编译器在编译时期也可以进行类型检查
  • 避免强制类型转换:因为前面已经约束了类型,所以在使用时就已知了类型,便省去了类型转换的过程,使得代码更加可读,也减少了出错的机会
  • 潜在的性能收益:泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。

泛型类、泛型接口和泛型方法

大概知晓了泛型的知识,来看看我们如何使用泛型:

泛型类

使用示例:

class A<E>{    private E e;    public A(){ }    public A(E e){        this.e = e;    }    public void setE(E e){        this.e = e;    }    public E getE(){        return e;    }}public class MyGenericityClass extends NewPrint{    A<String> a = new A<String>("socra");    @Test    public void testA(){        println(a.getE());    }}

对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

    @Test    public void testGenericityType(){        A<String> str = new A<String>("socra");        A<Integer> no = new A<Integer>(10);        println(str.getClass()); // class Genericity.A        println(no.getClass()); // class Genericity.A        System.out.println(str.getClass()==no.getClass()); // true    }

输出结果竟不是预料的,原以为在编译时,编译器会将所有的泛型擦除变成其真实的类型,但现在看来似乎不是这样

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

很奇怪,不是吗?(《Think in Java》P372之后的章节有详述

泛型中“擦除”

一个很玄学的东西:Java泛型中的具体类型信息在运行时都被擦除了,而被当作泛型类型的对象去使用。(在泛型代码内部是无法获取任何有关泛型参数类型的信息)

在基于擦除的实现中,泛型类型被当作第二类类型被处理(没有具体化),即不能在某些重要的上下文中使用的类型。泛型类型只有在静态类型检查期间才会出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,List这样的类型注解被擦除为List,而普通的类型变量类型在未指定边界的情况下将被擦除为Object

那么问题来了,我们是如何得知泛型中参数类型的呢?毕竟我们在运行时还需要检查其类性呢
首先在编写代码时进行检查就容易实现了,编辑器自动检测泛型类型是否一致,下面来看看编译运行时的检测办法:
首先是一点不使用泛型的代码:

 public class Test2{     static class A{         private Object obj;        public A(){ }        public A(Object obj){            this.obj = obj;        }        public void setObject(Object obj){            this.obj = obj;        }        public Object getObject(){            return obj;        }     }     public static void main(String[] args){        A a = new A();        a.setObject("socra");        String str = (String)a.getObject();    } }

反编译后查看其字节码发现:
这里写图片描述
注意红线画到的地方

接下来看同样操作使用泛型的写法:

public class Test{    static class A<E>{        private E e;        public A(){ }        public A(E e){            this.e = e;        }        public void setE(E e){            this.e = e;        }        public E getE(){            return e;        }    }       public static void main(String[] args){        A<String> a = new A<String>();        a.setE("socra");        String str = a.getE();    }}

同样反编译查看字节码可以发现:
这里写图片描述
可以看到两者的字节码是一样的,然后注意到checkcast这个部分,这是检查类型的语句,事实上这才是关键所在。
编译时擦除了泛型的参数类型信息,在编译时在边界地方开始检查类型,所谓边界就是对象进入和离开的地方。

  • 在实例一中,会在强制转型的地方开始检测参数类型;
  • 在实例二中,会在调用getObject()方法处检查参数类型

综上,我们知道为什么泛型可以知道参数类型信息了(先擦除后检查类型,毕竟泛型的主要目的之一就是希望将错误检测移入到编译期

泛型中的边界

边界可以在泛型的参数类型上设置限制条件,例如class A<T extends B>,表示的意思就是参数类型必须是B类型或者是继承自B的子类

泛型接口

看过了上面泛型类的例子,就知道泛型接口就是接口有参数类型

public interface B<E>{    public void setE(E e);    public E getE();}class NewB implements B<String>{    // 泛型接口中的泛型对象定义在实现类中    String name ;    @Override    public void setE(String str){        this.name = str;    }    @Override    public String getE(){        return name;    }}

注意接口声明的小细节

  • 接口的默认访问修饰符是protected
  • 接口中的属性只能是static或final修饰的已知类型的对象,同时接口中不允许声明构造方法
  • 泛型对象需要在实现类中定义

泛型方法

关注到这个是因为学到了hibernate中的某一个方法,看一看Java中的泛型方法。

    /**     * 泛型方法     * <T> 用来声明该方法为泛型方法     * @param t 参数类型对象     * @return     */    public static <T> T display(T t){        println("hello,这里是泛型方法");        return t;    }    @Test    public void testDisplay(){        String name = "socra";        String name2 = display(name);        println(name2);    }

这里还有dalao提供的进阶版泛型方法,当然了,框架中使用的泛型方法就是这种类型:

    /**     * 基于反射的泛型方法     * Class<T> 声明泛型的T的具体类型     * @param t 是泛型T类的需要被代理的对象     * @return 实例化的代理对象     * @throws IllegalAccessException  安全权限异常     * @throws InstantiationException  实例化异常     */    public <T> T getObject(Class<T> t) throws InstantiationException, IllegalAccessException{        T newt = t.newInstance(); // 基于反射创建对象        return newt;    }

类型通配符

从上面的例子中,可以得知A和A其实还是一种类型,那么能否将这两种类型看作是与A类型有关系的父子类型呢?
这里就需要有一个引用类型,用来在逻辑上表示形如A和A父类的引用类型。这就引出了我们的关注焦点——类型统配符。

神奇的 ‘?’

java中类型通配符一般是使用?代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且A

    public static void aPrintln(A<?> a){        println(a.getE());    }    @Test    // 用以测试通配符    public void testWildcard(){        A<String> str = new A<String>("socra");        A<Integer> no = new A<Integer>(10);        aPrintln(str); // socra        aPrintln(no); // 10    }

可以看到将A

通配符的上下界

其实这是泛型边界的定义,上文也有说到,但边界也可用于通配符中

  • 类型通配符上界:<? extends T>,必须是T类或者其子类
  • 类型通配符下界:<? super E>,必须是E类或者是E类的父类

泛型数组?

不存在的,Java中没有泛型数组这么一说,所有想用到泛型数组的地方都可以使用List<E>来代替

话尾

一不小心怎么研究了这么多,前前后后加上翻书查资料加上做做小实验,4个小时+应该是有的,不敢说翻了个底朝天,掌握大部分应该是有的。
其实很喜欢这种状态。
当然了,对Java掌握的越深越好啊 :-),还是那句话,先狗后人

1 0