Java“失效”的private修饰符

Java语言开发中有四种权限修饰符:public、protected、default以及private,对于private修饰符最常见的一种解释就是它修饰的方法或者属性只对本类自身可见。但是当Java引入内部类以后,好像权限修饰符与它已经没有了明显的关联,不可否定内部类的引入是一个很实用的特性,可是有些程序员认为它却违背了Java语言比C或C++更加简单的设计理念,在某些意义上来说也确实如此。本篇文章所说的“失效”的private修饰符就是指在内部类与外部类之间的访问控制上面。本文使用的jdk版是"1.8.0_111"。

private修饰符失效场景

//方式一:内部类访问外部类私有属性
public class OuterClass {
	
	private String name="admin";
	
	private class InnerClass{
		public void printOuterName(){
			System.out.println(name);//admin
		}
	}
	
	public static void main(String[] args) {
		OuterClass outer=new OuterClass();
		InnerClass inner=outer.new InnerClass();
		inner.printOuterName();
	}
}

//方式二:外部类访问内部类的私有属性
public class OuterClass {
	
	public class InnerClass{
		private String name="admin";
	}
	
	public static void main(String[] args) {
		OuterClass outer=new OuterClass();
		InnerClass inner=outer.new InnerClass();
		System.out.println(inner.name);//admin
	}
}

private失效场景原因分析

首先需要明确一点的是内部类其实是编译时现象,与JVM无关。编译器会把内部类翻译成用美元符号$分割内部类名和外部类名常规文件,但是JVM对此却是一无所知。所以如果我们想分析为什么private“失效”,只需要看看生成的class文件中究竟是什么样子的就可以了,以及编译后class文件代码是否跟我们实际Java代码有较大差异。

下面是Java中javap反编译相关的几个命令,本篇文章用到两个反编译指令分别是:-c,-p。

用法: javap  
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath         指定查找用户类文件的位置
  -cp                指定查找用户类文件的位置
  -bootclasspath     覆盖引导类文件的位置

方式一失效场景

先执行javap -c命令进行反汇编,OuterClass结果如下:

public class com.yimi.demo.OuterClass {
  public com.yimi.demo.OuterClass();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #12                 // String admin
       7: putfield      #14                 // Field name:Ljava/lang/String;
      10: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/yimi/demo/OuterClass
       3: dup
       4: invokespecial #22                 // Method "<init>":()V
       7: astore_1
       8: new           #23                 // class com/yimi/demo/OuterClass$InnerClass
      11: dup
      12: aload_1
      13: dup
      14: invokevirtual #25                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      17: pop
      18: aconst_null
      19: invokespecial #29                 // Method com/yimi/demo/OuterClass$InnerClass."<init>":(Lcom/yimi/demo/OuterClass;Lcom/yimi/demo/OuterClass$InnerClass;)V
      22: astore_2
      23: aload_2
      24: invokevirtual #32                 // Method com/yimi/demo/OuterClass$InnerClass.printOuterName:()V
      27: return

  static java.lang.String access$0(com.yimi.demo.OuterClass);
    Code:
       0: aload_0
       1: getfield      #14                 // Field name:Ljava/lang/String;
       4: areturn
} 

反汇编之后发现多了一个static方法access$0。

static java.lang.String access$0(com.yimi.demo.OuterClass);
Code:
   0: aload_0
   1: getfield      #14                 // Field name:Ljava/lang/String;
   4: areturn 

借助用Javap -p我们查看一下编译后的所有类和成员。

public class com.yimi.demo.OuterClass {
  private java.lang.String name;
  public com.yimi.demo.OuterClass();
  public static void main(java.lang.String[]);
  static java.lang.String access$0(com.yimi.demo.OuterClass);
}

