Android复习二回又三次,从虚构机的角度驾驭静态代码块和结构代码块

ca88会员登录中心 2
ca88会员登录中心

从虚拟机的角度理解静态代码块和构造代码块,虚拟机静态

所谓静态代码块,是指用static关键字修饰的代码块,特点是代码块会在类的构造代码块、构造函数之前运行,
且只会执行一次。而构造代码块,则就是单纯的由花括号构成的代码块,特点是代码块会在类的构造函数之前运行,
且每次实例化对象都会被调用。本篇blog从虚拟机的角度描述静态代码块和构造代码块,加深理解。

首先,我们要知道,当你将.java文件编译成.class文件时,如果有静态代码块的话,
他会在.class文件中插入一段称为<clinit>的函数代替静态代码块。如果有构造代码块,他会在各个构造函数中插入一段称为<init>的函数代替构造代码块

其次,我们要理解类的生命周期(如下图),<clinit>函数的调用发生在初始化阶`段,而初始化阶段仅有一次,所以`函数仅会调用一次,这就是为什么
静态代码块仅会在“使用类”的时候被调用一次,其他情况均不会被调用。

ca88会员登录中心 1

咱们使用javap反汇编看看。

public class Student {

    static {
        System.out.println("第一个构造代码块");
    }
    static {
        System.out.println("第二个构造代码块");
    }
    {
        System.out.println("第一个构造代码块");
    }
    {
        System.out.println("第二个构造代码块");
    }
    public Student() {
        System.out.println("默认构造函数");
    }
    public Student(String name,int age) {
        System.out.println("自定义构造函数");
        this.name = name;
        this.age = age;
    }
    private String name = "Clive";
    private static int age = 17;
    private final static String COUNTRY = "CN";
    private final String SCHOOL = "JXAU";
}



/*
E:\eclipseWork\Test\bin\test>javap -c Student
警告: 二进制文件Student包含test.Student
Compiled from "Student.java"
public class test.Student {
  static {};
    Code:
       0: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #25                 // String 第一个构造代码块
       5: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      11: ldc           #33                 // String 第二个构造代码块
      13: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: bipush        17
      18: putstatic     #35                 // Field age:I
      21: return

  public test.Student();
    Code:
       0: aload_0
       1: invokespecial #40                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #42                 // String Clive
      23: putfield      #44                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #14                 // String JXAU
      29: putfield      #46                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #48                 // String 默认构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return

  public test.Student(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #40                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #42                 // String Clive
      23: putfield      #44                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #14                 // String JXAU
      29: putfield      #46                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #53                 // String 自定义构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: aload_0
      41: aload_1
      42: putfield      #44                 // Field name:Ljava/lang/String;
      45: iload_2
      46: putstatic     #35                 // Field age:I
      49: return
}
*/

咦?说好的<clinit>函数呢?这是因为<clinit>函数不能被java调用,它只能作为类加载过程的一部分由JVM直接调用,具体见文末引用。JVM对函数做了“美化处理”,
反汇编的代码中static {};就是指<clinit>静态代码块。从<clinit>函数中,我们可以得到一些信息。

  • 多个静态代码块最终会融合成一个<clinit>函数供JVM调用

源码中有两个静态代码块,而反汇编得出的代码仅有一个<clinit>

  • 静态数据域的赋值操作在<clinit>函数中完成

数据域age是静态数据域(private static int age =
17;),对应<clinit>函数中偏移量为18的指定。

18: putstatic #28 // Field age:I

从构造函数中,我们可以得到与构造代码块相关的一些信息

  • final static数据域的赋值操作不在<clinit>函数中完成

数据域COUNTRY用了不仅用了static关键字修饰,还用了final修饰,但是反汇编代码中并没有相关的赋值指令,
因为当一个数据域用final修饰时,其赋值操作在准备阶段就已经完成了,数据域SCHOOL也是如此。

  • 每当构造函数运行时,函数都会被调用

从反汇编代码中可以看到,默认构造函数和自定义构造函数中都插入了一段调用<init>函数的字节码指令,
invokespecial #33 // Method java/lang/Object.””:()V
这也就是为什么构造代码块有这个特性:每次实例化对象时静态代码块都会被执行,且在构造函数之前执行

  • 多个构造代码块也被融合成了一个函数

