Java中的数组和List集合以及类型强转
在java中,集合操作有两种方式——容器、数组;容器相比较于数组,多了可扩展性,这里仅以容器的代表List,来对比和数组的关系。都知道在java引入的泛型和自动拆装箱等语法糖后,集合操作也变得简单安全。也都知道其实泛型在到字节码层面上时,会被擦除,虽然字节码中还会保留泛型参数(可以利用反射看到),但对真实的的类并不产生多大影响。那么,对于List来说,如果泛型存在继承关...
在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);
这里的操作是这样的:
- 新建List容器,存储Number类型
- 将一个Integer类型对象放入
- 通过去泛型操作将存储Number类型的List容器引用赋值给存储Date类型的List容器
- 通过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;
这里所有的操作都是有效的,一般而言:
- 子类实例同样是父类的实例
- 只有类型见存在继承关系,才能进行强制类型转换,并且只有真实类型为子类时,强转才不会出错
那么我们有理由推理,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
- 如果本类和目标类相同,那么返回true
- 如果本类为Object,目标类不是基本数据类型,返回true
- 如果本类为数组,目标类也为数组,则使用本类和目标类的componentType来调用isAssignableFrom进行判断
- 如果本类为接口,则获取目标类所有实现的接口,然后逐个判断是否与 自身相同
- 如果本来为其他一般类,则逐级获取目标类的父类,直到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自动生成的,因此至少安全和效率会有很大的保证。
更多推荐
所有评论(0)