在Java程式設計中,使用private關鍵字修飾了某個成員,只有這個成員所在的類別和這個類別的方法可以使用,其他的類別都無法存取到這個private成員。
上面描述了private修飾符的基本職能,今天來研究一下private功能失效的情況。
Java內部類別
在Java中相信很多人都用過內部類,Java允許在一個類別裡面定義另一個類,類別裡面的類別就是內部類,也叫做嵌套類別。一個簡單的內部類別實作可以如下複製程式碼如下:
class OuterClass {
class InnerClass{
}
}
今天的問題和Java內部類別相關,只涉及部分和本文研究相關的內部類別知識,具體關於Java內部類別後續的文章會介紹。
第一次失效?
一個我們在程式設計中常用到的場景,就是在一個內部類別裡面存取外部類別的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真的失效了?
編譯器在搗鬼?
我們使用javap指令來查看一下產生的兩個class文件
OuterClass的反編譯結果複製程式碼如下:
15:30 $ javap -c OuterClass
Compiled 來自 "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 來自 "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
小結
這部分private看起來失效可,實際上並沒有失效,因為當內部類別呼叫外部類別的私有屬性時,其真正的執行是呼叫了編譯器產生的屬性的靜態方法(即acess$0,access$1等)來取得這些屬性值。這一切都是編譯器的特殊處理。
這次也失效?
如果說上面的寫法很常用,那麼這樣的寫法是不是很少接觸,但卻可以運作。
複製代碼代碼如下:
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
再來個總結
其中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++不一樣。