I. Grundlagen innerer Klassen

In Java kann man eine Klasse innerhalb einer anderen Klasse oder innerhalb einer Methode definieren. Eine solche Klasse wird als innere Klasse bezeichnet. Im weiteren Sinne umfassen innere Klassen im Allgemeinen die folgenden vier Typen: Member-Innerklassen, lokale innere Klassen, anonyme innere Klassen und statische innere Klassen. Betrachten wir zunächst die Verwendung dieser vier Arten von inneren Klassen.

1. Member-Innerklasse

Eine Member-Innerklasse ist die gebräuchlichste Art innerer Klasse. Sie wird innerhalb einer anderen Klasse definiert, wie unten gezeigt:

class Circle {
    double radius = 0;
     
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

Auf diese Weise scheint die Klasse Draw ein Member der Klasse Circle zu sein, und Circle wird als äußere Klasse bezeichnet. Eine Member-Innere-Klasse kann bedingungslos auf alle Member-Eigenschaften und Member-Methoden der äußeren Klasse zugreifen, einschließlich privater und statischer Member.

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
            System.out.println(count);   //外部类的静态成员
        }
    }
}

Beachten Sie jedoch, dass eine Verdeckung (Hiding) auftritt, wenn eine Member-Innere-Klasse eine Member-Variable oder -Methode mit demselben Namen wie die äußere Klasse besitzt. Das heißt, standardmäßig wird auf das Member der Member-Inneren-Klasse zugegriffen. Um auf das gleichnamige Member der äußeren Klasse zuzugreifen, müssen Sie folgendermaßen darauf zugreifen:

外部类.this.成员变量
外部类.this.成员方法

Obwohl eine Member-Innere-Klasse bedingungslos auf die Member der äußeren Klasse zugreifen kann, kann die äußere Klasse nicht so frei auf die Member einer Member-Inneren-Klasse zugreifen. In der äußeren Klasse müssen Sie, um auf Member einer Member-Inneren-Klasse zuzugreifen, zuerst ein Objekt der Member-Inneren-Klasse erstellen und dann über eine Referenz darauf zugreifen, die auf dieses Objekt zeigt:

class Circle {
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
}

Eine Member-Innere-Klasse ist an die äußere Klasse gebunden. Das heißt, um ein Objekt einer Member-Inneren-Klasse zu erstellen, muss zuerst ein Objekt der äußeren Klasse existieren. Die allgemeine Vorgehensweise zum Erstellen eines Objekts einer Member-Inneren-Klasse ist wie folgt:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
         
        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}
 
class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    class Inner {
        public Inner() {
             
        }
    }
}

Innere Klassen können private, protected, public und Paketzugriff haben. Zum Beispiel kann im obigen Beispiel auf die Member-Innere-Klasse Inner, wenn sie mit private modifiziert wird, nur innerhalb der äußeren Klasse zugegriffen werden; wenn sie mit public modifiziert wird, kann von überall auf sie zugegriffen werden; wenn sie mit protected modifiziert wird, kann nur innerhalb desselben Pakets oder von Klassen, die die äußere Klasse erben, auf sie zugegriffen werden; wenn sie Standardzugriff hat, kann nur innerhalb desselben Pakets auf sie zugegriffen werden. Das unterscheidet sich ein wenig von einer äußeren Klasse, die nur mit public und Paketzugriff modifiziert werden kann. Ich persönlich verstehe es so: Da eine Member-Innere-Klasse wie ein Member der äußeren Klasse aussieht, kann sie genau wie Klassenmember mehrere Zugriffsmodifikatoren haben.

2. Lokale innere Klasse

Eine lokale innere Klasse ist eine Klasse, die innerhalb einer Methode oder eines Gültigkeitsbereichs definiert wird. Der Unterschied zwischen ihr und einer inneren Mitgliedsklasse besteht darin, dass der Zugriff auf eine lokale innere Klasse auf diese Methode oder diesen Gültigkeitsbereich beschränkt ist.

class People{
    public People() {
         
    }
}
 
