Brandon-Winterfell's Blog

静态-构造-代码块

问题的来由

前几天跟着组件化那门课学习吧,老师在静态代码块里初始化对象,当时老师不管是静态变量还是成员变量都是用的m前缀,那我就喜欢成员变量用m前缀,静态变量用s前缀。我突然在想在这两个大括号之间(静态代码块)构造(new)对象,这个对象到底是静态的,还是普通的成员变量?(其实是局部变量)(在静态代码块里执行初始化操作的对象是静态变量(这个静态变量在代码块外面已经声明的了),因为静态变量是类级别,成员变量是对象级别的,此时对象都没创建(new)出来,或者说还没有执行到初始化成员变量的代码)。
局部变量
普通变量

牛客网上的一道题

前两天就想解决这个问题,去google了一下,大致就是:在静态代码块里构造(new)对象是静态变量呢还是成员变量呢。(而这实质上算是另外一个问题了,事实上在静态代码块里构造(new)的对象是局部变量。从上面第一个图可以看出来。)然后看到牛客网的一道题。
以下代码的输出结果是?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
https://www.nowcoder.com/questionTerminal/ab6eb06face84c4e81ab5bc6f0f7f258?source=relative
public class B
{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static
{
System.out.println("静态块");
}
public static void main(String[] args)
{
B t = new B();
}
}

A.静态块 构造块 构造块 构造块
B.构造块 静态块 构造块 构造块
C.构造块 构造块 静态块 构造块
D.构造块 构造块 构造块 静态块

然后我还选错了。卧槽。正确答案是C。

对下面静态代码块咋不初始化?的解释:加载类的时候,会执行静态初始化,所以,先初始化 t1 ,这个时候,发现B已经在加载过程中了,所以不会再次触发类加载。但是类的方法体,元数据已经有了,所以 先分配内存,再调用B的构造函数。

开始时JVM加载B.class,对所有的静态成员进行声明,t1 t2被初始化为默认值,为null,又因为t1 t2需要被显式初始化,所以对t1进行显式初始化,初始化代码块→构造函数(没有就是调用默认的构造函数),咦!静态代码块咋不初始化?因为在开始时已经对static部分进行了初始化,虽然只对static变量进行了初始化,但在初始化t1时也不会再执行static块了,因为JVM认为这是第二次加载类B了,所以static会在t1初始化时被忽略掉,所以直接初始化非static部分,也就是构造块部分(输出’’构造块’’)接着构造函数(无输出)。接着对t2进行初始化过程同t1相同(输出’构造块’),

此时就对所有的static变量都完成了初始化,接着就执行static块部分(输出’静态块’),(static变量与static块的执行顺序是按它们声明的顺序执行)

上面是类级别的加载,下面是对象级别的构造。

接着执行,main方法,同样也,new了对象,调用构造函数输出(’构造块’)

代码块

对于构造块不懂,然后搜索到一篇文章:java提高篇(十二)—–代码块
以下转载那篇文章 http://www.cnblogs.com/chenssy/p/3413229.html
在编程过程中我们可能会遇到如下这种形式的程序:

1
2
3
4
5
public class Test {
{
////
}
}

这种形式的程序段我们将其称之为代码块,所谓代码块就是用大括号({})将多行代码封装在一起,形成一个独立的数据体,用于实现特定的算法。一般来说代码块是不能单独运行的,它必须要有运行主体。在Java中代码块主要分为四种:

普通代码块

普通代码块是我们用得最多的也是最普遍的,它就是在方法名后面用{}括起来的代码段。普通代码块是不能够单独存在的,它必须要紧跟在方法名后面。同时也必须要使用方法名调用它。

1
2
3
4
5
public class Test {
public void test(){
System.out.println("普通代码块");
}
}

静态代码块

想到静态我们就会想到static,静态代码块就是用static修饰的用{}括起来的代码段,它的主要目的就是对静态属性进行初始化。