源码中有两个构造代码块,而构造函数中仅调用了一次<init>函数,可见虚拟机将构造代码快融合了

  • 普通数据域的赋值操作在构造函数中完成

数据域name是普通的数据域(private String name =
“Clive”;)。赋值操作在默认构造函数中对应的是
偏移量为21的字节码指令,在自定义构造函数中,对应的字节码偏移量也为21。

所谓静态代码块,是指用static关键字修饰的代码块,特点是代码块会在类的构造代码块、构造函数之前运行,
且只会执行一次。而构造代码块,则就是单纯的由花括号构成的代码块,特点是代码块会在类的构造函数之前运行,
且每次实例化对象都会被调用。本篇blog从虚拟机的角度描述静态代码块和构造代码块,加深理解。

1. 解释下主函数:

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

public :表该函数访问权限最大
static :表该函数随着类的加载便存在
void :表主函数无具体返回值类型
main :非关键字,但可以被JVM识别
(String[] args) :函数的参数是一个字符串类型的数组
主函数是固定格式的,被JVM识别,且只识别这个。
args 是数组对象,虚拟机调用主函数时,传入的是new String[0]

静态代码块与<clinit>的关系,构造代码块与<init>的关系

根据上面的反汇编代码,我们得知,把<clinit>函数简单的当做静态代码块是错误的一个观点,因为<clinit>函数中还包含了
对静态数据域的赋值操作,所以,<clinit>函数与静态代码块的关系应该是包含关系。构造代码块与<init>的关系也是如此,不再赘述。

首先,我们要知道,当你将.java文件编译成.class文件时,如果有静态代码块的话,
他会在.class文件中插入一段称为<clinit>的函数代替静态代码块。如果有构造代码块,他会在各个构造函数中插入一段称为<init>的函数代替构造代码块

2. 构造函数

作用:给对象初始化 。
多个构造函数以重载形式出现

2.1. 构造函数在写法上与一般函数不同的地方在于:

  • 函数名与类名一致
  • 无返回值类型
  • 无需return语句

2.2.
对象一创建即调用与之对应的构造函数,比如生活中小孩一出生就会哭。
2.3.
若类未定义构造函数,系统就会默认给该类加一个空参数的构造函数,如果开发人员自定义了构造函数,就不会有这个默认的了。
2.4. 构造函数和一般函数在运行上的不同?
构造函数:对象一建立就执行,进行初始化。一个对象建立,构造函数只会运行一次
一般函数:调用时才执行,是给对象添加对象具备的功能。一个对象建立,一般方法可被对象调用多次
(如:婴儿会吃饭,但并非一直不停地吃。)
2.5. 何时定义构造函数?
分析事物时,该事物存在具备一些特性或行为,则将这些内容定义在构造函数中。

继承关系中的

当父类中也有静态代码块时,子类的静态代码块中并没有显示或者隐式调用父类静态代码块的指令,但虚拟机会保证在子类的<clinit>函数运行之前,父类的<clinit>已经运行完成。

其次,我们要理解类的生命周期(如下图),<clinit>函数的调用发生在初始化阶`段,而初始化阶段仅有一次,所以`函数仅会调用一次,这就是为什么
静态代码块仅会在“使用类”的时候被调用一次,其他情况均不会被调用。

3. 构造代码块

构造代码块是给对象进行初始化
对象一建立就执行,且优先于构造函数执行
ca88会员登录中心,与构造函数的区别

  • 构造代码块给所有对象进行初始化。
  • 构造函数给对应对象初始化。

并非所有类都会产生函数

如果类中没有静态代码块,也没有静态数据域的赋值操作,也就不会产出<clinit>函数

public class Student {

    {
        System.out.println("第一个构造代码块");
    }
    {
        System.out.println("第二个构造代码块");
    }
    public Student() {
        System.out.println("默认构造函数");
    }
    public Student(String name,int age) {
        System.out.println("自定义构造函数");
        this.name = name;
    }
    private String name = "Clive";
    private final static String COUNTRY = "CN";
    private final String SCHOOL = "JXAU";
}

