从字节码来看java内部类

在java中一个类的内部可以存在另一个类,这个类被称为内部类,也被叫做类的嵌套,内部类大体可以分为静态内部类和非静态内部类下面来简单研究下静态内部类

public class Demo1 {
	private static int i =1;
	
	static class Inner{
		public static void fun(){
			System.out.println(i);
		}
	}
	
	public static void main(String[] args) {
		Inner.fun();
	}
}

上述代码会输出1,也就是将外部类的值输出出来,这里就有个疑问,问什么静态内部类中可以获取到外部类的变量值呢,它是怎么获取的呢?

下面就分析一下其中的原理,首先我们先对这个类进行javac编译,然后可以看到编译出了两个class文件

《从字节码来看java内部类》

一个是外部类class,另一个是内部类的class文件

下面就通过反编译查看一下,这个类在编译后到底做了什么,下面是外部类的字节码文件

Classfile /F:/java/Demo1.class
  Last modified 2018-12-25; size 475 bytes
  MD5 checksum f813abf14144ad32bb9d886e9a595910
  Compiled from "Demo1.java"
public class Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #4.#22         // Demo1.i:I
   #2 = Methodref          #5.#23         // java/lang/Object."<init>":()V
   #3 = Methodref          #6.#24         // Demo1$Inner.fun:()V
   #4 = Class              #25            // Demo1
   #5 = Class              #26            // java/lang/Object
   #6 = Class              #27            // Demo1$Inner
   #7 = Utf8               Inner
   #8 = Utf8               InnerClasses
   #9 = Utf8               i
  #10 = Utf8               I
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               access$000
  #18 = Utf8               ()I
  #19 = Utf8               <clinit>
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo1.java
  #22 = NameAndType        #9:#10         // i:I
  #23 = NameAndType        #11:#12        // "<init>":()V
  #24 = NameAndType        #28:#12        // fun:()V
  #25 = Utf8               Demo1
  #26 = Utf8               java/lang/Object
  #27 = Utf8               Demo1$Inner
  #28 = Utf8               fun
{
  public Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #3                  // Method Demo1$Inner.fun:()V
         3: return
      LineNumberTable:
        line 11: 0
        line 12: 3

  static int access$000();
    descriptor: ()I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field i:I
         3: ireturn
      LineNumberTable:
        line 1: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #1                  // Field i:I
         4: return
      LineNumberTable:
        line 2: 0
}
SourceFile: "Demo1.java"
InnerClasses:
     static #7= #6 of #4; //Inner=class Demo1$Inner of class Demo1

通过字节码文件我们发现 static int access$000();这个方法并不是我们定义在类中的方法而通过他的方法标志位可以发现他是ACC_SYNTHETIC修饰的也就是说他是虚拟机在编译时候jvm生成的方法,让我们继续看看这个方法做了什么,首先它从1号常量池中获取到了变量i的值,然后返回,乍一看他只做了这两步,这里也的确是从常量池中获取到了i的值,可我们并没有发现调用该方法的字节码指令,那么这个方法是如何执行的呢?这里我们不要忘了还有一个class文件的,下面我们就对它也进行一下反编译来看一下这个class内部做了什么

Classfile /F:/java/Demo1$Inner.class
  Last modified 2018-12-25; size 441 bytes
  MD5 checksum 2d14d6f850ebf0d35f04162ca663bf39
  Compiled from "Demo1.java"
class Demo1$Inner
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // Demo1.access$000:()I
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(I)V
   #5 = Class              #21            // Demo1$Inner
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               fun
  #12 = Utf8               SourceFile
  #13 = Utf8               Demo1.java
  #14 = NameAndType        #7:#8          // "<init>":()V
  #15 = Class              #25            // java/lang/System
  #16 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #17 = Class              #28            // Demo1
  #18 = NameAndType        #29:#30        // access$000:()I
  #19 = Class              #31            // java/io/PrintStream
  #20 = NameAndType        #32:#33        // println:(I)V
  #21 = Utf8               Demo1$Inner
  #22 = Utf8               Inner
  #23 = Utf8               InnerClasses
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               Demo1
  #29 = Utf8               access$000
  #30 = Utf8               ()I
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V
{
  Demo1$Inner();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0

  public static void fun();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokestatic  #3                  // Method Demo1.access$000:()I
         6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 6: 0
        line 7: 9
}
SourceFile: "Demo1.java"
InnerClasses:
     static #22= #5 of #17; //Inner=class Demo1$Inner of class Demo1