1
2
3
4
5
public class Test {
static{
System.out.println("静态代码块");
}
}

同步代码块

使用 synchronized 关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。

构造代码块

在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块。我们明白一个类必须至少有一个构造函数,构造函数在生成对象时被调用。构造代码块和构造函数一样同样是在生成一个对象时被调用,那么构造代码在什么时候被调用?如何调用的呢?看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
/**
* 构造代码
*/
{
System.out.println("执行构造代码块...");
}
/**
* 无参构造函数
*/
public Test(){
System.out.println("执行无参构造函数...");
}
/**
* 有参构造函数
* @param id id
*/
public Test(String id){
System.out.println("执行有参构造函数...");
}
}

上面定义了一个非常简单的类,该类包含无参构造函数、有参构造函数以及构造代码块,同时在上面也提过代码块是没有独立运行的能力,他必须要有一个可以承载的载体,那么编译器会如何来处理构造代码块呢?编译器会将代码块按照他们的顺序(假如有多个代码块)插入到所有的构造函数的最前端,这样就能保证不管调用哪个构造函数都会执行所有的构造代码块。上面代码等同于如下形式(这有点简单了,因为没有普通成员的初始化,看一下下面的 普通成员变量和代码块的执行顺序,哎呀,而构造块存在的意义就是对实例变量进行初始化呀那就没必要再构造块外面再另外的写初始化实例变量的代码了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
/**
* 无参构造函数
*/
public Test(){
System.out.println("执行构造代码块...");
System.out.println("执行无参构造函数...");
}
/**
* 有参构造函数
* @param id id
*/
public Test(String id){
System.out.println("执行构造代码块...");
System.out.println("执行有参构造函数...");
}
}

运行结果

1
2
3
4
5
public static void main(String[] args) {
new Test();
System.out.println("----------------");
new Test("1");
}
------------
Output:
执行构造代码块...
执行无参构造函数...
----------------
执行构造代码块...
执行有参构造函数...

从上面的运行结果可以看出在new一个对象的时候总是先执行构造代码,再执行构造函数,但是有一点需要注意构造代码不是在构造函数之前运行的,它是依托构造函数执行的。正是由于构造代码块有这几个特性,所以它常用于如下场景:

1、 初始化实例变量

如果一个类中存在若干个构造函数,这些构造函数都需要对实例变量进行初始化,如果我们直接在构造函数中实例化,必定会产生很多重复代码,繁琐和可读性差。这里我们可以充分利用构造代码块来实现。这是利用编译器会将构造代码块添加到每个构造函数中的特性。

2、 初始化实例环境

一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时创建此场景。我们可以利用构造代码块来创建此场景,尤其是该场景的创建过程较为复杂。构造代码会在构造函数之前执行。

上面两个常用场景都充分利用构造代码块的特性,能够很好的解决在实例化对象时构造函数比较难解决的问题,利用构造代码不仅可以减少代码量,同时也是程序的可读性增强了。特别是当一个对象的创建过程比较复杂,需要实现一些复杂逻辑,这个时候如果在构造函数中实现逻辑,这是不推荐的,因为我们提倡构造函数要尽可能的简单易懂,所以我们可以使用构造代码封装这些逻辑实现部分。

静态代码块、构造代码块、构造函数执行顺序

从词面上我们就可以看出他们的区别。静态代码块,静态,其作用级别为类,构造代码块、构造函数,构造,其作用级别为对象。

1、 静态代码块,它是随着类的加载而被执行,只要类被加载了就会执行,而且只会加载一次,主要用于给类进行初始化。

2、 构造代码块,每创建一个对象时就会执行一次,且优先于构造函数,主要用于初始化不同对象共性的初始化内容和初始化实例环境。

3、 构造函数,每创建一个对象时就会执行一次。同时构造函数是给特定对象进行初始化,而构造代码是给所有对象进行初始化,作用区域不同。

通过上面的分析,他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。