初学java是我们就被告知每个类都至少有一个构造方法,为了保证这一点,如果用户没有给java类定义明确的构造方法的时候,java为我们提供了一个默认的构造方法,这个构造方法没有参数,修饰符是public并且方法体为空。知其然知其所以然嘛,现在可以知道了默认构造方法的保证也是编译器帮助我们做的。另外从反汇编可以看出编译器还会为我们定义的每一个属性都生成一个对应的static方法如access$0,如果还有第二个属性则命名为access$1等等,方法入参为OuterClass的一个类对象。

接下来看一下内部类反汇编之后代码。

class com.yimi.demo.OuterClass$InnerClass {
  final com.yimi.demo.OuterClass this$0;

  public void printOuterName();
    Code:
       0: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #14                 // Field this$0:Lcom/yimi/demo/OuterClass;
       7: invokestatic  #27                 // Method com/yimi/demo/OuterClass.access$0:(Lcom/yimi/demo/OuterClass;)Ljava/lang/String;
      10: invokevirtual #33                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: return

  com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass, com.yimi.demo.OuterClass$InnerClass);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #40                 // Method "<init>":(Lcom/yimi/demo/OuterClass;)V
       5: return
}

使用javap -p查看编译后所有的类和成员。

class com.yimi.demo.OuterClass$InnerClass {
  final com.yimi.demo.OuterClass this$0;
  private com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass);
  public void printOuterName();
  com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass, com.yimi.demo.OuterClass$InnerClass);
}

这里有一处跟外部类很不一样的地方,Java代码写的是内部类是使用private修饰,但是反汇编之后查看以及查看编译后的所有类和成员都没有了private权限修饰符,这是为什么呢?原因还在编译器,当然了跟JVM虚拟机也多少有关系,在JVM虚拟机中不存在私有类,因此编译器会使用私有构造方法生成一个包可见的类。

private com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass);

因为是私有的构造方法,所以外部是没有办法访问到的,因此存在第二个可见构造方法。

com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass, com.yimi.demo.OuterClass$InnerClass);

实际上第二个构造方法会调用第一个构造方法。

还有一处就是内部类中生成了一个外部类的引用this$0,为了方便查看,我们将内部类权限修饰符更改为public,这样更方便我们分析。

public com.yimi.demo.OuterClass$InnerClass(com.yimi.demo.OuterClass);
    Code:
       0: aload_0
       1: invokespecial #11                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #14                 // Field this$0:Lcom/yimi/demo/OuterClass;
       9: return

这种情况下就会生成只有一个构造器的代码了,通过注释可以知道通过构造方法内部类将外部类的引用this$0传递了过来。所以在开发过程中需要慎用内部类,如果使用建议用静态内部类,因为静态内部类不会持有外部类引用。

结论:内部类可以访问外部类的私有属性,实际上是在编译时外部类会生成一个默认权限修饰符修饰静态方法如access$0(),当内部类访问外部类私有属性的时候实际上是通过外部类调用编译器生成的该属性对应的静态方法直接访问的。所以这里一切原因都是因为编译器的特殊处理。

方式二失效场景

这里就不做解释了,处理逻辑类似第一种逻辑,直接上代码。

外部类的反汇编信息以及编译后的所有类及成员。

public class com.yimi.test.OuterClass {
  public com.yimi.test.OuterClass();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/yimi/test/OuterClass
       3: dup
       4: invokespecial #16                 // Method "<init>":()V
       7: astore_1
       8: new           #17                 // class com/yimi/test/OuterClass$InnerClass
      11: dup
      12: aload_1
      13: dup
      14: invokevirtual #19                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      17: pop
      18: invokespecial #23                 // Method com/yimi/test/OuterClass$InnerClass."<init>":(Lcom/yimi/test/OuterClass;)V
      21: astore_2
      22: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokestatic  #32                 // Method com/yimi/test/OuterClass$InnerClass.access$0:(Lcom/yimi/test/OuterClass$InnerClass;)Ljava/lang/String;
      29: invokevirtual #36                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      32: return
}
//所有类及成员
public class com.yimi.test.OuterClass {
  public com.yimi.test.OuterClass();
  public static void main(java.lang.String[]);
}

