Java 프로그래밍에서는 private 키워드를 사용하여 멤버를 수정하면 해당 멤버가 위치한 클래스와 해당 클래스의 메서드만 사용할 수 있으며 다른 클래스는 이 private 멤버에 접근할 수 없습니다.
위에서는 private 수식어의 기본 기능에 대해 설명했습니다. 오늘은 private 기능의 실패에 대해 알아보겠습니다.
자바 내부 클래스
많은 사람들이 Java에서 내부 클래스를 사용해 왔다고 생각합니다. Java에서는 클래스 내부의 클래스를 중첩 클래스라고도 하는 내부 클래스로 정의할 수 있습니다. 간단한 내부 클래스 구현은 다음과 같이 복사할 수 있습니다.
클래스 외부 클래스 {
클래스 내부 클래스{
}
}
오늘의 질문은 Java 내부 클래스에 관한 것이며, 이 기사의 연구와 관련된 내부 클래스 지식의 일부만 포함합니다. Java 내부 클래스에 대한 구체적인 후속 기사가 소개됩니다.
첫 번째 실패?
프로그래밍에서 자주 사용하는 시나리오는 내부 클래스에서 외부 클래스의 전용 멤버 변수나 메서드에 액세스하는 것입니다. 다음 코드로 구현되었습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 OuterClass {
개인 문자열 언어 = "en";
개인 문자열 지역 = "미국";
공개 클래스 InnerClass {
공공 무효 printOuterClassPrivateFields() {
문자열 필드 = "언어=" + 언어 + ";지역=" + 지역;
System.out.println(필드);
}
}
공개 정적 무효 메인(String[] args) {
OuterClass 외부 = 새로운 OuterClass();
OuterClass.InnerClass inner = external.new InnerClass();
inner.printOuterClassPrivateFields();
}
}
Private-modified 멤버는 해당 멤버가 나타내는 클래스에서만 액세스할 수 있다는 것이 사실이 아닌 이유는 무엇입니까? 비공개는 정말 무효인가요?
컴파일러가 문제를 일으키고 있나요?
javap 명령을 사용하여 생성된 두 클래스 파일을 살펴보겠습니다.
OuterClass의 디컴파일 결과 복사 코드는 다음과 같습니다.
15:30 $ javap -c 외부 클래스
"OuterClass.java"에서 컴파일됨
공용 클래스 OuterClass는 java.lang.Object를 확장합니다.
공공 OuterClass();
암호:
0: 로드_0
1: Invokespecial #11; //메소드 java/lang/Object."<init>":()V
4: 로드_0
5: ldc #13;
7: putfield #15; //필드 언어:Ljava/lang/String;
10: 로드_0
11: ldc #17; //문자열 US;
13: putfield #19; //필드 영역:Ljava/lang/String;
16: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: 새로운 #1; //클래스 OuterClass;
3: 복제
4: Invokespecial #27; //메소드 "<init>":()V
7: astore_1
8: 새로운 #28; //클래스 OuterClass$InnerClass
11: 복제
12: 로드_1
13: 복제
14: Invokevirtual #30; //메소드 java/lang/Object.getClass:()Ljava/lang/Class;
17: 팝
18: Invokespecial #34; //OuterClass$InnerClass."<init>":(LOuterClass;)V
21: astore_2
22: 로드_2
23: Invokevirtual #37; //메서드 OuterClass$InnerClass.printOuterClassPrivateFields:()V
26: 복귀
정적 java.lang.String access$0(OuterClass);
암호:
0: 로드_0
1: getfield #15; //필드 언어:Ljava/lang/String;
4: 복귀
정적 java.lang.String access$1(OuterClass);
암호:
0: 로드_0
1: getfield #19; //필드 영역:Ljava/lang/String;
4: 복귀
}
뭐? 아니요, 우리는 OuterClass에서 이 두 가지 메소드를 정의하지 않았습니다.
정적 java.lang.String access$0(OuterClass);
암호:
0: 로드_0
1: getfield #15; //필드 언어:Ljava/lang/String;
4: 복귀
정적 java.lang.String access$1(OuterClass);
암호:
0: 로드_0
1: getfield #19; //필드 영역:Ljava/lang/String;
4: 복귀
}
주어진 설명에 따르면 access$0은 externalClass의 언어 속성을 반환하고, access$1은 externalClass의 지역 속성을 반환합니다. 그리고 두 메서드 모두 OuterClass의 인스턴스를 매개 변수로 받아들입니다. 이 두 가지 메서드가 생성되는 이유는 무엇이며 어떤 역할을 합니까? 내부 클래스의 디컴파일 결과를 살펴보면 알 수 있습니다.
OuterClass$InnerClass의 디컴파일 결과
다음과 같이 코드 코드를 복사합니다.
15:37 $ javap -c OuterClass/$InnerClass
"OuterClass.java"에서 컴파일됨
공용 클래스 OuterClass$InnerClass는 java.lang.Object를 확장합니다.
최종 OuterClass this$0;
공개 OuterClass$InnerClass(OuterClass);
암호:
0: 로드_0
1: 로드_1
2: putfield #10; //이 필드를 입력하세요.$0:LOuterClass;
5: 로드_0
6: Invokespecial #12; //메소드 java/lang/Object."<init>":()V
9: 복귀
공공 무효 printOuterClassPrivateFields();
암호:
0: 새로운 #20; //클래스 java/lang/StringBuilder
3: 복제
4: ldc #22; //문자열 언어=
6: Invokespecial #24; //메소드 java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: 로드_0
10: getfield #10; //이 필드를 $0:LOuterClass;
13: Invokestatic #27; //OuterClass.access$0:(LOuterClass;)Ljava/lang/String;
16: Invokevirtual #33; //메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #37; //문자열 ;지역=
21: Invokevirtual #33; //메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: 로드_0
25: getfield #10; //이 필드를 $0:LOuterClass;
28: Invokestatic #39; //OuterClass.access$1:(LOuterClass;)Ljava/lang/String;
31: Invokevirtual #33; //메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: Invokevirtual #42; //메소드 java/lang/StringBuilder.toString:()Ljava/lang/String;
37: astore_1
38: getstatic #46; //필드 java/lang/System.out:Ljava/io/PrintStream;
41: 로드_1
42: Invokevirtual #52; //메소드 java/io/PrintStream.println:(Ljava/lang/String;)V
45: 복귀
}
다음 코드는 OuterClass의 언어 전용 속성을 얻는 것이 목적인 access$0의 코드를 호출합니다.
다음과 같이 코드 코드를 복사합니다.
13: Invokestatic #27; //OuterClass.access$0:(LOuterClass;)Ljava/lang/String;
다음 코드는 OutherClass의 지역 개인 속성을 얻는 것이 목적인 access$1의 코드를 호출합니다.
다음과 같이 코드 코드를 복사합니다.
28: Invokestatic #39; //OuterClass.access$1:(LOuterClass;)Ljava/lang/String;
참고: 내부 클래스가 생성되면 외부 클래스의 참조가 전달되어 내부 클래스의 속성으로 사용되므로 내부 클래스는 외부 클래스에 대한 참조를 보유합니다.
this$0은 내부 클래스가 보유한 외부 클래스 참조입니다. 참조는 생성자 메서드를 통해 전달되고 할당됩니다.
다음과 같이 코드 코드를 복사합니다.
최종 OuterClass this$0;
공개 OuterClass$InnerClass(OuterClass);
암호:
0: 로드_0
1: 로드_1
2: putfield #10; //이 필드를 입력하세요.$0:LOuterClass;
5: 로드_0
6: Invokespecial #12; //메소드 java/lang/Object."<init>":()V
9: 복귀
요약
private의 이 부분은 유효하지 않은 것처럼 보이지만 실제로는 유효하지 않습니다. 왜냐하면 내부 클래스가 외부 클래스의 private 속성을 호출할 때 실제 실행은 컴파일러에서 생성된 속성의 정적 메서드(예: 액세스)를 호출하는 것이기 때문입니다. $0, access$1 등)을 사용하여 이러한 속성 값을 가져옵니다. 이것은 모두 컴파일러에 의한 특수 처리입니다.
이번에는 효과가 없나요?
위의 쓰기 방법이 매우 일반적으로 사용된다면 이 쓰기 방법은 거의 사용되지 않지만 실행될 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 AnotherOuterClass {
공개 정적 무효 메인(String[] args) {
InnerClass inner = new AnotherOuterClass().new InnerClass();
System.out.println("InnerClass Filed = " + inner.x);
}
클래스 내부 클래스 {
개인 정수 x = 10;
}
}
위와 같이 javap를 이용하여 디컴파일하고 살펴봅니다. 하지만 이번에는 InnerClass의 결과를 살펴보겠습니다. 코드는 다음과 같습니다.
16:03 $ javap -c AnotherOuterClass/$InnerClass
"AnotherOuterClass.java"에서 컴파일됨
클래스 AnotherOuterClass$InnerClass는 java.lang.Object를 확장합니다.
final AnotherOuterClass this$0;
AnotherOuterClass$InnerClass(AnotherOuterClass);
암호:
0: 로드_0
1: 로드_1
2: putfield #12; //Field this$0:LAnotherOuterClass;
5: 로드_0
6: Invokespecial #14; //메소드 java/lang/Object."<init>":()V
9: 로드_0
10: 바이푸시 10
12: putfield #17; 필드 x:I;
15: 복귀
static int access$0(AnotherOuterClass$InnerClass);
암호:
0: 로드_0
1: getfield #17; //필드 x:I;
4: 반환
}
이런 일이 다시 발생했고, 컴파일러는 x 값을 얻기 위해 private 속성을 한 번 얻기 위해 백도어 메소드 access$0을 자동으로 생성했습니다.
AnotherOuterClass.class의 디컴파일 결과 복사 코드는 다음과 같습니다.
16:08 $ javap -c AnotherOuterClass
"AnotherOuterClass.java"에서 컴파일됨
공용 클래스 AnotherOuterClass는 java.lang.Object를 확장합니다.
공개 AnotherOuterClass();
암호:
0: 로드_0
1: Invokespecial #8; //메소드 java/lang/Object."<init>":()V
4: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: 새로운 #16; //클래스 AnotherOuterClass$InnerClass
3: 복제
4: 새로운 #1; //클래스 AnotherOuterClass
7: 복제
8: Invokespecial #18; //메소드 "<init>":()V
11: 복제
12: Invokevirtual #19; //메소드 java/lang/Object.getClass:()Ljava/lang/Class;
15:팝
16: Invokespecial #23; //AnotherOuterClass$InnerClass."<init>":(LAnotherOuterClass;)V
19: astore_1
20: getstatic #26; //필드 java/lang/System.out:Ljava/io/PrintStream;
23: 새로운 #32; //클래스 java/lang/StringBuilder
26: 듀프
27: ldc #34; //String InnerClass Filed =
29: Invokespecial #36; //메소드 java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
32: 로드_1
33: Invokestatic #39; //AnotherOuterClass$InnerClass.access$0:(LAnotherOuterClass$InnerClass;)I
36: Invokevirtual #43; //메소드 java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
39: Invokevirtual #47; //메소드 java/lang/StringBuilder.toString:()Ljava/lang/String;
42: Invokevirtual #51; //메소드 java/io/PrintStream.println:(Ljava/lang/String;)V
45: 복귀
}
이 호출은 내부 클래스의 인스턴스를 통해 전용 속성 x를 얻기 위한 외부 클래스의 작업입니다.
33: Invokestatic #39; //AnotherOuterClass$InnerClass.access$0:(LAnotherOuterClass$InnerClass;)I
또 다른 요약
그 중 공식 Java 문서에는 다음과 같은 문장이 있습니다.
멤버 또는 생성자가 비공개로 선언된 경우 멤버 또는 생성자의 선언을 포함하는 최상위 클래스(§7.6)의 본문 내에서 발생하는 경우에만 액세스가 허용됩니다.
즉, 내부 클래스의 멤버와 생성자가 전용 수정자로 설정된 경우 외부 클래스인 경우에만 액세스가 허용됩니다.
외부인이 내부 클래스의 비공개 멤버에 액세스하는 것을 방지하는 방법
위의 두 부분을 읽은 후에는 외부 클래스에서 내부 클래스의 전용 멤버에 액세스하는 것이 매우 어렵다는 것을 느낄 것입니다. 실제로 컴파일러가 "참견하기"를 원하는 사람은 누구입니까? 완료. 그것은 익명의 내부 클래스를 사용하는 것입니다.
mRunnable 객체의 유형이 익명 내부 클래스의 유형(일반적으로 얻을 수 없음)이 아닌 Runnable이고 Runanble에 x 속성이 없으므로 mRunnable.x는 허용되지 않습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 PrivateToOuter {
실행 가능 mRunnable = 새로운 Runnable(){
개인 정수 x=10;
@보수
공개 무효 실행() {
System.out.println(x);
}
};
공개 정적 무효 메인(문자열[] 인수){
PrivateToOuter p = new PrivateToOuter();
//System.out.println("anonymous class private filed= "+ p.mRunnable.x) //허용되지 않음
p.mRunnable.run(); // 허용됨
}
}
최종 요약
이 기사에서는 private이 표면적으로 유효하지 않은 것처럼 보이지만 실제로는 호출 시 간접적인 방법을 통해 private 속성을 얻습니다.
Java의 내부 클래스는 생성 시 외부 클래스에 대한 애플리케이션을 보유하지만 C++는 그렇지 않습니다.