public class Test {
    /**
     * 静态代码块
     */
    static{
        System.out.println("执行静态代码块...");
    }

    /**
     * 构造代码块
     */
    {
        System.out.println("执行构造代码块...");
    }

    /**
     * 无参构造函数
     */
    public Test(){
        System.out.println("执行无参构造函数...");
    }

    /**
     * 有参构造函数
     * @param id
     */
    public Test(String id){
        System.out.println("执行有参构造函数...");
    }

    public static void main(String[] args) {
        System.out.println("----------------------");
        new Test();
        System.out.println("----------------------");
        new Test("1");
    }
}
-----------
Output:
执行静态代码块...
----------------------
执行构造代码块...
执行无参构造函数...
----------------------
执行构造代码块...
执行有参构造函数...

普通成员变量和代码块的执行顺序

// 我要测试初始化普通成员变量先于构造块,还是按声明顺序
public class Test {

    public static void main(String[] args) {
        new Test();
    }

    // -------------------------------------------
    A a = new A(); // 普通成员变量a

    /**
     * 构造代码块
     */
    {
        System.out.println("执行构造代码块...");
    }

    A aa = new A(); 普通成员变量aa
    // -------------------------------------------

    /**
     * 无参构造函数
     */
    public Test(){
        System.out.println("执行无参构造函数...");
    }    
}

public class A {
    public A() {
        System.out.println("这是A的构造方法");
    }
}

运行结果:
这是A的构造方法
执行构造代码块...
这是A的构造方法
执行无参构造函数...

那么这样子的话,在有普通成员变量需要初始化的时候,上面 构造代码块 里所说的等同形式就有点不准确了。

有继承情况的初始化

链接:https://www.nowcoder.com/questionTerminal/ab6eb06face84c4e81ab5bc6f0f7f258?source=relative
来源:牛客网

初始化过程:
1.初始化父类中的静态成员变量和静态代码块 ; (按声明顺序)
2.初始化子类中的静态成员变量和静态代码块 ; (按声明顺序)
3.初始化父类的普通成员变量和代码块(按声明顺序),再执行父类的构造方法;
4.初始化子类的普通成员变量和代码块(按声明顺序),再执行子类的构造方法。

总结一下:
1.执行顺序:静态代码块>构造代码块>构造方法
理由:静态代码块(static{})在类加载的时候执行一次。
构造代码块({}内的部分)在每一次创建对象时执行,始终在构造方法前执行。
构造方法在新建对象时调用( 就是new的时候 )。
注意: a.静态代码块在类加载的时候就执行,所以它的优先级高于入口main()方法。
b.当三种形式不止一次出现,同优先级是按照先后顺序执行。

2.现在来看下有继承时的情况:

public class HelloB extends HelloA {

     public HelloB(){
         System.out.println("B的构造方法");
     }
     {
         System.out.println("B的构造代码块");
     }
     static{
         System.out.println("B的静态代码块");
     }
     //public static HelloB hB = new HelloB();
     public static void main(String[] args){
         new HelloB();//调用B的构造方法
     }
}

class HelloA{
     public HelloA(){
         System.out.println("A的构造方法");
     }
     {
         System.out.println("A的构造代码块");
     }
     static{
         System.out.println("A的静态代码块");
     }
}

输出结果为:
A的静态代码块
B的静态代码块
A的构造代码块
A的构造方法
B的构造代码块
B的构造方法

可以看出:
a.父类始终先调用(继承先调用父类),并且这三者之间的相对顺序始终保持不变
b.因为静态代码块在类加载时执行,所以先输出的是父类和子类的静态代码块
c.调用B的构造方法创建对象时,构造块和构造方法会一起按顺序执行,还是父类的先调用

补充:如果在B的静态代码块之前加一句:static HelloB hB = new HelloB(); B的静态代码块会在其执行完后再执行;如果加在之后,则会先执行,说明优先级相同时是按照先后顺序执行的。