/*
E:\eclipseWork\Test\bin\test>javap -c Student
警告: 二进制文件Student包含test.Student
Compiled from "Student.java"
public class test.Student {
  public test.Student();
    Code:
       0: aload_0
       1: invokespecial #17                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #35                 // String Clive
      23: putfield      #37                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #12                 // String JXAU
      29: putfield      #39                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #41                 // String 默认构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return

  public test.Student(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #17                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #35                 // String Clive
      23: putfield      #37                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #12                 // String JXAU
      29: putfield      #39                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #48                 // String 自定义构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: aload_0
      41: aload_1
      42: putfield      #37                 // Field name:Ljava/lang/String;
      45: return
}
*/

把Student中静态代码块和静态数据域删掉,最终生成的反汇编代码中并没有<clinit>函数

ca88会员登录中心 2

4. 静态static

静态变量也称为类变量
静态函数随着类的加载而加载,故静态函数可直接被类调用。
静态方法为共享数据,存储在方法区中
静态虽好,只能访问静态。非静态可访问静态
4.1. static 的特点:

  • 随着类的加载而加载,消失而消失,所以生命周期最长
  • 优先于对象存在,静态先存在,对象后存在
  • 被所有对象共享
  • 可直接被类名调用

4.2. 静态使用注意事项:

  • 静态方法只能访问静态成员,非静态方法既可以访问静态成员也可以访问非静态成员
  • 静态方法中不可以定义this,super关键字,因静态优先于对象存在
  • 主函数为静态函数

4.3. 静态的利与弊:

  • 利:对象的共享数据进行单独空间存储,节省空间,无必要每个对象一份,可直接被类名调用
  • 弊:生命周期过长,访问出现局限性(静态虽好,只能访问静态)

4.4. 如何使用静态?

  • 何时定义类变量?
    当对象中出现共享数据时,该数据用静态修饰,对象中的特有数据要定义成非静态存于堆内存中,
  • 合适定义静态变量?
    当功能内部未访问到非静态数据时

4.5. 静态代码块

static { 静态代码块中的执行语句 }

特点:随着类的加载而执行,只执行一次,用于给类进行初始化。

4.6. 静态函数应用
建立一个ArrayTools的类来实现对数组的操作,由于并未封装特有数据,因此可将函数定义成静态,然后通过类名来调用,另外为避免被创建对象应该讲构造函数私有化。

接口也会产生函数

接口中是不可以写静态代码块的(The interface XXX cannot define an
initializer),但其数据域都默认是静态的,
所以也会产生<clinit>函数。另外,只有真正使用了这个接口,其<clinit>函数才会运行。也就是说,两个接口A、B,A是B的父接口,
当使用B接口时,A接口的<clinit>函数并不会用运行,只有真正使用了A接口,其<clinit>函数才会运行,比如使用接口中的数据域。
另外,我不想骗各位,书上大意是如此,但是,我用javap生成的反汇编代码发现,并没有生成<clinit>函数,而是生成了一个默认构造函数,里面有对数据域的赋值操作,我想可能是JDK版本不同吧
🙂

public class Speak {
    int COUNT = 2;
}

/*
E:\eclipseWork\Test\bin\test>javap -c Speak
警告: 二进制文件Speak包含test.Speak
Compiled from "Speak.java"
public class test.Speak {
  int COUNT;

  public test.Speak();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_2
       6: putfield      #12                 // Field COUNT:I
       9: return
}
*/

 

咱们使用javap反汇编看看。

5. 实例变量和类变量的区别:

5.1. 存放位置:

  • 类变量随着类的加载而存在于方法区中
  • 实例变量随着对象的建立而存在于堆内存中

5.2. 生命周期:

  • 类变量的生命周期最长,随类的消失而消失
  • 实例变量随对象的消失而消失

<clinit>函数的线程安全性

虚拟机内部,为确保其线程安全,当多线程视图初始化同一个类时,仅一条线程允许进入<clinit>函数,其他线程必须等待。当<clinit>函数 执行完成时,其他线程发现<clinit>函数已经执行完毕,也就不会在执行<clinit>函数了。这种互斥的同步操作使用锁完成的,有锁就 有可能导致死锁,如下。

package test;

