• Java泛型与数组深入研究


    Java中的泛型与数组平时开发用的很多,除了偶尔遇到"NullPointerException"和"IndexOutOfBoundsException"一般也不会遇到太大问题。可是如果深入研究,可能会发现这两种类型有很多奇怪的特点。我查了一些资料,发现包括《Java编程思想》在内对这些问题的解释都含糊其辞(不排除是本人理解能力有限)。因此在大量实践的基础上,我只能提出自己的对这些问题的理解并总结下来。

    一、数组转型

    准备工作

    创建三个表示继承关系的类,供测试使用:

    class Fruit {
    }
    
    class Apple extends Fruit {
    }
    
    class RedApple extends Apple {
    }

    数组能够转型吗?

    // 测试:数组转型
    public void arrayTransform() {
        Fruit[] fruits = new Apple[10];
        Fruit[] newFruits = new Fruit[10];
        newFruits[0] = new Apple();
        newFruits[1] = new Apple();
        Apple[] apples = (Apple[]) newFruits; // 抛出异常java.lang.ClassCastException
    }

    编译期不报错,运行期抛异常。而且这个错误与newFruits所持有的实际对象无关。如果要强制转型只能遍历数组来完成。要理解这个问题本质,我们需要首先了解Java的数组内存模型。

    数组的内存模型

    数组转型,JVM只会检查它的直接类型。上面的例子中,对于Apple[]而言Fruit[]是它的父类,因此可以直接转型。而向下转型的时候,相当于Fruit转型Apple。因此在运行期报错。不过事情似乎不那么简单,Fruit[]真的是Apple[]的父类吗?

    public void arraySuperclass() {
        Fruit[] fruits = new Fruit[10];
        Apple[] apples = new Apple[10];
        System.out.println(fruits.getClass());
        System.out.println(apples.getClass());
        System.out.println(fruits.getClass().getSuperclass());
        System.out.println(apples.getClass().getSuperclass());
    }
    /* Output:
    class [LFruit;
    class [LApple;
    class java.lang.Object
    class java.lang.Object
    */

    从输出看,似乎数组之间并没有相互的继承关系。那么这样的转型到底是如何实现的呢?根据JVM的规范,数组类型是由虚拟机在运行时创建的,控制台输出的前两行即代表数组类型。同时JVM规范也定义了,数组的父类型为数组元素的父类型。对于数组,我们的总结如下:

    1. 数组类型只和声明类型相关与它的实际持有类型无关。
    2. 数组类的继承关系与声明类型的继承关系保持一致。

    二、泛型通配符

    extends的含义

    public void genericExtends(List<? extends Fruit> ls) {
        ls.add(new Apple()); // 编译报错
        Fruit f = ls.get(0);
    }

    使用extends修饰的泛型容器只能调用get()方法,并且genericExtends方法可以接受List<Fruit>、List<Apple>和List<RedApple>类型的参数。

    Java泛型是一种妥协的产物,实际在泛型内部无法知晓实际类型。Java为了实现泛型采用一种被称为“擦除”的机制,并且这种机制在编译期完成。这样做的目的是为了和老版本的.class文件保持兼容。extends本质是一种擦除的约束条件,表示List<? extends Fruit>对象将会被擦除成List<Fruit>,因此在方法内部开发人员可以调用在Fruit的所有方法。但是必须明确一点,List<? extends Fruit>与List<Fruit>绝不一样。这一点和数组类型相似,任何以Fruit的子类为泛型的容器类(如:List<Apple>)并不是Fruit容器类(如:List<Fruit>)的子类。如果必须要说它们之间存在什么共性的话就是以Fruit的子类为泛型的容器类可以在编译期被擦除为Fruit的容器类。要如何实现这一点呢?Java提出了在泛型中利用extends作为修饰符。因此,List<? extends Fruit>声明的本质是告诉编译期,请尝试把方法的参数(如:List<Apple>)擦除成List<Fruit>而不要直接擦除成List<Object>,如果成立则编译通过。

    因此在genericExtends方法的内部,ls表示它可能为List<Fruit>、List<Apple>和List<RedApple>之中的任何一个类型。因此ls不能使用add方法想容器中添加元素。

    super的含义

    public void genericSuper(List<? super Apple> ls) {
        ls.add(new Apple());
        ls.add(new RedApple());
    }
    
    public void methInvok() {
        List<Fruit> fruits = new ArrayList<>();
        this.genericSuper(fruits);
    }

    如果希望在方法内部可以向容器中添加元素,则推荐的做法是使用super修饰泛型。以它作为泛型修饰符的方法形参,从外部看表示:我可以接收以泛型声明类自身或所有父类为泛型的容器类,如(List<Apple>或List<Fruit>)。它相当于容器添加了一个下界。从内部看表示:参数ls能够接收所有泛型声明类自身或为基类的对象,如(Apple 或 RedApple)。

    关于泛型通配符,我们总结如下:

    1. 包含通配符的泛型对象在编译期会被擦除,利用extends和super能够为为擦除行为增加约束。
    2. 通配符表示为某一种明确的类型,具体类型只有在运行期可知。
  • 相关阅读:
    拍照上传图片方向调整
    js 压缩上传图片
    js 各种循环语法
    本地Git仓库对照多个远程仓库
    nrm安装与配置使用
    面试常见问题
    NodeJs文件路径
    vscode添加智能提示(typings)
    前端常用的工具库
    DeepMask学习笔记
  • 原文地址:https://www.cnblogs.com/learnhow/p/12312825.html
Copyright © 2020-2023  润新知