class Man{
    public Man(){
         
    }
     
    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}
 

Hinweis: Eine lokale innere Klasse ist wie eine lokale Variable innerhalb einer Methode; sie kann keine Modifikatoren public, protected, private oder static haben.

3. Anonyme innere Klasse

Anonyme innere Klassen werden wahrscheinlich am häufigsten verwendet, wenn wir in unserer täglichen Arbeit Code schreiben. Beim Schreiben von Event-Listener-Code ist die Verwendung anonymer innerer Klassen nicht nur praktisch, sondern macht den Code auch leichter wartbar. Der folgende Code ist ein Android-Event-Listener-Code:

scan_bt.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});
 
history_bt.setOnClickListener(new OnClickListener() {
     
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});

Dieser Code setzt Listener für zwei Buttons, und hier werden anonyme innere Klassen verwendet. In diesem Code:

new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
}

Dies ist die Verwendung einer anonymen inneren Klasse. Im Code muss ein Listener-Objekt für den Button gesetzt werden. Mit einer anonymen inneren Klasse kann ein entsprechendes Objekt erstellt werden, während die Methoden der Elternklasse oder des Interfaces implementiert werden, aber die Voraussetzung ist, dass die Elternklasse oder das Interface bereits existieren muss. Natürlich ist die folgende Vorgehensweise ebenfalls akzeptabel und erzielt denselben Effekt wie die Verwendung der obigen anonymen inneren Klasse.

private void setListener()
{
    scan_bt.setOnClickListener(new Listener1());       
    history_bt.setOnClickListener(new Listener2());
}
 
class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}
 
class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}

Obwohl diese Vorgehensweise denselben Effekt erzielen kann, ist sie sowohl weitschweifig als auch schwer zu warten, daher werden im Allgemeinen anonyme innere Klassen verwendet, um Event-Listener-Code zu schreiben. Ebenso können anonyme innere Klassen keine Zugriffsmodifikatoren oder den static-Modifikator haben.

Eine anonyme innere Klasse ist die einzige Art von Klasse, die keinen Konstruktor hat. Da sie keinen Konstruktor hat, ist der Einsatzbereich anonymer innerer Klassen sehr begrenzt. Die meisten anonymen inneren Klassen werden für Interface-Callbacks verwendet. Zur Compile-Zeit benennt das System eine anonyme innere Klasse automatisch als Outter$1.class. Im Allgemeinen werden anonyme innere Klassen verwendet, um von anderen Klassen zu erben oder Interfaces zu implementieren, ohne zusätzliche Methoden hinzufügen zu müssen; sie implementieren oder überschreiben nur geerbte Methoden.

4. Statische innere Klasse

Eine statische innere Klasse ist ebenfalls eine Klasse, die innerhalb einer anderen Klasse definiert wird, außer dass das Schlüsselwort static vor die Klasse gesetzt wird. Eine statische innere Klasse muss nicht von der äußeren Klasse abhängen, was den statischen Member-Eigenschaften einer Klasse einigermaßen ähnelt, und sie kann nicht die nicht-statischen Membervariablen oder -methoden der äußeren Klasse verwenden. Das ist leicht zu verstehen: Da ein Objekt einer statischen inneren Klasse ohne ein Objekt der äußeren Klasse erstellt werden kann, würde ein Widerspruch entstehen, wenn es erlaubt wäre, auf die nicht-statischen Member der äußeren Klasse zuzugreifen, da die nicht-statischen Member der äußeren Klasse von einem bestimmten Objekt abhängen müssen.

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}
 
class Outter {
    public Outter() {
         
    }
     
    static class Inner {
        public Inner() {
             
        }
    }
}


II. Eingehendes Verständnis innerer Klassen

1. Warum kann eine Member-Inner-Class uneingeschränkt auf die Member der äußeren Klasse zugreifen?

Vorher haben wir bereits erörtert, dass eine Member-Inner-Class uneingeschränkt auf die Member der äußeren Klasse zugreifen kann. Wie genau wird das also umgesetzt? Dekompilieren wir die Bytecode-Datei, um es herauszufinden. Tatsächlich kompiliert der Compiler die Member-Inner-Class in eine separate Bytecode-Datei. Nachfolgend der Code von Outter.java:

public class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    protected class Inner {
        public Inner() {
             
        }
    }
}

Nach der Kompilierung erscheinen zwei Bytecode-Dateien:

Das Dekompilieren der Datei Outter$Inner.class liefert die folgende Information:

E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
  SourceFile: "Outter.java"
  InnerClass:
   #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  com/cxh/test2/Outter$Inner
const #2 = Asciz        com/cxh/test2/Outter$Inner;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        this$0;
const #6 = Asciz        Lcom/cxh/test2/Outter;;
const #7 = Asciz        <init>;
const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz        Code;
const #10 = Field       #1.#11; //  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;
const #12 = Method      #3.#13; //  java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;//  "<init>":()V
const #14 = Asciz       ()V;
const #15 = Asciz       LineNumberTable;
const #16 = Asciz       LocalVariableTable;
const #17 = Asciz       this;
const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz       SourceFile;
const #20 = Asciz       Outter.java;
const #21 = Asciz       InnerClasses;
const #22 = class       #23;    //  com/cxh/test2/Outter
const #23 = Asciz       com/cxh/test2/Outter;
const #24 = Asciz       Inner;
 
{
final com.cxh.test2.Outter this$0;
 
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;
   5:   aload_0
   6:   invokespecial   #12; //Method java/lang/Object."<init>":()V
   9:   return
  LineNumberTable:
   line 16: 0
   line 18: 9
 
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/cxh/test2/Outter$Inner;
 
 
}

Die Zeilen 11 bis 35 sind der Inhalt des Konstantenpools. Erklären wir den Inhalt in Zeile 38 Punkt für Punkt:

final com.cxh.test2.Outter this$0;

Diese Zeile ist ein Zeiger auf das Objekt der äußeren Klasse. Wenn man das sieht, muss es jedem plötzlich klar werden. Das heißt, der Compiler fügt standardmäßig eine Referenz, die auf das Objekt der äußeren Klasse zeigt, für die Member-Inner-Class hinzu. Wie wird diese Referenz dann initialisiert? Als Nächstes betrachten wir den Konstruktor der inneren Klasse:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

Daran ist zu erkennen, dass der Compiler, obwohl der Konstruktor der von uns definierten inneren Klasse ein Konstruktor ohne Argumente ist, standardmäßig dennoch einen Parameter hinzufügt, dessen Typ eine Referenz auf das Objekt der äußeren Klasse ist. Daher zeigt der Zeiger Outter this&0 in der inneren Member-Klasse auf das Objekt der äußeren Klasse, wodurch auf die Mitglieder der äußeren Klasse in der inneren Member-Klasse frei zugegriffen werden kann. Das zeigt auch indirekt, dass eine innere Member-Klasse von der äußeren Klasse abhängt. Wenn kein Objekt der äußeren Klasse erzeugt wird, kann die Referenz Outter this&0 nicht initialisiert und zugewiesen werden, daher kann kein Objekt einer inneren Member-Klasse erzeugt werden.

2. Warum können lokale innere Klassen und anonyme innere Klassen nur auf lokale final-Variablen zugreifen?

Diese Frage hat wahrscheinlich viele Leute beschäftigt. Bevor wir sie diskutieren, sehen wir uns den folgenden Code an:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}
 

Dieser Code wird in zwei Klassendateien kompiliert: Test.class und Test1.class.Standardmäßig benennt der Compiler anonyme innere Klassen und lokale innere Klassen alsOutterx.class(x ist eine positive ganze Zahl)。

Gemäß der obigen Abbildung heißt die anonyme innere Klasse in der test-Methode Test$1.

Im obigen Code: Wenn das final vor einer der Variablen a oder b entfernt wird, lässt sich dieser Code nicht kompilieren. Betrachten wir zunächst folgendes Problem:

Wenn die test-Methode ihre Ausführung beendet, endet der Lebenszyklus der Variablen a. Zu diesem Zeitpunkt ist der Lebenszyklus des Thread-Objekts möglicherweise noch nicht beendet. Daher wird es unmöglich, in der run-Methode von Thread weiterhin auf die Variable a zuzugreifen. Aber dieser Effekt muss trotzdem erzielt werden. Was kann man tun? Java verwendet einen Kopieransatz, um dieses Problem zu lösen. Das Dekompilieren des Bytecodes dieses Codes ergibt Folgendes:

Wir können sehen, dass es in der run-Methode eine Anweisung gibt:

bipush 10

Diese Anweisung legt den Operanden 10 auf den Stack, was anzeigt, dass eine lokale Variable verwendet wird. Dieser Vorgang wird vom Compiler standardmäßig während der Kompilierung durchgeführt. Wenn der Wert der Variablen zur Kompilierungszeit bestimmt werden kann, fügt der Compiler standardmäßig ein Literal mit gleichem Inhalt zum Konstantenpool der anonymen inneren Klasse (lokale innere Klasse) hinzu oder bettet den entsprechenden Bytecode direkt in den ausgeführten Bytecode ein. Auf diese Weise ist die von der anonymen inneren Klasse verwendete Variable eine andere lokale Variable, nur ihr Wert ist gleich dem der lokalen Variablen in der Methode, sodass sie vollständig unabhängig von der lokalen Variablen in der Methode ist.

Betrachten Sie nun ein weiteres Beispiel:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

Die Dekompilierung ergibt:

Wir können sehen, dass der Konstruktor der anonymen inneren Klasse Test$1 zwei Parameter hat: einen Verweis auf das Objekt der äußeren Klasse und eine int-Variable. Offensichtlich wird der Parameter a in der test-Methode als Argument übergeben, um die Kopie der Variablen a in der anonymen inneren Klasse zu initialisieren.

Das heißt, wenn der Wert der lokalen Variablen zur Kompilierungszeit bestimmt werden kann, wird direkt eine Kopie in der anonymen inneren Klasse erstellt. Wenn der Wert der lokalen Variablen zur Kompilierungszeit nicht bestimmt werden kann, wird die Kopie initialisiert, indem Parameter über den Konstruktor übergeben werden.

Aus dem Obigen geht hervor, dass die in der run-Methode zugegriffene Variable a überhaupt nicht die lokale Variable a in der test-Methode ist. Dies löst das zuvor erwähnte Problem der inkonsistenten Lebenszyklen. Aber ein neues Problem entsteht: Da die in der run-Methode zugegriffene Variable a und die Variable a in der test-Methode nicht dieselbe Variable sind, was würde passieren, wenn der Wert der Variablen a in der run-Methode geändert wird?

Ja, es würde zu Dateninkonsistenzen führen, was nicht die ursprüngliche Absicht und die Anforderungen erfüllen würde. Um dieses Problem zu lösen, beschränkt der Java-Compiler die Variable a darauf, eine final-Variable zu sein, und erlaubt nicht, dass die Variable a geändert wird. Bei Variablen vom Referenztyp ist es nicht erlaubt, auf ein neues Objekt zu verweisen. Dadurch wird das Problem der Dateninkonsistenz gelöst.

An diesem Punkt sollte jedem klar sein, warum lokale Variablen und Parameter in einer Methode mit final eingeschränkt werden müssen.

3. Gibt es etwas Besonderes an statischen inneren Klassen?

Aus dem Obigen können wir entnehmen, dass statische innere Klassen nicht von der äußeren Klasse abhängen, was bedeutet, dass Sie ein Objekt der inneren Klasse erstellen können, ohne ein Objekt der äußeren Klasse zu erstellen. Außerdem halten statische innere Klassen keine Referenz auf das Objekt der äußeren Klasse. Leser können versuchen, die Klassendatei selbst zu dekompilieren, um zu sehen, dass es keineOutter this&0Referenz gibt.


III. Anwendungsfälle und Vorteile innerer Klassen