import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        // 模仿多线程情况下初始化类 //
        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Task("test.A"));
        executor.execute(new Task("test.B"));
        executor.shutdown();
        System.out.println("over");
    }

    private static class Task implements Runnable {
        private String className;

        public Task(String className) {
            this.className = className;
        }

        public void run() {
            try {
                Class.forName(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

package test;

public class A {

    static {
        System.out.println("enter static block of A");
        try {
            Thread.sleep(5000);
            Class.forName("test.B"); //初始化B
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out static block of A");
    }
}

package test;

public class B {

    static {
        System.out.println("enter static block of B");
        try {
            Thread.sleep(5000);
            Class.forName("test.A"); //初始化A
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out static block of B");
    }
}

 上述代码模拟了在多线程环境下初始化类而造成的死锁现象。当一个线程进入类A的<clinit>函数时,
根据代码,其又要初始化类B,而类B的<clinit>函数已经由另一个线程占据,且要初始化类A。这样双方都抱着自己的资源不放,又去请求别的资源,自然会造成死锁。

 

public class Student {

    static {
        System.out.println("第一个构造代码块");
    }
    static {
        System.out.println("第二个构造代码块");
    }
    {
        System.out.println("第一个构造代码块");
    }
    {
        System.out.println("第二个构造代码块");
    }
    public Student() {
        System.out.println("默认构造函数");
    }
    public Student(String name,int age) {
        System.out.println("自定义构造函数");
        this.name = name;
        this.age = age;
    }
    private String name = "Clive";
    private static int age = 17;
    private final static String COUNTRY = "CN";
    private final String SCHOOL = "JXAU";
}



/*
E:\eclipseWork\Test\bin\test>javap -c Student
警告: 二进制文件Student包含test.Student
Compiled from "Student.java"
public class test.Student {
  static {};
    Code:
       0: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #25                 // String 第一个构造代码块
       5: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      11: ldc           #33                 // String 第二个构造代码块
      13: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: bipush        17
      18: putstatic     #35                 // Field age:I
      21: return

  public test.Student();
    Code:
       0: aload_0
       1: invokespecial #40                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #42                 // String Clive
      23: putfield      #44                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #14                 // String JXAU
      29: putfield      #46                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #48                 // String 默认构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return

  public test.Student(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #40                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #42                 // String Clive
      23: putfield      #44                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #14                 // String JXAU
      29: putfield      #46                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #53                 // String 自定义构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: aload_0
      41: aload_1
      42: putfield      #44                 // Field name:Ljava/lang/String;
      45: iload_2
      46: putstatic     #35                 // Field age:I
      49: return
}
*/

6. 成员变量和局部变量

  • 作用范围:
    成员变量作用于整个类,局部变量作用于函数或语句中
  • 在内存中的位置
    成员变量存在堆内存中,因为对象存在而在内存中存在;局部变量存在于栈内存中。

总结

<clinit>函数包含了静态代码块中的代码,在类的初始化阶段运行,且仅会运行一次。<init>包含了构造代码块中的代码,在实例化对象的时候运行,会在构造函数之前运行

咦?说好的<clinit>函数呢?这是因为<clinit>函数不能被java调用,它只能作为类加载过程的一部分由JVM直接调用,具体见文末引用。JVM对函数做了“美化处理”,
反汇编的代码中static {};就是指<clinit>静态代码块。从<clinit>函数中,我们可以得到一些信息。

7. 说明下句的内存空间是如何开辟的:

ShowSpaceCreate p = new ShowSpaceCreate("zhang" , 20)
  • 因new用到了ShowSpaceCreate.class,所以先找到该类文件加载到堆内存中
  • 执行该类中static代码块,若有,则是给该类初始化
  • 在堆内存中开辟空间,分配内存地址
  • 在堆内存中建立对象的特有属性,并进行默认初始化
  • 对属性进行显示初始化
  • 对对象进行构造代码块初始化
  • 对对象进行构造函数初始化
  • 将内存地址赋给栈内存中的p变量。

引用

1.《深入理解Java虚拟机》

2.《实战Java虚拟机》

3.论坛:

所谓静态代码块,是指用static关键字修饰的代码块,特点是代码块会在类的构造…

  • 多个静态代码块最终会融合成一个<clinit>函数供JVM调用

8. 匿名对象

new Car().run();

未给对象取名,new完直接调用方法
用法:

  • 当对对象的方法只调用一次时,可用匿名简化
    若对一个对象进行多个成员调用则必须命名
  • 可将匿名对象作为实际参数传递,如:
    show (new Car());

源码中有两个静态代码块,而反汇编得出的代码仅有一个<clinit>

9. 内部类

将一个类定义在另一个类里面,里面的这个类就是内部类
**9.1. ** 访问特点:

  • 可直接访问外部类成员,包括私有的
    私有成员只能在类内部使用,内部类在类的内部;
    之所以可以直接访问外部类成员,是因为内部类持有一个外部类引用。
    格式:外部类.this.外部类成员
  • 外部类要访问内部类成员需要创建内部类的对象。

**9.2. **内部类在成员位置上时可被访问修饰符修饰
如:
private 将内部类在外部类中进行封装。
static 使内部类具备静态特性,此时只能访问外部类中的静态成员。

**9.3. **在外部其他类中直接访问static内部类的非静态成员:

new OuterClass.Inner().function();

**9.4. **在外部其他类中直接访问static内部类的静态成员:

OuterClass.Inner().function();

<a>注意:</a>
当内部类成员定义了一个静态成员,则该类必须也为静态类
当外部类静态方法访问内部类,则该内部类为static的。

**9.5. ** 局部内部类
局部内部类不可以被成员修饰符修饰:private ,static
局部内部类可以直接访问外部类中成员,因为还持有引用,但不可以访问所在局部中的变量,常量可以。

**9.6. ** 匿名内部类
定义匿名内部类的前提是:内部类必须是继承或者实现一个接口。
格式:

new 父类或接口(){定义子类内容}

匿名内部类即一个匿名子类对象,而这个类有点胖,可以理解成为带内容的对象
匿名内部类定义的方法最好不要超过三个。

  • 静态数据域的赋值操作在<clinit>函数中完成

10. final修饰符

final可以修饰类、变量和方法,分别表示类不能被继承;变量值不可改变,其实就是常量;函数不可被重写。
局部内部类只能访问该局部中被final修饰的量。

数据域age是静态数据域(private static int age =
17;),对应<clinit>函数中偏移量为18的指定。

18: putstatic #28 // Field age:I

从构造函数中,我们可以得到与构造代码块相关的一些信息

  • final static数据域的赋值操作不在<clinit>函数中完成

数据域COUNTRY用了不仅用了static关键字修饰,还用了final修饰,但是反汇编代码中并没有相关的赋值指令,
因为当一个数据域用final修饰时,其赋值操作在准备阶段就已经完成了,数据域SCHOOL也是如此。

  • 每当构造函数运行时,<init>函数都会被调用

从反汇编代码中可以看到,默认构造函数和自定义构造函数中都插入了一段调用<init>函数的字节码指令,
invokespecial #33 // Method java/lang/Object.””:()V
这也就是为什么构造代码块有这个特性:每次实例化对象时静态代码块都会被执行,且在构造函数之前执行

  • 多个构造代码块也被融合成了一个函数

源码中有两个构造代码块,而构造函数中仅调用了一次<init>函数,可见虚拟机将构造代码快融合了

  • 普通数据域的赋值操作在构造函数中完成

数据域name是普通的数据域(private String name =
“Clive”;)。赋值操作在默认构造函数中对应的是
偏移量为21的字节码指令,在自定义构造函数中,对应的字节码偏移量也为21。

静态代码块与<clinit>的关系,构造代码块与<init>的关系

根据上面的反汇编代码,我们得知,把<clinit>函数简单的当做静态代码块是错误的一个观点,因为<clinit>函数中还包含了
对静态数据域的赋值操作,所以,<clinit>函数与静态代码块的关系应该是包含关系。构造代码块与<init>的关系也是如此,不再赘述。

继承关系中的

当父类中也有静态代码块时,子类的静态代码块中并没有显示或者隐式调用父类静态代码块的指令,但虚拟机会保证在子类的<clinit>函数运行之前,父类的<clinit>已经运行完成。

并非所有类都会产生函数

如果类中没有静态代码块,也没有静态数据域的赋值操作,也就不会产出<clinit>函数

public class Student {

    {
        System.out.println("第一个构造代码块");
    }
    {
        System.out.println("第二个构造代码块");
    }
    public Student() {
        System.out.println("默认构造函数");
    }
    public Student(String name,int age) {
        System.out.println("自定义构造函数");
        this.name = name;
    }
    private String name = "Clive";
    private final static String COUNTRY = "CN";
    private final String SCHOOL = "JXAU";
}

/*
E:\eclipseWork\Test\bin\test>javap -c Student
警告: 二进制文件Student包含test.Student
Compiled from "Student.java"
public class test.Student {
  public test.Student();
    Code:
       0: aload_0
       1: invokespecial #17                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #35                 // String Clive
      23: putfield      #37                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #12                 // String JXAU
      29: putfield      #39                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #41                 // String 默认构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return

  public test.Student(java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #17                 // Method java/lang/Object."<init>":()V
       4: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #25                 // String 第一个构造代码块
       9: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #33                 // String 第二个构造代码块
      17: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_0
      21: ldc           #35                 // String Clive
      23: putfield      #37                 // Field name:Ljava/lang/String;
      26: aload_0
      27: ldc           #12                 // String JXAU
      29: putfield      #39                 // Field SCHOOL:Ljava/lang/String;
      32: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #48                 // String 自定义构造函数
      37: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: aload_0
      41: aload_1
      42: putfield      #37                 // Field name:Ljava/lang/String;
      45: return
}
*/

把Student中静态代码块和静态数据域删掉,最终生成的反汇编代码中并没有<clinit>函数

接口也会产生<clinit>函数

接口中是不可以写静态代码块的(The interface XXX cannot define an
initializer),但其数据域都默认是静态的,
所以也会产生<clinit>函数。另外,只有真正使用了这个接口,其<clinit>函数才会运行。也就是说,两个接口A、B,A是B的父接口,
当使用B接口时,A接口的<clinit>函数并不会用运行,只有真正使用了A接口,其<clinit>函数才会运行,比如使用接口中的数据域。
另外,我不想骗各位,书上大意是如此,但是,我用javap生成的反汇编代码发现,并没有生成<clinit>函数,而是生成了一个默认构造函数,里面有对数据域的赋值操作,我想可能是JDK版本不同吧
🙂

public class Speak {
    int COUNT = 2;
}

/*
E:\eclipseWork\Test\bin\test>javap -c Speak
警告: 二进制文件Speak包含test.Speak
Compiled from "Speak.java"
public class test.Speak {
  int COUNT;

  public test.Speak();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_2
       6: putfield      #12                 // Field COUNT:I
       9: return
}
*/

 

<clinit>函数的线程安全性

虚拟机内部,为确保其线程安全,当多线程视图初始化同一个类时,仅一条线程允许进入<clinit>函数,其他线程必须等待。当<clinit>函数 执行完成时,其他线程发现<clinit>函数已经执行完毕,也就不会在执行<clinit>函数了。这种互斥的同步操作使用锁完成的,有锁就 有可能导致死锁,如下。

package test;

import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        // 模仿多线程情况下初始化类 //
        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Task("test.A"));
        executor.execute(new Task("test.B"));
        executor.shutdown();
        System.out.println("over");
    }

    private static class Task implements Runnable {
        private String className;

        public Task(String className) {
            this.className = className;
        }

        public void run() {
            try {
                Class.forName(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

package test;

public class A {

    static {
        System.out.println("enter static block of A");
        try {
            Thread.sleep(5000);
            Class.forName("test.B"); //初始化B
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out static block of A");
    }
}

package test;

public class B {

    static {
        System.out.println("enter static block of B");
        try {
            Thread.sleep(5000);
            Class.forName("test.A"); //初始化A
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out static block of B");
    }
}

 上述代码模拟了在多线程环境下初始化类而造成的死锁现象。当一个线程进入类A的<clinit>函数时,
根据代码,其又要初始化类B,而类B的<clinit>函数已经由另一个线程占据,且要初始化类A。这样双方都抱着自己的资源不放,又去请求别的资源,自然会造成死锁。

 

总结

<clinit>函数包含了静态代码块中的代码,在类的初始化阶段运行,且仅会运行一次。<init>包含了构造代码块中的代码,在实例化对象的时候运行,会在构造函数之前运行

引用

1.《深入理解Java虚拟机》

2.《实战Java虚拟机》

3.论坛:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图