一 概述 阿里巴巴一道笔试题
面试时,经常会遇到看代码写运算结果的题目,里面涉及静态变量,静态代码块,静态方法,实例变量,实例代码块,构造方法等的代码,如下:
请说程序执行运算结果。
public class Programer {
public static int salary = getSalary(); //静态变量
private int workAge = getWorkAge();//实例变量
static {//静态代码块
System.out.println(1);
}
private static int getSalary() { //静态方法
System.out.println(2);
return 20000;
}
public Programer() { //构造方法
System.out.println(3);
}
{//实例代码块
System.out.println(4);
}
private int getWorkAge() { //非静态方法
System.out.println(5);
return 10;
}
public static void main(String[] args) {//主方法
new Programer();
new Programer();
System.out.println(6);
}
}
参考答案:
2
1
5
4
3
5
4
3
6
相信大部分同学根据自身的经验,是能轻松解题的。但在实际的面试中题目可能变幻莫测,如果对类的实例化顺序不太熟悉,解决起来估计还是有一定难度的。
因此今天就对类实例化做一个专题讲解,我们从写的java源代码到字节码,再从字节码执行流程反推到java源代码来讲解相关代码的执行流程。
二 类加载及初始化详解
2.1 源码到字节码
2.1.1 类结构简介
javac Student.java--->Student.class
使用工具javac编译的过程中,其实会经历非常复杂的过程,我们不用去深入研究。我们只要关心编译之后的class字节码文件。
这个class字节码文件中含有哪些数据呢,如何查看呢?我们可以从官方的文档中找到答案。
首先,class文件是二进制文件,直接使用文本工具读取是看不到信息的。我们可以使用专门的软件来查看class文件中的字节信息。例如:winhex, sublime
查看Student.class字节码信息如下:
如果要看懂上述文件,我们需要查阅官方文档说明:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
2.1.2 字节码反汇编工具
查看字节码文件如果直接使用二进制工具去读,可读性极差。我们可以借助一些工具帮我们查看类中的信息,而且还能看到反编译后的汇编指令。 通过汇编代码,我们可以深入的了解java代码的工作机制。
1. javap
javap是JDK自身携带的反编译工具,我们可以直接使用,很方便:
在cmd或者在IDEA的Terminal中可以查看javap的帮助信息:
javap -help
可以根据自己的需要给javap加入参数使用,例如:
javap -p -v xxxx.class // 显示所有类和成员的
这个是最直接,最方便的查看工具了,但是可读性没有一些插件好。
jclasslib:比较强大,可以查看类中的所有信息
ASMPlugin:主要可以用来查看汇编指令,格式会比较友好。
先去打开设置中的plugins,安装插件,完成后需要重启。
- jclasslib插件使用:
- 显示效果
ASM Bytecode Viewer
直接在源码文件中鼠标右键,选择ASM Bytecode Viewer
就可以了
界面:
当然插件有很多,功能大致一样,关键是能够灵活使用,看懂插件的内容。后续的一些内容,我们将会结合工具使用进行讲解相关知识点。
2.2 类加载机制
Java虚拟机会动态的加载、链接与初始化类和接口。
JVM专题课程: http://yun.itheima.com/course/584.html
步骤:
2.2.1 加载
加载是根据特定的名称查找类或者接口类型的二进制表示,并由此二进制形式来创建类或接口的所对应Class对象的过程。
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象, 作为对方法区中这些数据的访问入口
2.2.2 链接
链接过过程中要做的三步:
-
验证:保证被加载类的正确性
文件格式验证
元数据验证
字节码验证
符号引用验证
-
准备
为类的静态变量分配内存, 并将其初始化为默认值
例如:
class A{ static int a=10; } 准备阶段会为a静态变量分配4个字节的空间,并初始值为0。
-
解析
把类中的符号引用转换为直接引用
类中的符号引用就是class字节码中的原始信息,比如变量名,方法名,类名等,需要解析成为运行时内存中相关的地址引用,方便使用。
例如:调用一个方法时,在java语言层面就是一个方法名这个符号,但是在底层应该要根据这个符号找到内存中的直接地址来调用。
2.2.3 初始化
对类的静态变量, 静态代码块执行初始化操作
class A{
static int a = 10;
}
到了初始化阶段,a的值就可以赋值为10了
这个阶段其实是<clinit>方法执行过程的体现,请看2.3.1节
2.3 类初始化过程【重点】
一个类要实例化,必须先要让自己先初始化,类初始化过程主要是对静态成分的初始化。如下:
public class Student{
public static String name="itheima"; //静态变量
static{//静态代码块
name = "itcast";
}
}
我们使用反编译工具将Student类字节码反编译,如下:
// class version 53.0 (53)
// access flags 0x21
public class Student {
public static Ljava/lang/String; name
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LStudent; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
static <clinit>()V
L0
LINENUMBER 2 L0
LDC "itheima"
PUTSTATIC Student.name : Ljava/lang/String;
L1
LINENUMBER 4 L1
LDC "itcast"
PUTSTATIC Student.name : Ljava/lang/String;
L2
LINENUMBER 5 L2
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
2.3.1 方法详解
当一个类编译之后,字节码文件中会产生一个类构造器方法:<cinit>(),而这个方法中的逻辑就是当前类中的静态变量和静态代码块的初始化逻辑整合。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZxcgaam-1587961908980)(imgs/image-20200409173545310.png)]
2.3.2 静态变量和静态代码块初始化顺序
-
静态变量直接赋值底层原理:
当字节码在加载过程中的链接阶段时,有三个步骤,分别是验证,准备,解析。在准备阶段,为类的静态变量分配内存, 并将其初始化为默认值。只有到了初始化阶段才会正式赋值。
public static int a = 10; //在链接的准备阶段:开辟空间存储a的数据,初始为0 //在出初始化阶段:将静态变量a赋值为10
-
静态代码块
静态代码块的逻辑也将会在类的初始化阶段执行。
实践:
静态变量的赋值和静态代码块中的逻辑,都会整合到<cinit>方法中执行,那么问题来了,如果多个静态代码块和静态变量的赋值,那么他的初始化顺序又会是怎样的呢?例如:
public class Student {
static int height;//0
static int age = 10;
static {
age = 20;
}
static {
name = "Jack";
}
static String name = "Rose";
public static void main(String[] args) {
System.out.println(height);//0
System.out.println(name);//Rose
System.out.println(age);//20
}
}
要解答这个问题,你要观察编译后的字节码中<cinit>方法中具体指令就可以了。如下:
指令解析:
static <clinit>()V
L0
LINENUMBER 3 L0 //L0 对应 源码中第3行
BIPUSH 10 //将单字节的常量值10推入到栈顶
PUTSTATIC Student.age : I //将栈顶的值10取出赋值给 Student.age
L1
LINENUMBER 6 L1
BIPUSH 20
PUTSTATIC Student.age : I
L2
LINENUMBER 10 L2
LDC "Jack" //将字符串常量“Jack”推入栈顶
PUTSTATIC Student.name : Ljava/lang/String;
L3
LINENUMBER 13 L3
LDC "Rose"
PUTSTATIC Student.name : Ljava/lang/String;
RETURN //结束
MAXSTACK = 1
MAXLOCALS = 0
}
结论:
- 如果静态变量只有定义,没有做赋值初始化,那么只有默认值,在<cinit>方法中不会看到赋值的指令。
- 静态代码块和静态变量直接赋值初始化顺序是按照编码顺序从上到下完成的。静态变量的值以最后一次赋值为准。
如果没有静态变量的直接赋值,也没有静态代码块,那么就不会产生<cinit>方法了,这个可以在反编译工具中验证。
2.3.3 继承中类初始化分析
当一个类存在父类时,一定是先对父类进行初始化然后再初始化子类的。
看下面的代码,说出执行顺序。
class Fu {
static int a = getNum1();
static {
System.out.println("1");
}
private static int getNum1() {
System.out.println("2");
return 10;
}
}
class Zi extends Fu {
static int b = getNum2();
static {
System.out.println("3");
}
public static int getNum2() {
System.out.println("4");
return 20;
}
public static void main(String[] args) {
//main方法执行
}
}
答案:
2
1
4
3
三 对象创建和初始化过程
定义一个Teacher类,里面含有实例变量,实例代码块,构造方法。定义另外一个测试类Test,在main方法中创建Teacher对象,研究其过程。
public class Teacher {
int age = 10;
{
age = 20;
}
public Teacher() {
}
public Teacher(int age) {
this.age = age;
}
}
class Test{
public static void main(String[] args) {
Teacher t = new Teacher();
}
}
3.1 new对象底层字节码指令分析
就一句创建对象的语句在底层字节码指令是怎样体现的呢?
Teacher t = new Teacher();
如下:
public static main([Ljava/lang/String;)V
L0
LINENUMBER 18 L0
NEW Teacher //创建Teacher对象,并将引用值压入栈顶
DUP //复制栈顶的引用,重新入栈(操作数栈存在连个相同引用)
INVOKESPECIAL Teacher.<init> ()V //调用无参构造器进行初始化
ASTORE 1 //将栈顶引用值赋值给变量表中第二个变量
L1
LINENUMBER 19 L1
RETURN
L2
//本地变量表
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE t LTeacher; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,类中静态相关成分就在这个过程中完成初始化,这个过程可以体现在<cinit>方法中(请看2.3章节)。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。对象内存分配同时,实例变量也会被赋予默认值(零值)。
在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序员的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化,不管是哪一种方式的初始化,编译之后都会在方法 <init>
中统一执行。
3.2 对象初始化过程详解【重点】
3.2.1 <init>方法详解
图中可以看出,开辟对象空间所用的指令是 new
完成的。当程序员给实例变量直接赋值,或者使用实例代码块,构造方法去给实例变量初始化值,编译器最后都会统一的放到<init>方法完成。
<init> 方法 和 java中的构造方法是一一对应的,有多少个构造方法就有多少个<init>方法,请看示例:
public class A {
int c;
{
b = 200;
}
int b = 20;
int a = 10;
public A() { }
public A(int c) {
this.c = c;
}
}
反编译字节码,观察<init>方法
我们从反编译后的结果可以推导出以下结论:
- 每一个构造方法都会对应一个<init>方法
- 每个<init>方法内部都会先执行父类的<init>方法
3.2.2 实例变量初始化顺序分析
我们继续研究3.2.1节中的代码,观察下实例变量直接赋值,及在实例代码块中赋值,在构造方法中赋值这三种方式顺序特点。
可以得到以下结论:
-
执行完父类的<init>方法后按编码顺序,从上到下再执行直接赋值或者实例代码块,最后执行构造方法内部的赋值语句。
-
<init>方法的模型:
3.2.3 继承中实例变量初始化顺序
从上一节中模型图中还可以看出,父类中的实例变量赋值,实例代码块逻辑,构造方法执行是优先于子类完成的,如下:
四 类实例化顺序总结
通过前两章的学习,我们大致理清类实例化一般的过程了,分成两个阶段:
4.1 类初始化阶段
- 类初始化阶段底层有<cinit>方法完成,<cinit>方法是有静态变量赋值语句和静态代码块的结合而形成的。
- 静态变量赋值语句和静态代码块的执行顺序有其本身编码顺序决定,自上往下执行。
- 如果父类存在静态资源的初始化,那么父类会优先与子类完成执行<cinit>方法。因为子类是依赖父类而存在的。
- 一个类中如果没有静态变量的赋值也没有静态代码块,不会存在<cinit>方法
4.2 对象的创建和初始化阶段
-
对象的创建及初始化阶段,对象空间的开辟是由new指令完成的。对应空间中实例变量的初始化则由<init>方法完成的。
-
<init>方法由实例变量赋值语句,实例代码块,构造方法结合而成,每个java中的构造方法都对应了一个<init>方法。
-
<init>方法中先执行父类的<init>方法,然后执行本类的<init>方法剩下的内容。如下图:
4.3 类初始化和对象初始化顺序探究
大部分情况下,我们都会看到一个现象,类初始化会优先于对象的初始化。例如,
public class A {
//静态变量
static int a = getA();
private static int getA() {
System.out.print(1);
return 10;
}
//静态代码块
static {
System.out.print(2);
a = 20;
}
//非静态
int b = getB();
private int getB() {
System.out.print(3);
return 20;
}
//实例代码块
{
System.out.print(4);
b = 40;
}
//构造方法
public A() {
System.out.print(5);
}
public static void main(String[] args) {
System.out.print(6);
new A();
}
}
先完成类初始化,然后执行main方法,main方法中创建对象初始化对象。
我们很轻松就能得到结果: 126345
然而这不是一个绝对的情况,类初始和实例化可能会混合在一起完成。如下:
增加一个静态变量,类型是本身,并创建对象直接赋值。
public class A {
//静态变量
static int a = getA();
static A obj = new A();//******新加入********
private static int getA() {
System.out.print(1);
return 10;
}
//静态代码块
static {
System.out.print(2);
a = 20;
}
//非静态
int b = getB();
private int getB() {
System.out.print(3);
return 20;
}
//实例代码块
{
System.out.print(4);
b = 40;
}
//构造方法
public A() {
System.out.print(5);
}
public static void main(String[] args) {
System.out.print(6);
new A();
}
}
当类加载后完成第二个阶段链接,其实就可以投入使用了。在初始化阶段中,如果涉及到本类的对象实例化也是可以完成的。我们可以从底层代码论证:
五 经典笔试案例
5.1 阿里经典笔试讲解
阅读代码,分析出打印结果:
public class Test1 {
public static int k = 0;
public static Test1 t1 = new Test1("t1");
public static Test1 t2 = new Test1("t2");
public static int i = print("i");//3
public static int n = 99;
public int j = print("j");
static {
print("静态块");
}
public Test1(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
{
print("构造块");
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
Test1 t = new Test1("init");
}
}
参考答案
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102