Warum werden innere Klassen in Java benötigt? Zusammengefasst gibt es hauptsächlich die folgenden vier Punkte:

  • 1. Jede innere Klasse kann unabhängig eine Implementierung eines Interfaces erben, sodass es keine Auswirkung auf die innere Klasse hat, ob die äußere Klasse bereits eine bestimmte (Interface-)Implementierung geerbt hat. Innere Klassen machen die Lösung für Mehrfachvererbung vollständig.
  • 2. Es ist praktisch, Klassen mit bestimmten logischen Beziehungen zusammenzufassen und sie auch nach außen zu verbergen.
  • 3. Es ist praktisch, ereignisgesteuerte Programme zu schreiben.
  • 4. Es ist praktisch, Thread-Code zu schreiben.

Ich persönlich denke, dass der erste Punkt einer der wichtigsten Gründe ist. Die Existenz innerer Klassen macht Javas Mechanismus der Mehrfachvererbung vollständiger.

IV. Häufige schriftliche Test- und Interviewfragen zu inneren Klassen

1. Füllen Sie den Code an (1), (2), (3) gemäß den Kommentaren aus

public class Test{
    public static void main(String[] args){
           // 初始化Bean1
           (1)
           bean1.I++;
           // 初始化Bean2
           (2)
           bean2.J++;
           //初始化Bean3
           (3)
           bean3.k++;
    }
    class Bean1{
           public int I = 0;
    }
 
    static class Bean2{
           public int J = 0;
    }
}
 
class Bean{
    class Bean3{
           public int k = 0;
    }
}

Aus dem Obigen folgt: Bei Member-Innerklassen müssen Sie zuerst eine Instanz der äußeren Klasse erstellen, bevor Sie eine Instanz der inneren Klasse erstellen können. Bei statischen inneren Klassen können Sie jedoch eine Instanz der inneren Klasse erstellen, ohne eine Instanz der äußeren Klasse zu erstellen.

Die allgemeine Form zum Erstellen eines Objekts einer statischen inneren Klasse ist:OuterClass.InnerClass xxx = new OuterClass.InnerClass()

Die allgemeine Form zum Erstellen eines Objekts einer Member-Innerklasse ist:OuterClass.InnerClass xxx = outerObjectName.new InnerClass()

Daher lautet der Code an (1), (2), (3) jeweils:

Test test = new Test();    
Test.Bean1 bean1 = test.new Bean1();
Test.Bean2 b2 = new Test.Bean2();
Bean bean = new Bean();     

Bean.Bean3 bean3 =  bean.new Bean3();

2. Was ist die Ausgabe des folgenden Codes?

public class Test {
    public static void main(String[] args)  {
        Outter outter = new Outter();
        outter.new Inner().print();
    }
}
 
 
class Outter
{
    private int a = 1;
    class Inner {
        private int a = 2;
        public void print() {
            int a = 3;
            System.out.println("局部变量:" + a);
            System.out.println("内部类变量:" + this.a);
            System.out.println("外部类变量:" + Outter.this.a);
        }
    }
}
3
2
1

Zum Schluss noch ein Wissenspunkt: bezüglich der Vererbung von Member-Innerklassen. Im Allgemeinen werden innere Klassen selten zur Vererbung verwendet. Wenn sie jedoch zur Vererbung verwendet werden, sind zwei Punkte zu beachten:

  • 1) Die Referenz auf eine Member-Innerklasse muss die Form Outter.Inner haben
  • 2) Der Konstruktor muss eine Referenz auf das Objekt der äußeren Klasse haben und super() über diese Referenz aufrufen. Dieser Code ist aus "Thinking in Java" entnommen
class WithInner {
    class Inner{
         
    }
}
class InheritInner extends WithInner.Inner {
      
    // InheritInner() 是不能通过编译的,一定要加上形参
    InheritInner(WithInner wi) {
        wi.super(); //必须有这句调用
    }
  
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner obj = new InheritInner(wi);
    }
}

Originalquelle: https://www.cnblogs.com/dolphin0520/p/3811445.html