首先可以肯定的是,数组是一个对象;但是在推导的过程中还是有些难以理解的问题,比如对于任意一个引用对象A
,
数组是协变的,所以Object[]
是A[]
的父类,即Object[] o = A[]
;
数组是一个对象,所以数组的父类是Object
,即Object oo = o
;
那么A[]
、Object[]
和Object
是什么关系呢?
是这样吗?
我们可以通过反射来观察一下:
private static void test05() { Object[] o = new String[2]; System.out.println(o.getClass().getName()); System.out.println(o.getClass().getSuperclass().getName()); String[] s = (String[]) o; System.out.println(s.getClass().getSuperclass().getName()); Object oo = s; } 打印: [Ljava.lang.String; java.lang.Object java.lang.Object
可以看见A[]
和Object[]
的直接父类都是Object
,所以他们之间的关系也一定不是上图中的多继承关系,那么数组协变产生的关系一定不同于extends
关键字产生的关系;
extends
关键字产生的继承关系是怎么定义呢?
这里我们可以从《Virtual Machine Specifications》中找到答案:
// ClassFile 结构ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attribute_count; attributes_info; attributes[attributes_count]; }
可以看到extends
关键字产生的继承关系是记录在class文件中的super_class
里面的。我这里还没有在 JDK 源码里面找到数组协变关系的产生,但是可以猜想这个应该是后来加的类似语法关系的结构。这里先留着以后看源码的时候确认吧。
在准备看Array源码的时候,我直接就点开了java.lang.reflect.Array
,后来才知道这根本不是Array的源码,看包名就知道,这是使用反射操作数组的一些方法。Array
的class是在运行过程中动态生成的。
那么在Array
的class中到底包含了什么呢?在很多的资料中都写了,Array
中有类似public final int length
的成员变量。但是在《Java Language Specifications》10.1. Array Types
中明确写了,length不是类型的一部分;
An array's length is not part of its type.
private static void test06() { String[] s = new String[2]; System.out.println(s.length); System.out.println(s.getClass().getDeclaredFields().length); try { System.out.println(s.getClass().getDeclaredField("length")); } catch (NoSuchFieldException e) { System.out.println(e.toString()); } } 打印:20java.lang.NoSuchFieldException: length
可以看到length
并不是Array
的成员变量,那么length
是从哪里来的呢?
同样我们可以从ClassFile
结构中找打答案;
可以看到Array
的length
信息是记录在对象头中的,而读取length
信息的时候,是使用的arraylength
字节码指令来读取的。
// 数组创建的几种形式String[] s = {"a", "b", "c"}; // 初始化器String[] s1 = new String[3]; // 有维度表达式String[] s2 = (String[]) Array.newInstance(String.class, 3); // 有维度表达式
数组创建流程
是否有维度表达式: 无: 创建的时候每个元素递归深入初始化,失败则退出 变量类型检查 -> 与数组类型不兼容 -> 编译错误 不是可具化类型(如:null) -> 编译错误 空间不足 -> OutOfMemoryError 有: 创建的时候,从左向右地计算,任意维度表达式计算失败则退出 检查所有维度值,有小于0 -> NegativeArraySizeException 分配空间,若空间不足 -> OutOfMemoryError 只有一个维度表达式,创建一维数组,每个元素初始化化为初始值 有n个维度表达式,执行深度为n-1的循环
逆变与协变用来描述类型转换(type transformation)后的继承关系
如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
f(⋅)是逆变的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是不变的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
正因为数组是协变的,所以Object[] o = new A[];
有种看法认为这是在泛型产生之前的妥协产物,比如在 JDK5 之前还没有泛型,但是很多地方需要用泛型来解决,比如:
// java.util.Arrayspublic static boolean equals(Object[] a, Object[] a2) { if (a==a2) return true; if (a==null || a2==null) return false; int length = a.length; if (a2.length != length) return false; for (int i=0; i最后调用的是
Object.equals()
方法,但是不想全部都重写equals
,这里最简单的就是让数组实现协变的特性;3. 为什么不能使用泛型数组
这里简单的讲是因为泛型是不变的,而数组是协变的,所以不能使用泛型数组;
// 如果泛型也是协变的private static void test07() { List可以看到如果泛型也是协变的,那么Collection 在存取数据的时候,就会产生类型转换错误;
4. 为什么数组可以是协变的
private static void test07() { Object[] o = new String[2]; o[0] = 123; } 运行时: Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer可以看到数组,在存数据的时候,还会检查数据类型是否兼容,所以数组可以是协变的。
五、数组在 java 和 c++ 中的区别
C++ 中的数组只是一个指针,java 中的数组是一个对象
java 中访问数组会有额外的范围检查
java 中会确保数组被初始化
六、Array 和 ArrayList的效率对比
private static final int SIZE = 50000;private static final Random RANDOM = new Random();private static void test_array() { System.out.println("Array:"); long start = System.currentTimeMillis(); String[] s = new String[SIZE]; for (int i = 0; i < SIZE; i++) { s[i] = i + ""; } System.out.println("insert:" + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0, len = SIZE * 10; i < len; i++) { String ss = s[RANDOM.nextInt(SIZE)]; } System.out.println("get:" + (System.currentTimeMillis() - start)); } private static void test_list() { System.out.println("ArrayList:"); long start = System.currentTimeMillis(); Listlist = new ArrayList<>(SIZE); for (int i = 0; i < SIZE; i++) { list.add(i + ""); } System.out.println("insert:" + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0, len = SIZE * 10; i < len; i++) { String s = list.get(RANDOM.nextInt(SIZE)); } System.out.println("get:" + (System.currentTimeMillis() - start)); } 打印: Array: insert:13get:10ArrayList: insert:7get:22 对比可以看到,数组的插入和随机访问效率都要比ArrayList高,但是一般建议优先使用列表,只有在优先考虑效率的时候才考虑使用数组,因为
数组是协变的不能使用泛型
数组是具体化的,只有在运行时才知道元素的类型
七、总结
在看数组的时候,因为class是动态创建的,所以看了很久,但是根据数组的特性,基本可以认为数组的域和方法,类似于:
class Aimplements Cloneable, java.io.Serializable { public final int length = X; public T[] clone() { try { return (T[]) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.getMessage()); } } } 创新互联www.cdcxhl.cn,专业提供香港、美国云服务器,动态BGP最优骨干路由自动选择,持续稳定高效的网络助力业务部署。公司持有工信部办法的idc、isp许可证, 机房独有T级流量清洗系统配攻击溯源,准确进行流量调度,确保服务器高可用性。佳节活动现已开启,新人活动云服务器买多久送多久。
名称栏目:JDK源码分析(2)之Array相关-创新互联
本文地址:http://jkwzsj.com/article/djpche.html