在java中,集合操作有两种方式——容器、数组;
容器相比较于数组,多了可扩展性,这里仅以容器的代表List,来对比和数组的关系。

都知道在java引入的泛型和自动拆装箱等语法糖后,集合操作也变得简单安全。
也都知道其实泛型在到字节码层面上时,会被擦除,虽然字节码中还会保留泛型参数(可以利用反射看到),但对真实的的类并不产生多大影响。

那么,对于List来说,如果泛型存在继承关系,是否可以强行转换呢?

一、List泛型类型转换

先看一个关于泛型的代码:

List<Number> numbers=new ArrayList<>();

//无法直接赋值 :Incompatible types
//List<Object> objects=numbers;

//无法进行类型转换:Inconvertible types; cannot cast 'java.util.List<java.lang.Number>' to 'java.util.List<java.lang.Object>
//List<Object> objectss=(List<Object>)numbers;

如果放开注释,编译器会直接报错;错误警告在注释中有标出;

可以看到,虽然泛型参数不会改变List类的类型,但在有了泛型之后,无法直接赋值,也无法进行类型的强行转换

那怎么样才能赋值成功呢?

List<Number> numbers=new ArrayList<>();
List presenter =numbers;
List<Object> objects=presenter;

只有这样:先消除泛型处理,然后再直接赋值

虽然说换了一种方式,但其实代码逻辑与之前错误的那种并无不同,那为什么编译器对上一种操作方式不认可呢?

这个需要继续看接下来的代码:

List<Number> numbers=new ArrayList<>();
numbers.add(new Integer(200));
List presenter =numbers;
List<Date> objects=presenter;
Date date = objects.get(0);

这里的操作是这样的:

  1. 新建List容器,存储Number类型
  2. 将一个Integer类型对象放入
  3. 通过去泛型操作将存储Number类型的List容器引用赋值给存储Date类型的List容器
  4. 通过get方法获取容器中第一个元素(真实类型为Number),然后强转为Date类型

这段代码完全符合Java语法,但真正运行时就会出现异常:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.util.Date

由此可见,最开始一段代码中,进行非相同类型相互赋值是很有风险的,因此编译器对此做了特殊处理,Java是一门基于安全的语言,这种很危险的事情当然会有相应的警告,但要是人为使坏(比如通过反射向容器中添加元素),那就没办法了。

同样的,除了容器泛型外,数组有时候也需要进行一些类型转换操作,那么数组会和泛型容器一样出现相同的问题么?

二、类型数组

Java中枚举类型enum继承自Enum类,但数组类可不继承自Array类,对于容器类来说不允许的操作,数组类确是允许的:

Integer[] integers=new Integer[2];
Number[] numbers=integers;

如上代码所示,一个Integer类型数组,可以直接赋值给Number类型数组

与集合不同的是,泛型不同的List所对应的class类相同类型数据对应的class类不同

因此说到底,泛型集合根本不是强制类型转换,因为表面类型(和静态分派是所说的类型相似)并没有发生变化。这点可以通过编译器调试模式REPL来进行分析;

1、基本类型对应的数组

java中虽然说基本类型(如int)可以和包装类型(如Integer)自动拆装箱,仿佛随时随地可以相互转化,但其实只是表面语法糖而已,同样的,基本类型数组和对象数组也存在不同之处。

先写出一些实例代码用于调试:

int[] a=new int[3];

然后进入调试模式查看变量a到底是什么类型:

① 数组类型

这里写图片描述

这里写图片描述

