美文网首页
Java泛型

Java泛型

作者: bertrand319 | 来源:发表于2018-11-04 17:11 被阅读0次

    简介

    JDK 5.0以后增加了几个新的扩展功能,其中一个就是泛型。泛型允许我们把类型泛化。以5.0版本以前的Collections为例子:

    List myIntList = new LinkedList(); // 1
    myIntList.add(new Integer(0)); // 2
    Integer x = (Integer) myIntList.iterator().next(); // 3 
    

    比如说第三行,程序员实际上是知道数据是什么类型,但不得不进行强制转化,因为编译器只能保证从iterator返回的类型是Object。当然有时候程序员也会犯错,比如说记错返回类型导致运行时错误。
    为了避免上述问题,引入泛型,改造如下

    List<Integer> myIntList = new LinkedList<Integer>(); // 1'
    myIntList.add(new Integer(0)); // 2'
    Integer x = myIntList.iterator().next(); // 3'
    

    通过传入类型参数Integer,在第三步把烦人的强制转化给去掉了,编译器现在在编译阶段确保返回的类型安全,避免了上述问题,这也增强了程序的可读性及稳定性。

    定义基本泛型

    拿List作为例子

    public interfacep  List <E> {
        void add(E x);
        Iterator<E> iterator();
    }
    

    在简介中我们定义了List<Integer>,我们或者可以认为List<Integer>是E被Integer代表的List的版本,类似下面的代码:

    public interface IntegerList {
        void add(Integer x);
        Iterator<Integer> iterator();
    }
    

    但实际上在Java中,源文件、可执行文件、硬盘已经内存中都不会存在多份不同类型的List实现,这跟C++的模板实现方式很不一样。泛化类只会编译一次,生成一个class文件,跟原来的类或者接口生成一样。

    泛型及子类型

    比如下面的代码

    List<String> ls = new ArrayList<String>(); // 1
    List<Object> lo = ls; // 2 
    

    第一步明显不会有问题,第二步是不是合法呢,也就是说List<String>是不是List<Object>的子类。为了确认一下,我们增加多几行代码

    lo.add(new Object()); // 3
    String s = ls.get(0); // 4: 试图把Object强制转化成String
    
    

    假设第二步合法,则我们可以在lo中插入任意对象。因此ls可能会包含非String对象,从而有可能导致出错。
    实际上第2步代码,编译器在编译阶段就会报编译错误,因为List<String>不是List<Object>的子类。

    通配符

    假设需要为collection写一个打印所有元素的方法,在5.0版本以前可以这样写

    void printCollection(Collection c) {
        Iterator i = c.iterator();
        for (k = 0; k < c.size(); k++) {
            System.out.println(i.next());
        }
    }
    

    5.0版本以后尝试写一个新的通用版本,比如下面的代码

    void printCollection(Collection<Object> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
    

    实际上这个新版本会不如老的那个版本。老的版本可以支持任意的collection类型,而新版本只支持Collection<Object>,因为Collection<Object>并不是我们想要任意collection类型的父类。

    那什么才是任意collection的父类呢?Collection<?>,?就是所说的通配符,表示未知类型的collection,可以匹配任意的collection。代码改写如下

    void printCollection(Collection<?> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
    

    上面代码中我们读取出来的元素赋予Object类型,这样做是安全的,因为无论是哪类型的collection,它里面的元素都可以转成Object。但是,向里面添加任意对象确是不安全的,例如

    Collection<?> c = new ArrayList<String>();
    c.add(new Object()); // 编译错误
    

    由于?代表未知类型,所以不能把Object添加进去。

    带限定的通配符

    比如说以下这段代码

    public abstract class Shape {
        public abstract void draw(Canvas c);
    }
    
    public class Circle extends Shape {
        private int x, y, radius;
        public void draw(Canvas c) {
            ...
        }
    }
    
    public class Rectangle extends Shape {
        private int x, y, width, height;
        public void draw(Canvas c) {
            ...
        }
    }
    
    public class Canvas {
        public void draw(Shape s) {
            s.draw(this);
       }
    }
    

    每个画布上实际上有一系列形状要画,假设我们添加一个drawAll的函数

    public void drawAll(List<Shape> shapes) {
        for (Shape s: shapes) {
            s.draw(this);
       }
    }
    

    很明显上述只能支持List<Shape>的绘制,与我们的预期不符合。通过带限定的通配符,可以改成

    public void drawAll(List<? extends Shape> shapes) {
        ...
    }
    

    通过这种方式,告诉编译器,?号代表的未知类型实际上是Shape的自身或者子类。

    泛型方法

    假设我们需要写一个方法,把Object数组的元素全部放入Collection中,假设代码如下

    static void fromArrayToCollection(Object[] a, Collection<?> c) {
        for (Object o : a) { 
            c.add(o); // 编译错误
        }
    }
    

    当然通过上述章节,可以把Collection<?>改成Collection<Object>,但这并非是我们想要的。通过泛型方式改造

    static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
        for (T o : a) {
            c.add(o); // 正确
        }
    }
    

    上面会涉及到一个问题,什么时候使用通配符,什么时候需要使用泛型方法。为了了解,我们看一下Collection内部的几个方法

    interface Collection<E> {
        public boolean containsAll(Collection<?> c);
        public boolean addAll(Collection<? extends E> c);
    }
    

    我们可以用泛型方法重写

    interface Collection<E> {
        public <T> boolean containsAll(Collection<T> c);
        public <T extends E> boolean addAll(Collection<T> c);
    }
    

    可以看到,在两个方法中,T实际上只是被用到1次,同时返回值并不依赖于T。而在上述例子中只是为了支持不同类型的Collection可以被传入而已,返回值也跟传入的类型参数无关,使用通配符更加适合。

    关于泛型的Class

    假设有以下这段代码

    List <String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    System.out.println(l1.getClass() == l2.getClass());
    

    第3步返回的结果实际上是true,因为无论泛化类型是否一样,同样泛型类的实体对象在运行时的Class都一样。

    的确,事实上一个泛型化的类是指该类支持无论传入任何的泛化类型都遵守同样的行为。因此,泛化参数和泛化修饰符无法作用于静态方法或者静态区域,因为静态方法类的所有的实例都会公用,违背上述原则。

    基于上面的描述,假设以下的代码

    Collection cs = new ArrayList<String>();
    if (cs instanceof Collection<String>) { ... }
    

    第2步实际上非法,因为所有的泛型类实例共享同一个泛型类,并不存在Collection<String>。同样的,如下面的代码

    Collection<String> cstr = (Collection<String>) cs; //1
    
    <T> T badCast(T t, Object o) {
        return (T) o;    // 2
    }
    

    第1段代码会抛出Unchecked warning,因为Collection<String>在运行时并不存在,所以运行时系统实际上并不会做检查。同样,第3段代码中T在运行时也不存在。这表示使用过程中我们并不能保证代码安全。

    相关文章

      网友评论

          本文标题:Java泛型

          本文链接:https://www.haomeiwen.com/subject/yhvxxqtx.html