Java泛型详解

1 篇文章 0 订阅

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

当初学习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
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java泛型Java 5引入的新特性,可以提高代码的可读性和安全性,降低代码的耦合度。泛型是将类型参数化,实现代码的通用性。 一、泛型的基本语法 在声明类、接口、方法时可以使用泛型泛型的声明方式为在类名、接口名、方法名后面加上尖括号<>,括号中可以声明一个或多个类型参数,多个类型参数之间用逗号隔开。例如: ```java public class GenericClass<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T> { T getData(); void setData(T data); } public <T> void genericMethod(T data) { System.out.println(data); } ``` 其中,`GenericClass`是一个泛型类,`GenericInterface`是一个泛型接口,`genericMethod`是一个泛型方法。在这些声明中,`<T>`就是类型参数,可以用任何字母代替。 二、泛型的使用 1. 泛型类的使用 在使用泛型类时,需要在类名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericClass<String> gc = new GenericClass<>(); gc.setData("Hello World"); String data = gc.getData(); ``` 在这个例子中,`GenericClass`被声明为一个泛型类,`<String>`指定了具体的类型参数,即`data`字段的类型为`String`,`gc`对象被创建时没有指定类型参数,因为编译器可以根据上下文自动推断出类型参数为`String`。 2. 泛型接口的使用 在使用泛型接口时,也需要在接口名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericInterface<String> gi = new GenericInterface<String>() { private String data; @Override public String getData() { return data; } @Override public void setData(String data) { this.data = data; } }; gi.setData("Hello World"); String data = gi.getData(); ``` 在这个例子中,`GenericInterface`被声明为一个泛型接口,`<String>`指定了具体的类型参数,匿名内部类实现了该接口,并使用`String`作为类型参数。 3. 泛型方法的使用 在使用泛型方法时,需要在方法名前面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java genericMethod("Hello World"); ``` 在这个例子中,`genericMethod`被声明为一个泛型方法,`<T>`指定了类型参数,`T data`表示一个类型为`T`的参数,调用时可以传入任何类型的参数。 三、泛型的通配符 有时候,我们不知道泛型的具体类型,可以使用通配符`?`。通配符可以作为类型参数出现在方法的参数类型或返回类型中,但不能用于声明泛型类或泛型接口。例如: ```java public void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } } ``` 在这个例子中,`printList`方法的参数类型为`List<?>`,表示可以接受任何类型的`List`,无论是`List<String>`还是`List<Integer>`都可以。在方法内部,使用`Object`类型来遍历`List`中的元素。 四、泛型的继承 泛型类和泛型接口可以继承或实现其他泛型类或泛型接口,可以使用子类或实现类的类型参数来替换父类或接口的类型参数。例如: ```java public class SubGenericClass<T> extends GenericClass<T> {} public class SubGenericInterface<T> implements GenericInterface<T> { private T data; @Override public T getData() { return data; } @Override public void setData(T data) { this.data = data; } } ``` 在这个例子中,`SubGenericClass`继承了`GenericClass`,并使用了相同的类型参数`T`,`SubGenericInterface`实现了`GenericInterface`,也使用了相同的类型参数`T`。 五、泛型的限定 有时候,我们需要对泛型的类型参数进行限定,使其只能是某个类或接口的子类或实现类。可以使用`extends`关键字来限定类型参数的上限,或使用`super`关键字来限定类型参数的下限。例如: ```java public class GenericClass<T extends Number> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T extends Comparable<T>> { T getData(); void setData(T data); } ``` 在这个例子中,`GenericClass`的类型参数`T`被限定为`Number`的子类,`GenericInterface`的类型参数`T`被限定为实现了`Comparable`接口的类。 六、泛型的擦除 在Java中,泛型信息只存在于代码编译阶段,在编译后的字节码中会被擦除。在运行时,无法获取泛型的具体类型。例如: ```java public void genericMethod(List<String> list) { System.out.println(list.getClass()); } ``` 在这个例子中,`list`的类型为`List<String>`,但是在运行时,`getClass`返回的类型为`java.util.ArrayList`,因为泛型信息已经被擦除了。 七、泛型的类型推断 在Java 7中,引入了钻石操作符<>,可以使用它来省略类型参数的声明。例如: ```java List<String> list = new ArrayList<>(); ``` 在这个例子中,`ArrayList`的类型参数可以被编译器自动推断为`String`。 八、总结 Java泛型是一个强大的特性,可以提高代码的可读性和安全性,降低代码的耦合度。在使用泛型时,需要注意它的基本语法、使用方法、通配符、继承、限定、擦除和类型推断等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值