内部类相关代码如下:

public class com.yimi.test.OuterClass$InnerClass {
  final com.yimi.test.OuterClass this$0;

  public com.yimi.test.OuterClass$InnerClass(com.yimi.test.OuterClass);
    Code:
       0: aload_0
       1: invokespecial #13                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #16                 // Field this$0:Lcom/yimi/test/OuterClass;
       9: aload_0
      10: ldc           #18                 // String admin
      12: putfield      #20                 // Field name:Ljava/lang/String;
      15: return

  static java.lang.String access$0(com.yimi.test.OuterClass$InnerClass);
    Code:
       0: aload_0
       1: getfield      #20                 // Field name:Ljava/lang/String;
       4: areturn
}

//所有类及成员
public class com.yimi.test.OuterClass$InnerClass {
  private java.lang.String name;
  final com.yimi.test.OuterClass this$0;
  public com.yimi.test.OuterClass$InnerClass(com.yimi.test.OuterClass);
  static java.lang.String access$0(com.yimi.test.OuterClass$InnerClass);
}

通过上面两种情况的分析我们知道了都是编译器的处理才使得private"失效"了,下面是官方文档的解释。

if the member or constructor is declared private, then access is permitted if and only if it occurs within the body of the top level class (§7.6) that encloses the declaration of the member or constructor.

其它

上面所说的private“失效”的情况都是针对的成员内部类,事实上静态内部类也是类似的逻辑。当然了局部内部类在方法体内的访问也类似上述逻辑,只是将相应的外部类对应成了该内部类所在的方法。但是有一种情况是例外的,那就是匿名内部类,因为匿名内部类在编译时不会根据属性生成相对应的static方法了,因此这时候在外部类中访问匿名内部类的私有属性是不可以的,编译时就会报语法错误,但是反过来匿名内部类确实可以访问外部类的私有属性的,因为外部类不受匿名内部类影响,对应的属性字段还是会生成相应的access$0之类的方法。

public class Outer {
	
	private Runnable runnable=new Runnable(){

		private String name="admin";
		
		public void run() {
			System.out.println(name);
		}
		
	};
	
	public static void main(String[] args) {
		Outer outer=new Outer();
		outer.runnable.run();
		//System.out.println(outer.runnable.name);//无法访问到name属性
	}
}

让然借助javap命令查看一下编译后的class文件信息。

class com.yimi.test.Outer$1 implements java.lang.Runnable {
  final com.yimi.test.Outer this$0;

  com.yimi.test.Outer$1(com.yimi.test.Outer);
    Code:
       0: aload_0
       1: invokespecial #15                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #18                 // Field this$0:Lcom/yimi/test/Outer;
       9: aload_0
      10: ldc           #20                 // String admin
      12: putfield      #22                 // Field name:Ljava/lang/String;
      15: return

  public void run();
    Code:
       0: getstatic     #29                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #22                 // Field name:Ljava/lang/String;
       7: invokevirtual #35                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return
}
//所有类及成员
class com.yimi.test.Outer$1 implements java.lang.Runnable {
  private java.lang.String name;
  final com.yimi.test.Outer this$0;
  com.yimi.test.Outer$1(com.yimi.test.Outer);
  public void run();
}

从这里可以看出来匿名内部类在编译后明显缺少了static之类的方法,但是让人会持有外部类的引用this$0。

有关内部类的属性本篇博客先介绍到这里,有关内部类更多的知识点后续有时间再继续探讨。由于个人能力有限,所以在文章中难免有错误疏忽之处,还望及时之指出以便查漏补缺共同进步!

参考资料

深入理解Java中为什么内部类可以访问外部类的成员

领略Java内部类的“内部”

java反汇编及JVM指令集(指令码、助记符、功能描述)

JAVA构造时成员初始化的陷阱

评论

您确定要删除吗?删除之后不可恢复