之前写过一篇深入理解数组的博文【Java核心技术卷】深入理解Java数组, 这篇文章主要从理论的角度, 探讨了Java的数组。
这篇文章主要从实战的角度去探究数组的本质。
在正文开始之前,我们有必要先关注一下类的加载机制:
在Java代码中,类型的加载
、连接
与初始化
过程都是在程序运行期间完成的
这里的类型指的是我们定义的class interface,枚举等等,这里不涉及到对象的概念,是一种runtime的阶段。这种加载机制提供了更大的灵活性,增加了更多的可能性。
简单地说类型的加载 最常见的就是把字节码文件从磁盘中加载到内存,连接就是将类与类之间的关系确定好,并且对字节码的一些处理,校验等 也就是在这一阶段完成了初始化 就是对类型中的静态字段赋值等等
具体流程如下:
类的加载、连接与初始化
- 加载:查找并加载类(class文件)的二进制数据
- 连接
·-验证:确保被加载的类的正确性(class文件的格式等)
·-准备:为类的静态变量分配内存,并将其初始化为默认值(准备阶段 还没有类的概念)
·-解析:把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
用图示表述为
Java程序对类的使用方式可分为两种
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
不严格划分,主动使用分为七种
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.test.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(包含main方法的类)
- JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
下面我们试图探究数组的本质
先看一个例子:
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
运行结果发现控制台没有输出任何东西
没有任何的输出 证实Test类并没有对TestValue类主动使用
我们已知的有两点:
- 静态代码块是在类加载时自动执行的,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
- 所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
你可能会有疑问了 我们这里都new出来一个TestValue[ ] 的实例啊。我们看看它的类型
package com.leetcodePractise.tstudy;
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
System.out.println(testValues.getClass());
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
打印结果:
TestValue是我们生成的数组从属的类型,其实这是Java虚拟机帮助我们在运行期声明出来的,但是我们却没有在代码中显式声明出来这种类型。
以下面的测试为例:
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
反编译Test.class
Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
public com.leetcodePractise.tstudy.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #2 // class com/leetcodePractise/tstudy/TestValue
5: astore_1
6: return
}
aload_0 将第一个引用类型本地变量推送至栈顶
anewarray 助记符表示创建一个引用型(如类、接口、数组)的数组,并将其引用值压入栈顶
astore_1表示将栈顶引用型数值存入第二个本地变量
重点关注anewarray ,这里仅仅是创建一个引用型的数组。这个数组的类型在运行期确定为TestValue类型,这是一个引用型的数据,并没有将TestValue类进行加载,所以不会有static静态代码块的执行
不相信的话,看下面的结果
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
testValues[0].dosomething();
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
public void dosomething(){
System.out.println("haha");
}
}
运行直接报错
改变一下
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
testValues[0] = new TestValue();
testValues[0].dosomething();
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
public void dosomething(){
System.out.println("haha");
}
}
是不是清晰多了??
二维数组与一维数组类似,我们测试一下二维数组
TestValue[][] testValues2 = new TestValue[10][10];
System.out.println(testValues2.getClass());
打印结果是:
class [[Lcom.leetcodePractise.tstudy.TestValue;
这里有两个左中括号以示二维数组
探究数组对象的父类
//进一步探究数组对象的父类型
System.out.println(testValues.getClass().getSuperclass());
System.out.println(testValues2.getClass().getSuperclass());
打印结果均为class java.lang.Object
对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为[Lcom leetcodePractise tstudy. Testvalue这种形式。
动态生成的类型,其父类型就是 Object
对于数组来说, JavaDoc经常将构成数组的元素为 Component,实际上就是将数组降低一个维度后的类型。
对下面这段代码编译成的字节码文件进行反编译
package com.leetcodePractise.tstudy;
public class Test {
public static void main(String[] args) {
TestValue[] testValues = new TestValue[10];
System.out.println(testValues.getClass());
TestValue[][] testValues2 = new TestValue[10][10];
System.out.println(testValues2.getClass());
}
}
class TestValue{
static {
System.out.println("TestValue static code");
}
}
反编译结果
Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
public com.leetcodePractise.tstudy.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #2 // class com/leetcodePractise/tstudy/TestValue
5: astore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
13: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
16: bipush 10
18: bipush 10
20: multianewarray #6, 2 // class "[[Lcom/leetcodePractise/tstudy/TestValue;"
24: astore_2
25: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_2
29: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
32: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
35: return
}
anewarray 助记符表示创建一个引用型(如类、接口、数组)的数组,并将其引用值压入栈顶
astore_1表示将栈顶引用型数值存入第二个本地变量
aload_1 将第二个引用类型本地变量推送至栈顶
multianewarray 创建指定类型和指定维度的多维数组(执行指令时,操作栈中必须包含各维度的长度值),并将其引用压入栈顶
astore_2表示将栈顶引用型数值存入第三个本地变量
aload_2 将第三个引用类型本地变量推送至栈顶
复盘一下吧:
数组对象的类型是在运行期确定下来的,这个过程并没有主动使用运行期确定下来的类,因此不会引起类的加载。如果要想通过索引使用对象,还需要new出类相应的实例。
数组的本质已经探讨过了,因为数组对象的类型是在运行期确定下来的,这也留下了一个包袱,就是数组协变。
这里也提一下吧
下面就是演示:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
//编译时通过,编译时Fruit[]数组可以装入Fruit及其子类
//运行时Apple[]数组可以装入Apple及其子类,
//运行时异常,运行时Apple[]数组不可以装入Fruit类
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
}
上面的注释非常详细
fruit[0] = new Fruit(); // ArrayStoreException
因为数组对象的类型是在运行期确定下来的,此时的fruit[0]的类型是Apple类型的。
结果new出来的是它的父类肯定会报错
为了严谨起见,我们测试一下
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
System.out.println(fruit[0].getClass());
System.out.println(fruit[1].getClass());
}
}
结果:
有很多人疑惑,为什么学习底层? 相信,这篇文章已经告诉你答案了,这也是为什么有的人写的代码,bug很少,遇见了也很快解决。有的人写的代码,bug不仅多,却要花了大量的时间去debug的原因了。