《Java编程思想》第十五章 泛型

目录

 

前言:

1. 简单泛型

2. 泛型接口

3. 泛型方法

4. 类型擦除

5. 泛型边界和通配符

6. 异常

总结


前言:

本系列是我本人阅读java编程思想这本书的读书笔记,主要阅读第五章到第十七章以及第二十一章的内容,今天的笔记是第十五章

在面向对象的编程语言中,多态是一种泛化的手段,你可以把方法的参数设定为基类,那么该方法就可以接收从这个基类中派生的任何类型作为参数,但是这种行为大多数时候也会有一些性能损耗。局限于单继承体系,对程序来说限制还是太多,这个时候接口出现了,任何实现了该接口的类都能够满足该方法,这种限制就小了许多,但是还是不够,因为一旦指定了接口,你的代码就必须实现特定的接口。

这个时候,泛型出现了,泛型实现了参数化类型的概念,使得代码可以应用于多种类型,泛型是如何做到这一点的呢,就是通过解耦类或者方法与所使用的的类型间的约束。

1. 简单泛型

泛型的出现有这许多的原因,其中最引人注目的原因就是为了创建容器类。容器,就是存放一组对象的地方,通常情况下,这一组对象的类型应该保持相同,泛型的主要目的之一就是指定容器需要存放什么类型的对象,而且由编译器来保证类型的正确性。要达到这个目的,需要使用类型参数,用尖括号括起来,放在类名的后面,就比如这样

class Container<T>

T就是类型参数,这样当你要创建Container对象的时候必须指明类型,然后你就只能在Container中存放这个类型或者这个类型的子类的对象了,比如说你想在Container中存放一个A类型,那就要这么创建:Container<A> a=new Container<>(),而且不用担心,取出的元素也自动是正确的类型。java泛型的核心概念就是,告诉编译器想要什么类型,其他的细节就由编译器来处理。

2. 泛型接口

泛型也可以用于接口,例如生成器,这是一种专门用来创建对象的类,实际上,这是工厂方法设计模式的一种应用,因为一般的工厂方法需要参数,而使用生成器的话不需要给定参数。书中给出了一个例子

public interface Generator<T>{
    T next();
}
public class Coffee {
    private static long counter = 0;
    private final long id = counter++;
    public String toString() {
       return getClass().getSimpleName() + " " + id;
    }
}    
public class Latte extends Coffee{}
public class Mocha extends Coffee{}
public class Americano extends Coffee{}
public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee>{
    private Class[] types={Latte.class,Mocha.class,Americano.class};
    private static Random rand=new Random(47);
    public CoffeeGenerator(){}
    private int size = 0;
    public CoffeeGenerator(int sz){size=sz;}
    public Coffee next(){
        try{
            return (Coffee)types[rand.nextInt(types.length)].netInstance();
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }
    class CoffeeIterator implements Iterator<Coffee>{
        int count=size;
        public boolean hasNext(){return count>0;}
        public Coffee next(){
            count--;
            return CofferGenerator.this.next();
        }
        public void remove(){
            throw new UnsupportedOperationException();
        }
    };
    public Iterator<Coffee> iterator(){
        return new CoffeeIterator();
    }
    public static void main(String[] args){
        CoffeeGenerator gen=new CoffeeGenerator();
        for(int i=0;i<3;i++){
            System.out.println(gen.next());
        }
        for(Coffee c:new CoffeeGenerator(3)){
            System.out.println(c);
        }
    }
}

参数化的Generator接口确保next()返回值是参数的类型。

3. 泛型方法

目前为止,我们看到的泛型都是应用于类或者接口上,方法上也同样可以使用参数化类型,泛型方法使得方法能够独立于类而产生变化。要定义泛型方法也很简单,只要把泛型类型参数列表放在返回值之前就像这样

public <T> void f()

4. 类型擦除

java中的泛型,其实是伪泛型,因为java在编译期间,会把所有的泛型信息擦掉,在编译后生成的字节码中是不包含泛型中的类型信息的,也就是说比如你定义了List<String>,但是经过编译后其实还是List,下面用一个例子来证明泛型中存在类型擦除

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}

public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

泛型的类型擦除实际上会引起多态冲突,比如

Pair<Date> p = new DateInter();
p.setSecond(new Date());
//这里就无法实现多态想要达到的效果因为它会直接调用从父类继承的参数为Object类型的setSecond方法

java是怎么解决类型擦除引起的多态冲突呢,答案是桥方法,这里引入我之前写过的一篇博文,简单的阐述了什么是桥方法以及协变返回类型https://www.cnblogs.com/wangjimmy/p/6903685.html

还要说明一点,泛型类型不能是基本数据类型,必须是基本数据类型的包装类。

5. 泛型边界和通配符

在第十一章的时候我们已经见到了一些通配符的示例,在泛型参数表达式中使用?,?表示无界通配符,对于不确定或者不关心实际要操作的类型,可以使用无界通配符,表示可以持有任何类型,当无界通配符使用在容器类中时,需要注意一个细节,来看个例子

public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

public static void main(String[] args) {
    List<String> l1 = new ArrayList<>();
    l1.add("aa");
    l1.add("bb");
    l1.add("cc");
    printList(l1);
    List<Integer> l2 = new ArrayList<>();
    l2.add(11);
    l2.add(22);
    l2.add(33);
    printList(l2);
    
}

这里printList方法的参数中,list加上了无界通配符,使得list既可以接收string类型,也可以接收integer类型,但是有一个操作是不被允许的,那就是不能对list执行add操作,因为list不知道自己持有的具体类型是什么,但是也有例外那就是可以add(null),因为null是所有引用类型都有的元素。同样的,对list也不能执行get操作,但是object类型是个例外,因为object是所有引用类型的父类。

说到通配符那就还有一个边界的问题,当你需要把泛型参数类型限定在一个范围的时候,通配符的边界就起了作用,通配符有上界"<? extends E>",有下界"<? super T>"。上界用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。下界用super关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object。

6. 异常

简单一句话,由于类型擦除的原因,catch语句不能捕获泛型类型的异常。

总结

这一章我们学习了java的泛型,了解了泛型的基本概念,知道了泛型的使用场景,学习了泛型的通配符以及上下界,到此,本章就结束了。

发布了31 篇原创文章 · 获赞 9 · 访问量 2541
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 1024 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览