由字节码,我们可以发现在3号常量池里已经存在我们之前提到的access方法(#3 = Methodref #17.#18 // Demo1.access$000:()) ,而我们也可以在下面的fun()方法中发现invokestatic指令,用来调用access方法,这样也就说明了,在静态内部类调用外部类变量的时候,jvm会创建一个access的方法,用来取出外部类中定义的变量并返回回来,然后在内部类调用方法的时候在通过指令先调用这个access方法拿到这个变量,这里还有一点要说的就是前面我们在外部类中定义了一个变量,所以只生成了一个access方法,如果我们定义两个变量,就会生成两个access方法,以此类推也就是说每个access就是为了从外部类中取得一个变量的值供内部类使用的。

下面再来说一下非静态内部类,代码如下

public class Demo1 {
	private static int i =1;
	
	 class Inner{
		public  void fun(){
			System.out.println(i);
		}
	}
	
	public static void main(String[] args) {
		new Demo1().new Inner().fun();
	}
}

由于内部类必须要依存一个外部类,所以在内部类为非静态的时候,需要先实例化外部类在通过这个实例去创建内部类对象同样,我们来看一下这个类的字节码文件,看一下new Demo1().new Inner().fun();这一行代码究竟是怎么执行的

Classfile /F:/java/Demo1.class
  Last modified 2018-12-25; size 565 bytes
  MD5 checksum 0c9dbb142aa6e8119d154d803935f045
  Compiled from "Demo1.java"
public class Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #4.#25         // Demo1.i:I
   #2 = Methodref          #9.#26         // java/lang/Object."<init>":()V
   #3 = Class              #27            // Demo1$Inner
   #4 = Class              #28            // Demo1
   #5 = Methodref          #4.#26         // Demo1."<init>":()V
   #6 = Methodref          #9.#29         // java/lang/Object.getClass:()Ljava/lang/Class;
   #7 = Methodref          #3.#30         // Demo1$Inner."<init>":(LDemo1;)V
   #8 = Methodref          #3.#31         // Demo1$Inner.fun:()V
   #9 = Class              #32            // java/lang/Object
  #10 = Utf8               Inner
  #11 = Utf8               InnerClasses
  #12 = Utf8               i
  #13 = Utf8               I
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               access$000
  #21 = Utf8               ()I
  #22 = Utf8               <clinit>
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo1.java
  #25 = NameAndType        #12:#13        // i:I
  #26 = NameAndType        #14:#15        // "<init>":()V
  #27 = Utf8               Demo1$Inner
  #28 = Utf8               Demo1
  #29 = NameAndType        #33:#34        // getClass:()Ljava/lang/Class;
  #30 = NameAndType        #14:#35        // "<init>":(LDemo1;)V
  #31 = NameAndType        #36:#15        // fun:()V
  #32 = Utf8               java/lang/Object
  #33 = Utf8               getClass
  #34 = Utf8               ()Ljava/lang/Class;
  #35 = Utf8               (LDemo1;)V
  #36 = Utf8               fun
{
  public Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=1
         0: new           #3                  // class Demo1$Inner
         3: dup
         4: new           #4                  // class Demo1
         7: dup
         8: invokespecial #5                  // Method "<init>":()V
        11: dup
        12: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        15: pop
        16: invokespecial #7                  // Method Demo1$Inner."<init>":(LDemo1;)V
        19: invokevirtual #8                  // Method Demo1$Inner.fun:()V
        22: return
      LineNumberTable:
        line 11: 0
        line 12: 22

  static int access$000();
    descriptor: ()I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field i:I
         3: ireturn
      LineNumberTable:
        line 1: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #1                  // Field i:I
         4: return
      LineNumberTable:
        line 2: 0
}
SourceFile: "Demo1.java"
InnerClasses:
     #10= #3 of #4; //Inner=class Demo1$Inner of class Demo1

我们同样还是可以看到access方法,他的作用同静态内部类中的access,这里我们重点看一下这一段

 		0: new           #3                  // class Demo1$Inner
         3: dup
         4: new           #4                  // class Demo1
         7: dup
         8: invokespecial #5                  // Method "<init>":()V
        11: dup
        12: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        15: pop
        16: invokespecial #7                  // Method Demo1$Inner."<init>":(LDemo1;)V
        19: invokevirtual #8                  // Method Demo1$Inner.fun:()V

这段指令是上面main方法中的一段,下面我们来简单分析一下,首先,通过new指令实例化一个内部类的实例,然后通过dup复制该值并亚如栈顶,然后实例外部类对象,同样复制压栈,然后invokespecial指令指令初始化方法,该方法指向5号常量池也就是外部类的初始化方法,然后复制压栈,需要着重注意的是第12行指令,这里调用了一个普通的方法getClass来获取外部类的class对象,然后pop出栈,然后16行执行内部类的初始化方法,并将外部类的引用传入了进去,然后调用了fun方法,由此可知,当内部类为非静态的时候,想要调用内部类的方法的时候,就是将外部类的引用传入进内部类的初始化方法中,然后调用,也就是外部类的this,这样的话内部类就可以通过外部类对象来访问外部类的非静态成员变量。

这里还有一点要说明一下,上面的两个例子的外部类变量都是采用的静态变量,如果我们使用实例变量,还能不能执行成功呢?这时外部类的变量又该如何获取呢?

下面我们把外部类的变量改为实例变量,重新编译一下

public class Demo1 {
	private  int i =1;
	
	 class Inner{
		public  void fun(){
			System.out.println(i);
		}
	}
	
	public static void main(String[] args) {
		new Demo1().new Inner().fun();
	}
}

然后我们看一下他的access方法和之前有什么不同之处,为了方便对比,将之前的access方法复制过来进行对比,上面的为静态变量的,下面的为实例变量的

 static int access$000();
    descriptor: ()I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field i:I
         3: ireturn
      LineNumberTable:
        line 1: 0
  static int access$000(Demo1);
    descriptor: (LDemo1;)I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field i:I
         4: ireturn
      LineNumberTable:
        line 1: 0

通过比较,我们能发现,当变量为实例变量的时候,在access方法中传入了一个参数,也就是外部类的对象,同时多了一条指令aload_0,这条指令是加载this对象的,局部变量的0号位引用为this,然后就和之前相同了,取出变量并返回。

    原文作者:lexBBQ
    原文地址: https://blog.csdn.net/qq_36258498/article/details/85255018
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