图片 4

Java中失效的private修饰符解析

在Java编程中,使用private关键字修饰了某个成员,只有这个成员所在的类和这个类的方法可以使用,其他的类都无法访问到这个private成员。

看JAVA的反射时,看到有个synthetic ,还有一个方法isSynthetic()
很好奇,就了解了一下:

上面描述了private修饰符的基本职能,今天来研究一下private功能失效的情况。

1.定义

Any constructs introduced by a Java compiler that do not have a
corresponding construct in the source code must be marked as synthetic,
except for default constructors, the class initialization method, and
the values and valueOf methods of the Enum class.

大意为:由java编译器生成的(除了像默认构造函数这一类的)方法,或者类

Java内部类

在Java中相信很多人都用过内部类,Java允许在一个类里面定义另一个类,类里面的类就是内部类,也叫做嵌套类。一个简单的内部类实现可以如下

class OuterClass {
    class InnerClass{
    }
}

今天的问题和Java内部类相关,只涉及到部分和本文研究相关的内部类知识,具体关于Java内部类后续的文章会介绍。

2.实例

既然知道synthetic方法和synthetic类是由编译器生成的,那到底编译器会怎么生成这些东西,又在什么情况下会生成这些东西呢?

先看一段代码:

import static java.lang.System.out;

public final class DemonstrateSyntheticMethods
{
   public static void main(final String[] arguments)
   {
      DemonstrateSyntheticMethods.NestedClass nested =
         new DemonstrateSyntheticMethods.NestedClass();
      out.println("String: " + nested.highlyConfidential);
   }

   private static final class NestedClass
   {
      private String highlyConfidential = "Don't tell anyone about me";
      private int highlyConfidentialInt = 42;
      private Calendar highlyConfidentialCalendar = Calendar.getInstance();
      private boolean highlyConfidentialBoolean = true;
   }
}

编译之后,可以看到三个文件:

图片 1

其中,最下面的这个类文件很好解释,就是我们的主class,中间的文件,是我们的内部类,上面的文件,后面再讲,我们先看一下中间这个内部类

第一次失效?

一个我们在编程中经常用到的场景,就是在一个内部类里面访问外部类的private成员变量或者方法,这是可以的。如下面的代码实现。

public class OuterClass {
  private String language = "en";
  private String region = "US";

  public class InnerClass {
      public void printOuterClassPrivateFields() {
          String fields = "language=" + language + ";region=" + region;
          System.out.println(fields);
      }
  }

  public static void main(String[] args) {
      OuterClass outer = new OuterClass();
      OuterClass.InnerClass inner = outer.new InnerClass();
      inner.printOuterClassPrivateFields();
  }
}

这是为什么呢,不是private修饰的成员只能被成员所述的类才能访问么?难道private真的失效了么?

2.1 内部类的反编译结果

用javap
反编译DemonstrateSyntheticMethods$NestedClass.class,得到如下结果:

javap DemonstrateSyntheticMethods$NestedClass.class
Compiled from "DemonstrateSyntheticMethods.java"
final class DemonstrateSyntheticMethods$NestedClass {
  DemonstrateSyntheticMethods$NestedClass(DemonstrateSyntheticMethods$1);

  static java.lang.String access$100(DemonstrateSyntheticMethods$NestedClass);
}

先把构造函数放一边,我们来看这个标黑的方法access$100
这个是怎么回事呢?我们的源文件里找不到这个access方法啊?

编译器在捣鬼?

我们使用javap命令查看一下生成的两个class文件

OuterClass的反编译结果

15:30 $ javap -c  OuterClass
Compiled from "OuterClass.java"
public class OuterClass extends java.lang.Object{
public OuterClass();
  Code:
   0:  aload_0
   1:  invokespecial    #11; //Method java/lang/Object."<init>":()V
   4:  aload_0
   5:  ldc  #13; //String en
   7:  putfield #15; //Field language:Ljava/lang/String;
   10: aload_0
   11: ldc  #17; //String US
   13: putfield #19; //Field region:Ljava/lang/String;
   16: return

public static void main(java.lang.String[]);
  Code:
   0:  new  #1; //class OuterClass
   3:  dup
   4:  invokespecial    #27; //Method "<init>":()V
   7:  astore_1
   8:  new  #28; //class OuterClass$InnerClass
   11: dup
   12: aload_1
   13: dup
   14: invokevirtual    #30; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   17: pop
   18: invokespecial    #34; //Method OuterClass$InnerClass."<init>":(LOuterClass;)V
   21: astore_2
   22: aload_2
   23: invokevirtual    #37; //Method OuterClass$InnerClass.printOuterClassPrivateFields:()V
   26: return

static java.lang.String access$0(OuterClass);
  Code:
   0:  aload_0
   1:  getfield #15; //Field language:Ljava/lang/String;
   4:  areturn

static java.lang.String access$1(OuterClass);
  Code:
   0:  aload_0
   1:  getfield #19; //Field region:Ljava/lang/String;
   4:  areturn

}

咦?不对,在OuterClass中我们并没有定义这两个方法