可以看到,int[] 对应的类型为 [I

② 类方法、类成员

这里写图片描述

这里写图片描述

这个类型其实是JVM内部自动生成的,在外部无法访问到。

这里我们可以发现,竟然没有任何成员,也没有任何方法。我们平时使用数组的时候,明明是使用了length属性的,这里为什么没有显示呢?

看这个代码:

int[] a = new int[3];
int length = a.length;

生成的字节码是这样的:

...
5 arraylength
...

对于字节码来说,如果length真的是属性,那么应该是通过 getfield 命令来获取,这里显然不是这样。

对于数组元素的提取,则是使用了其他的字节码指令(如aastore等)来完成的。因此确实没有length这个成员域。

关于数组为何要使用length属性来获取长度,可以参阅:为什么使用length获取Java数组的长度

③ 接口

这里写图片描述

这里实现了Cloneable和Serializable,可以进行序列化和克隆。

④ 父类

这里写图片描述

这里看到,基本类型数组的父类为Object,这也符合情况,因为一般来说基本类型数组对象最多也只是将引用赋值给Object类型引用而已。

2、引用类型对应的数组

首先来思考一下,引用类型数组和基本类型数组是否基本类似?

使用一下之前的代码:

Integer[] integers = new Integer[5];
Number[] numbers = integers;
Integer[] is = (Integer[]) numbers;

这里所有的操作都是有效的,一般而言:

  1. 子类实例同样是父类的实例
  2. 只有类型见存在继承关系,才能进行强制类型转换,并且只有真实类型为子类时,强转才不会出错

那么我们有理由推理,Integer[] 是Number[] 的子类。

和基本类型一样,进行调试测试一下类型到底是什么:

① 引用数组类型

这里写图片描述

改数组类型为: [java.lang.Integer
格式和基本类型都一致,中括号表示一维,Integer表示类型

② 接口

这里写图片描述

和基本数组类型一样,实现了克隆和序列化接口

③ 父类

这里写图片描述

看到这里,我们发现与我们的推理有些不符合了,Integer[] 的父类竟然直接就是Object。

是猜想有问题么?但如果真的 Integer[] 和 Number[] 都是Object的直接子类,那两者强转肯定会类型转型转换异常的吧。

因此这里我们直接使用java代码去进行测试。

通常我们一般会使用 instanceof 操作符来判断继承关系

integers instanceof [java.lang.Integer

这样很明显是不行的,我们根本无法直接表示出数组类来,所以还得使用一般的方式:

integers instanceof Number[]

执行后可以看到,结果为 true ,这就很好了,我们完全可以通过 Integer 类的继承关系,找出 Integer[] 类的继承关系:

这里写图片描述

3、多维数组类型

通过调试模式我们可以验证 一维数组继承关系,但如果现在是多维数组,那么该如何处理呢?

这里我们可以直接看类继承是如何来判断的。

通用的 instanceof 操作符是java语法自带的,根本没法看到具体逻辑,因此我们需要查看继承判断方式的源码:

  • Class.isInstance()
  • Class.isAssignableFrom()

两者的判断逻辑其实是一样的:

public boolean isInstance(Object obj) {
    if (obj == null) {
        return false;
    }
    return isAssignableFrom(obj.getClass());
}

Class.isInstance() 方法会先判断是否为null,为null的话直接返回false;否则调用Class.isAssignableFrom()

public boolean isAssignableFrom(Class<?> cls) {
    if (this == cls) {
        return true;  // Can always assign to things of the same type.
    } else if (this == Object.class) {
        return !cls.isPrimitive();  // Can assign any reference to java.lang.Object.
    } else if (isArray()) {
        return cls.isArray() && componentType.isAssignableFrom(cls.componentType);
    } else if (isInterface()) {
        // Search iftable which has a flattened and uniqued list of interfaces.
        Object[] iftable = cls.ifTable;
        if (iftable != null) {
            for (int i = 0; i < iftable.length; i += 2) {
                if (iftable[i] == this) {
                    return true;
                }
            }
        }
        return false;
    } else {
        if (!cls.isInterface()) {
            for (cls = cls.superClass; cls != null; cls = cls.superClass) {
                if (cls == this) {
                    return true;
                }
            }
        }
        return false;
    }
}

这里进行的判断逻辑如下:这里用本类和目标类表示this与cls

  1. 如果本类和目标类相同,那么返回true
  2. 如果本类为Object,目标类不是基本数据类型,返回true
  3. 如果本类为数组,目标类也为数组,则使用本类和目标类的componentType来调用isAssignableFrom进行判断
  4. 如果本类为接口,则获取目标类所有实现的接口,然后逐个判断是否与 自身相同
  5. 如果本来为其他一般类,则逐级获取目标类的父类,直到Object,看是否有某一级父类会与自身相同。

这个逻辑很简单,这里直接看第三步对于数组类型的判断逻辑,其中有提到componentType,那么这个是什么存在呢?

/**
 * For array classes, the component class object for instanceof/checkcast (for String[][][],
 * this will be String[][]). null for non-array classes.
 */
private transient Class<?> componentType;

翻译过来大概意思是:

对于数组类型,component 用于在 instanceof 操作或者类型转换时进行判断;对String[][][]类型来说,其component为 String[][]
对于非数组类型,改字段为null

也就是说,对于多维数组 A[ ][ ][ ][ ]… 和 B[ ][ ][ ][ ]… ,只要维度相同,那么两者之间的继承关系可以直接通过 A 和 B 类来判断。

至于多维数组内存存储方式,可以参考:Java中数组在内存中的存放原理

三、List和数组,应该如何选择

在一般写代码时,都习惯性的使用容器类,因为它可以 动态扩容,很方便,那数组还有没有 什么用处呢?

1、数组使用场景

对于固定数量的对象,一般使用数组是比较好的,一方面数组占用内存空间肯定要少的多。
另外一方面,由于多维数组的存在,在操纵一些矩阵 类数据时,总会直观一些。

2、ArrayList、LinkList、数组

这个其实 很多人都 已经知道了,ArrayList底层使用的数组 ,在生成实例时,可以传递容量参数。

transient Object[] elementData;
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

elementData 变量用于存储元素值,Object类型表示ArrayList真实存在的其实类型很宽,使用泛型也只是在语法 层面上减少使用出错率。
transient变量则保证了集合中的元素不会存在线程同步的问题。

LinkList 则在底层使用了链表,

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

从代码中可以看到,这还是个双向链表

jdk1.5添加了增强for循环功能,对于数组来说,因为无法看到源码,暂时不考虑效率问题。

但从ArrayList和LinkList来看,很明显的LinkList使用增强for循环会更快一些,ArrayList由于内部为数组,因此普通的for循环访问速度会更快。

总的来说,由于容器List的出现,数组类使用场景已经没有以前那么多了,但由于容器类的特殊性,是通过JVM自动生成的,因此至少安全和效率会有很大的保证。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