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
相关文章
|
2月前
|
Rust Java 文件存储
Java系统中的错误码设计问题之通过properties文件管理Error Code如何解决
Java系统中的错误码设计问题之通过properties文件管理Error Code如何解决
33 1
|
19天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
174 37
|
20天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑】设计模式——原型模式
对比原型模式和传统方式的实现思路、代码方案、优缺点,阐述原型模式的使用场景,以及深拷贝、浅拷贝等相关概念,并扩展原型模式在Spring源码中的应用。
【Java笔记+踩坑】设计模式——原型模式
|
6天前
|
JSON Java Maven
关于使用Java-JWT的笔记
这篇文章介绍了使用Java-JWT库来生成和验证JSON Web Tokens (JWT) 的方法。文中解释了JWT的组成,包括头部、载荷和签名,并提供了如何使用java-jwt库生成和验证token的示例代码。此外,还提供了Maven依赖和一些关于token的标准声明和自定义声明的解释。
关于使用Java-JWT的笔记
|
20天前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇
消息中间件 缓存 监控
81 0
|
20天前
|
运维 Java 关系型数据库
【Java笔记+踩坑】SpringBoot基础2——运维实用
SpringBoot程序的打包与运行、临时配置、多环境配置、日志
【Java笔记+踩坑】SpringBoot基础2——运维实用
|
20天前
|
Java 数据库连接 API
【Java笔记+踩坑】Spring Data JPA
从常用注解、实体类和各层编写方法入手,详细介绍JPA框架在增删改查等方面的基本用法,以及填充用户名日期、分页查询等高级用法。
【Java笔记+踩坑】Spring Data JPA
|
20天前
|
SQL Java 数据库连接
【Java笔记+踩坑】MyBatisPlus基础
MyBatisPlus简介、标准数据层开发CRUD、业务层继承IService、ServiceImpl、条件查询、LambdaQueryWrapper、id生成策略、逻辑删除、乐观锁@Version、代码生成器、ActiveRecord
【Java笔记+踩坑】MyBatisPlus基础
|
20天前
|
前端开发 Java 数据库连接
【Java笔记+踩坑】SpringBoot——基础
springboot三种配置文件及其优先级、多环境配置、springboot整合junit,mybatis、ssmp综合图书案例
【Java笔记+踩坑】SpringBoot——基础
下一篇
无影云桌面