static java.lang.String access$0(OuterClass);
  Code:
   0:  aload_0
   1:  getfield #15; //Field language:Ljava/lang/String;
   4:  areturn

static java.lang.String access$1(OuterClass);
  Code:
   0:  aload_0
   1:  getfield #19; //Field region:Ljava/lang/String;
   4:  areturn

}

从给出来的注释来看,access$0返回outerClass的language属性;access$1返回outerClass的region属性。并且这两个方法都接受OuterClass的实例作为参数。这两个方法为什么生成呢,有什么作用呢?我们看一下内部类的反编译结果就知道了。

OuterClass$InnerClass的反编译结果

15:37 $ javap -c OuterClass$InnerClass
Compiled from "OuterClass.java"
public class OuterClass$InnerClass extends java.lang.Object{
final OuterClass this$0;

public OuterClass$InnerClass(OuterClass);
  Code:
   0:  aload_0
   1:  aload_1
   2:  putfield #10; //Field this$0:LOuterClass;
   5:  aload_0
   6:  invokespecial    #12; //Method java/lang/Object."<init>":()V
   9:  return

public void printOuterClassPrivateFields();
  Code:
   0:  new  #20; //class java/lang/StringBuilder
   3:  dup
   4:  ldc  #22; //String language=
   6:  invokespecial    #24; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   9:  aload_0
   10: getfield #10; //Field this$0:LOuterClass;
   13: invokestatic #27; //Method OuterClass.access$0:(LOuterClass;)Ljava/lang/String;
   16: invokevirtual    #33; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   19: ldc  #37; //String ;region=
   21: invokevirtual    #33; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   24: aload_0
   25: getfield #10; //Field this$0:LOuterClass;
   28: invokestatic #39; //Method OuterClass.access$1:(LOuterClass;)Ljava/lang/String;
   31: invokevirtual    #33; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   34: invokevirtual    #42; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   37: astore_1
   38: getstatic    #46; //Field java/lang/System.out:Ljava/io/PrintStream;
   41: aload_1
   42: invokevirtual    #52; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   45: return
}

下面代码调用access$0的代码,其目的是得到OuterClass的language 私有属性。

13:   invokestatic #27; //Method OuterClass.access$0:(LOuterClass;)Ljava/lang/String;

下面代码调用了access$1的代码,其目的是得到OutherClass的region 私有属性。

28:   invokestatic #39; //Method OuterClass.access$1:(LOuterClass;)Ljava/lang/String;

注意:在内部类构造的时候,会将外部类的引用传递进来,并且作为内部类的一个属性,所以内部类会持有一个其外部类的引用。

this$0就是内部类持有的外部类引用,通过构造方法传递引用并赋值。

final OuterClass this$0;

public OuterClass$InnerClass(OuterClass);
  Code:
   0:  aload_0
   1:  aload_1
   2:  putfield #10; //Field this$0:LOuterClass;
   5:  aload_0
   6:  invokespecial    #12; //Method java/lang/Object."<init>":()V
   9:  return

2.2 synthetic方法

这个方法就是编译器生成的synthetic方法,读者不信的话,可以用method.isSynthetic()
去验证一下。

为何要生成这样一个方法呢?

可以看到,我们的NestedClass类中,highConfidential是一个私有属性,而我们在外部类DemonstrateSyntheticMethods中,直接引用了这个属性。作为一个内部类,NestedClass的属性被外部类引用,在语义上毫无问题,但是这却苦了编译器。

为了能让一个private的变量被引用到,编译器生成了一个package
scope的access方法,这个方法就是一个get方法,在外部类使用highConfidential这个属性时,实际是使用了这个access方法。

在javap中可以看到直接的证据:

图片 2

图中红框的位置,可以很清楚的看到main方法实际上调用了access$100这个方法。

所以,结论很清楚了,编译器为了方便内部类的私有成员被外部类引用,生成了一个get方法,这可以被理解为一个trick,绕开了private成员变量的限制。

小结

这部分private看上去失效可,实际上并没有失效,因为当内部类调用外部类的私有属性时,其真正的执行是调用了编译器生成的属性的静态方法(即acess$0,access$1等)来获取这些属性值。这一切都是编译器的特殊处理。

2.3 synthetic类

定义已经提到,编译器不仅仅会生成方法,也会生成synthetic类。

我们回过头来看2.1提到的最后一个类DemonstrateSyntheticMethods$1.class

这个类是一个完全的空类,反编译后是这个样子:

// $FF: synthetic class
class DemonstrateSyntheticMethods$1 {
}

这个类只出场了一次,作为内部类NestedClass的package
scope的构造函数,如图所示:

图片 3

 

 那么,这个类的作用呢?笔者查了很多资料,都没有明确的说明这个类的用途,只能根据代码做推测如下:

NestedClass作为一个private类,其默认构造函数也是private的。那么,事实上,作为外部类的DemonstrateSyntheticMethods类,没有办法new这个内部类的对象,而这和我们需要的语义相违背。

