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

简介: 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
相关文章
|
12月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
118 2
|
12月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
140 1
|
9月前
|
存储 Java 开发者
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
本文详细介绍了 Java 中 `toString()` 方法的重写技巧及其重要
418 10
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
|
9月前
|
前端开发 JavaScript Java
Java构建工具-maven的复习笔记【适用于复习】
这篇文档由「潜意识Java」创作,主要介绍Maven的相关知识。内容涵盖Maven的基本概念、作用、项目导入步骤、依赖管理(包括依赖配置、代码示例、总结)、依赖传递、依赖范围以及依赖的生命周期等七个方面。作者擅长前端开发,秉持“得之坦然,失之淡然”的座右铭。期待您的点赞、关注和收藏,这将是作者持续创作的动力! [个人主页](https://blog.csdn.net/weixin_73355603?spm=1000.2115.3001.5343)
119 3
|
9月前
|
Java Windows
【Azure Function】部署Java Function失败:报错deploy [ERROR] Status code 401和警告 'China North 3' may not be a valid region
1:deploy [ERROR] Status code 401, (empty body). 2: China North 3 may not be a valid region,please refer to https://aka.ms/maven_function_configuration#supported-regions for values. 3:  <azure.functions.maven.plugin.version>1.36.0</azure.functions.maven.plugin.version>
132 11
|
10月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
10月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
11月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
166 2
|
11月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)

热门文章

最新文章