Java泛型详解

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

当初学习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// 再取第二个值时会出异常 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 newl = new ArrayList();

    @Test
    public void testNewList(){
        newl.add("hello");
        // 这里会直接拒绝加入Integer类型的元素
        //newl.add(100);
        newl.add("scora");

        for(int i=0;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 iterator();

    Object[] toArray();

     T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection c);

    boolean addAll(Collection c);

    boolean addAll(int index, Collection 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 listIterator();

    ListIterator listIterator(int index);

    List 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{
    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 a = new A("socra");

    @Test
    public void testA(){
        println(a.getE());
    }
}

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

    @Test
    public void testGenericityType(){
        A str = new A("socra");
        A no = new A(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();
    }
 }

反编译后查看其字节码发现:
Java泛型详解_第1张图片
注意红线画到的地方

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

public class Test{

    static class A{
        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 a = new A();
        a.setE("socra");
        String str = a.getE();
    }
}

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

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

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

泛型中的边界

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

泛型接口

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

public interface B<E>{
    public void setE(E e);

    public E getE();
}

class NewB implements B{
    // 泛型接口中的泛型对象定义在实现类中
    String name ;

    @Override
    public void setE(String str){
        this.name = str;
    }

    @Override
    public String getE(){
        return name;
    }
}

注意接口声明的小细节

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

泛型方法

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

    /**
     * 泛型方法
     *  用来声明该方法为泛型方法
     * @param t 参数类型对象
     * @return
     */
    public static  T display(T t){
        println("hello,这里是泛型方法");
        return t;
    }

    @Test
    public void testDisplay(){
        String name = "socra";
        String name2 = display(name);
        println(name2);
    }

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

    /**
     * 基于反射的泛型方法
     * Class 声明泛型的T的具体类型
     * @param t 是泛型T类的需要被代理的对象
     * @return 实例化的代理对象
     * @throws IllegalAccessException  安全权限异常
     * @throws InstantiationException  实例化异常
     */
    public  T getObject(Class 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 str = new A("socra");
        A no = new A(10);

        aPrintln(str); // socra
        aPrintln(no); // 10
    }

可以看到将A

通配符的上下界

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

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

泛型数组?

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

话尾

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

你可能感兴趣的:(Java,泛型,java,泛型)