那么,为了实现语义,编译器又用了一个trick,悄悄的生成了一个构造函数NestedClass(DemonstrateSyntheticMethods$1
obj), 这个构造函数是包可见的。

那么,外部类则可以通过new
NestedClass(null)的方式,得到内部类的对象。如果读者检查一下main方法的话,可以看到这个方法的调用如下图所示。

这就是这个synthetic类的作用。如果我们给我们的NestedClass
增加一个public级别的默认构造函数的话,则可以看到编译器不会再生成这个synthetic类。

图片 4

这次也失效?

如果说上面的写法很常用,那么这样的写法是不是很少接触,但是却可以运行。

public class AnotherOuterClass {
  public static void main(String[] args) {
      InnerClass inner = new AnotherOuterClass().new InnerClass();
      System.out.println("InnerClass Filed = " + inner.x);
  }

  class InnerClass {
      private int x = 10;
  }

}

和上面一样,使用javap反编译看一下。不过这次我们先看一下InnerClass的结果

16:03 $ javap -c AnotherOuterClass$InnerClass
Compiled from "AnotherOuterClass.java"
class AnotherOuterClass$InnerClass extends java.lang.Object{
final AnotherOuterClass this$0;

AnotherOuterClass$InnerClass(AnotherOuterClass);
  Code:
   0:  aload_0
   1:  aload_1
   2:  putfield #12; //Field this$0:LAnotherOuterClass;
   5:  aload_0
   6:  invokespecial    #14; //Method java/lang/Object."<init>":()V
   9:  aload_0
   10: bipush   10
   12: putfield #17; //Field x:I
   15: return

static int access$0(AnotherOuterClass$InnerClass);
  Code:
   0:  aload_0
   1:  getfield #17; //Field x:I
   4:  ireturn

}

又出现了,编译器又自动生成了一个获取私有属性的后门方法access$0一次来获取x的值。

AnotherOuterClass.class的反编译结果

16:08 $ javap -c AnotherOuterClass
Compiled from "AnotherOuterClass.java"
public class AnotherOuterClass extends java.lang.Object{
public AnotherOuterClass();
  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  #16; //class AnotherOuterClass$InnerClass
   3:  dup
   4:  new  #1; //class AnotherOuterClass
   7:  dup
   8:  invokespecial    #18; //Method "<init>":()V
   11: dup
   12: invokevirtual    #19; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   15: pop
   16: invokespecial    #23; //Method AnotherOuterClass$InnerClass."<init>":(LAnotherOuterClass;)V
   19: astore_1
   20: getstatic    #26; //Field java/lang/System.out:Ljava/io/PrintStream;
   23: new  #32; //class java/lang/StringBuilder
   26: dup
   27: ldc  #34; //String InnerClass Filed =
   29: invokespecial    #36; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   32: aload_1
   33: invokestatic #39; //Method AnotherOuterClass$InnerClass.access$0:(LAnotherOuterClass$InnerClass;)I
   36: invokevirtual    #43; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   39: invokevirtual    #47; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   42: invokevirtual    #51; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   45: return

}

其中这句调用就是外部类通过内部类的实例获取私有属性x的操作

33:   invokestatic #39; //Method AnotherOuterClass$InnerClass.access$0:(LAnotherOuterClass$InnerClass;)I

3.结论

编译器通过生成一些在源代码中不存在的synthetic方法和类的方式,实现了对private级别的字段和类的访问,从而绕开了语言限制,这可以算是一种trick。

在实际生产和应用中,基本不存在程序员需要考虑synthetic的地方。

PS: 在此提一个的常见的存在synthetic的案例。

如果同时用到了Enum和switch,如先定义一个enum枚举,然后用switch遍历这个枚举,java编译器会偷偷生成一个synthetic的数组,数组内容是enum的实例。

对于这种情况,笔者找到了一个资料,可供参考:

Java动态修改Enum实例


 

完。

再来个总结

其中java官方文档 有这样一句话

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.

意思是
如果(内部类的)成员和构造方法设定成了私有修饰符,当且仅当其外部类访问时是允许的。

如何让内部类私有成员不被外部访问

相信看完上面两部分,你会觉得,内部类的私有成员想不被外部类访问都很困难吧,谁让编译器“爱管闲事”呢,其实也是可以做到的。那就是使用匿名内部类。

由于mRunnable对象的类型为Runnable,而不是匿名内部类的类型(我们无法正常拿到),而Runanble中没有x这个属性,所以mRunnable.x是不被允许的。

public class PrivateToOuter {
  Runnable mRunnable = new Runnable(){
      private int x=10;
      @Override
      public void run() {
          System.out.println(x);
      }
  };

  public static void main(String[] args){
      PrivateToOuter p = new PrivateToOuter();
      //System.out.println("anonymous class private filed= "+ p.mRunnable.x); //not allowed
      p.mRunnable.run(); // allowed
  }
}

最后总结

  • 在本文中,private表面上看上去失效了,但实际上是没有的,而是在调用时通过间接的方法来获取私有的属性。
  • Java的内部类构造时持有对外部类的应用,C++不会,这一点和C++不一样。

发表评论

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