Java 中共有四种类型的代码块,分别是普通代码块、静态代码块、同步代码块和构造代码块。
构造代码块
说到构造代码块,就不得不提到构造函数。
基本原理
构造代码块是指在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。
一个类至少有一个构造函数(如果没有,编译器会为其创建一个无参构造函数),构造函数是在对象生成时调用的,那现在的问题来了:构造函数和构造代码块是什么关系?构造代码块是在什么时候执行的?
public class Person { { System.out.println("Person.instance initializer"); } public Person () { System.out.println("Person.Person"); } public Person (String name) { System.out.println("Person.Person by name"); } public static void main(String[] args) { Person person = new Person(); Person james = new Person("Peter"); } } 复制代码
执行之后输出如下:
>>> Person.instance initializer >>> Person.Person >>> Person.instance initializer >>> Person.Person by name 复制代码
每个构造函数的最前端都被插入了构造代码块,构造代码块会在每个构造函数内首先执行。如果有多个构造代码块,则按从上到下的顺序分别插入执行。
注意:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行。
构造代码块的应用场景
构造代码块可以应用到如下场景中:
- 初始化实例变量
如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义一个方法,然后在每个构造函数中调用该方法来实现。但是要在每个构造函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中,这才是解决此类问题的绝佳方式。
- 初始化实例环境
一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时创建此场景,例如在 JEE 开发中,要产生 HTTP Request 必须首先建立 HTTP Session,在创建 HTTP Request 时就可以通过构造代码块来检查 HTTP Session 是否已经存在,不存在则创建之。
以上两个场景利用了构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行。很好地利用构造代码块的这两个特性不仅可以减少代码量,还可以让程序更容易阅读。
特别是当所有的构造函数都要实现逻辑,而且这部分逻辑又很复杂时,这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑,按照业务顺序依次存放,这样在创建实例对象时 JVM 也就会按照顺序依次执行,实现复杂对象的模块化创建。
构造代码块的其他细节
刚刚说到编译器会自动把构造代码块插入到各个构造函数中。
那如果我们有多个构造函数,并在某个构造函数中调用了另外一个构造函数呢?那么构造代码块是否会被多次执行?
public class Person { { System.out.println("Person.instance initializer"); } public Person () { System.out.println("Person.Person"); } public Person (String name) { this(); System.out.println("Person.Person by name"); } public static void main(String[] args) { Person person = new Person(); Person james = new Person("Peter"); } } 复制代码
输出结果如下:
>>> Person.instance initializer >>> Person.Person >>> Person.instance initializer >>> Person.Person >>> Person.Person by name 复制代码
执行 new Person(); 时,先输出 Person.instance initializer 再输出 Person.Person 。很好理解。
当执行 new Person("Peter"); 时,由于在这个有参构造函数中通过 this(); 调用了无参构造函数。所以这个有参构造函数不会被插入构造代码块,以保证构造代码块不会被重复执行。
在构造函数中,如果遇到 this 关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。
对于 this 有特殊处理,那么 super 呢?如果该类有继承关系,在构造函数内部有调用父类的构造函数的话,构造代码块会被插入到哪个位置?
答案是构造代码块会被插入到 super 的后面
public class Person { { System.out.println("Person.instance initializer"); } public Person () { System.out.println("Person.Person"); } } class Student extends Person { { System.out.println("Student.instance initializer"); } public Student () { System.out.println("Student.Student"); } public static void main(String[] args) { Student student = new Student(); } } 复制代码
输入结果如下:
>>> Person.instance initializer >>> Person.Person >>> Student.instance initializer >>> Student.Student 复制代码
在调用 Student 的构造函数时,默认会添加 super; 调用父类的构造函数。这时候 Person 类的构造代码块被放置到 super(); 后面。
普通代码块、静态代码块、同步代码块
说完构造代码块,再来聊聊普通代码块、静态代码块和同步代码块。
普通代码块
在方法后面使用“{}”括起来的代码片段。
普通代码块不能直接执行,所以需要和方法名进行绑定,通过方法名来执行普通代码块里的内容。
public class Main { public void method() { System.out.println("Main.method"); } } 复制代码
静态代码块
在类中使用 static 修饰,并使用“{}”括起来的代码片段。
通常用于静态变量的初始化或对象创建前的环境初始化。
public class Main { static { System.out.println("Main.static initializer"); } public static void main(String[] args) { } } 复制代码
上述代码会打印出 Main.static initializer 。也就是说只要类被加载,无论是否有被使用,静态代码块都会被执行,而且是有且仅有的执行一次。
当有多个静态代码块的时候,顺序执行。
同步代码块
使用 synchronized 关键字修饰,并使用“{}”括起来的代码片段。
它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
public static void main(String[] args) { Object lock = new Object(); synchronized (lock) { System.out.println("Main.main"); } } 复制代码
synchronized 用于解决同步问题,需要传入一个对象作为“锁”。
当有多条线程同时访问共享数据时,只有获取到“锁”对象的线程才能进入执行,其他线程则会阻塞等待,直到获取“锁”对象的线程执行完毕,释放“锁”对象。
synchronized 除了可以用来构建同步代码块以外,还可以直接用来修饰方法,保证方法是线程安全。
public class Foo { public synchronized void syncMethod() { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { final Foo foo = new Foo(); Thread thread1 = new Thread(new Runnable() { public void run() { foo.syncMethod(); } }); thread1.start(); Thread thread2 = new Thread(new Runnable() { public void run() { foo.syncMethod(); } }); thread2.start(); Thread thread3 = new Thread(new Runnable() { public void run() { foo.syncMethod(); } }); thread3.start(); } } 复制代码
由于 syncMehotd 使用了 synchronized 进行修饰,所以一次只有一个线程可以进入执行。
直接使用 synchronized 修饰方法,底层和同步代码块一样,只是编译器帮我们将整个方法使用 synchronized(){} 进行包裹。
这时候会有个问题,是哪个对象类充当“锁”对象呢?答案是方法里面的 this ,谁调用方法谁充当“锁”对象。
写段代码进行验证:
public class Foo { public synchronized void syncMethod() { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } public void otherSyncMethod () { synchronized (this) { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { final Foo foo = new Foo(); Thread thread1 = new Thread(new Runnable() { public void run() { foo.syncMethod(); } }); thread1.start(); Thread thread2 = new Thread(new Runnable() { public void run() { foo.otherSyncMethod(); } }); thread2.start(); } } 复制代码
当其中一个线程调用 syncMethod 的时候,默认使用了 this (foo 对象) 作为锁,导致了别的线程调用 otherSyncMethod 时候锁对象 this (foo 对象) 尚未释放,只能阻塞等待。
类元素的执行顺序
如果一个类里面同时出现了静态变量、静态初始化块、实例变量、构造代码块、构造函数。
初始化/执行顺序依次是:静态内容(包括“静态变量”和“静态初始化块”,按照编写的先后顺序执行)> 非静态内容(包括“实例变量”和“构造代码块”,按照编写的先后顺序执行)> 构造函数。
其中静态内容是只要类被加载就会被初始化,而非静态内容和构造函数只有在类被使用的情况下才会被初始化。而且都是按照继承链的先后顺序执行:
class Super { // 1 静态内容,在类被加载的时候调用,按照该类编写的静态内容先后顺序执行 public static Object sObject = new Object(); // 2 静态内容,在类被加载的时候调用,按照该类编写的静态内容先后顺序执行 static { System.out.println("Super.static initializer"); } // 3 非静态内容,在类被使用的时候调用,按照该类编写的非静态内容先后顺序执行 { System.out.println("Super.instance initializer"); } // 4 非静态内容,在类被使用的时候调用,按照该类编写的非静态内容先后顺序执行 public Object iObject = new Object(); // 5 构造函数,在类被使用的时候调用,在所有静态和非静态内容执行完之后执行 public Super () { System.out.println("Super.Super"); } } class Sub extends Super { // 6 静态内容,在类被加载的时候调用,按照该类编写的静态内容先后顺序执行,按照继承链先调用完父类的静态内容再执行 static { System.out.println("Sub.static initializer"); } // 7 静态内容,在类被加载的时候调用,按照该类编写的静态内容先后顺序执行,按照继承链先调用完父类的静态内容再执行 public static Object sObject = new Object(); // 8 非静态内容,在类被使用的时候调用,按照该类编写的非静态内容先后顺序执行,按照继承链先调用完父类的非静态内容和构造方法再执行 public Object iObject = new Object(); // 9 非静态内容,在类被使用的时候调用,按照该类编写的非静态内容先后顺序执行,按照继承链先调用完父类的非静态内容和构造方法再执行 { System.out.println("Sub.instance initializer"); } // 10 构造函数,在类被使用的时候调用,在所有静态和非静态内容执行完之后执行,按照继承链先调用完父类的非静态内容和构造方法再执行 public Sub () { System.out.println("Sub.Sub"); } // 入口函数 public static void main(String[] args) { // 11 System.out.println("*********** Sub.main ***********"); Sub sub = new Sub(); } } 复制代码
由于入口函数是挂在 Sub 类下,所以执行入口函数,Sub 类肯定会被加载。分 Sub 类在入口函数是否被使用两种情况:
- 如果入口函数没有使用到 Sub 类(注释第 53 行),那么类是只被加载,没被使用,只有静态内容会被初始化/执行。执行顺序为:1 -> 2 -> 6 -> 7 -> 11
- 如果入口函数使用到 Sub 类(打开第 53 行),类被加载并使用,静态内容、非静态内容和构造函数都会被初始化/执行。执行顺序为:1 -> 2 -> 6 -> 7 -> 11 -> 3 -> 4 -> 5 -> 8 -> 9 -> 10