0
点赞
收藏
分享

微信扫一扫

Java Code Block 那些注意点| Java Debug 笔记

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
举报

相关推荐

0 条评论