Heim  >  Artikel  >  Java  >  Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

尚
nach vorne
2019-12-20 17:29:072121Durchsuche

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

Apropos interne Klasse: Viele Menschen müssen damit vertraut sein, fühlen sich aber möglicherweise nicht damit vertraut. Der Grund dafür ist, dass es nicht viele Szenarien gibt, die beim Schreiben von Code verwendet werden können. Am häufigsten werden Szenarien verwendet, bei denen eine Ereignisüberwachung erfolgt, und selbst wenn sie verwendet werden, wird die Verwendung interner Klassen selten zusammengefasst. Finden wir es heute heraus.

1. Grundlagen interner Klassen

In Java kann eine Klasse in einer anderen Klasse oder in einer Methode definiert werden, so wie diese Klassen sogenannte innere Klassen. Zu den inneren Klassen im weitesten Sinne gehören im Allgemeinen diese vier Typen: innere Mitgliedsklassen, lokale innere Klassen, anonyme innere Klassen und statische innere Klassen. Lassen Sie uns zunächst die Verwendung dieser vier internen Klassen verstehen.

1. Innere Klasse des Mitglieds

Die innere Klasse des Mitglieds ist die häufigste innere Klasse und wird in der folgenden Form definiert:

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

Auf diese Weise ist die Draw-Klasse wie ein Mitglied der Circle-Klasse und Circle wird als externe Klasse bezeichnet. Eine innere Mitgliedsklasse kann bedingungslos auf alle Mitgliedseigenschaften und Mitgliedsmethoden der äußeren Klasse zugreifen (einschließlich privater Mitglieder und statischer Mitglieder).

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);   //外部类的静态成员
        }
    }
}

Es sollte jedoch beachtet werden, dass, wenn eine innere Mitgliedsklasse eine Mitgliedsvariable oder -methode mit demselben Namen wie die äußere Klasse hat, ein Ausblenden erfolgt, d. h. standardmäßig Mitglieder der inneren Mitgliedsklasse zugegriffen wird. Wenn Sie auf ein Mitglied einer externen Klasse mit demselben Namen zugreifen möchten, müssen Sie in der folgenden Form darauf zugreifen:

External class.this.Member variable

External class .this.Member-Methode

Obwohl die innere Mitgliedsklasse bedingungslos auf die Mitglieder der äußeren Klasse zugreifen kann, ist die äußere Klasse nicht so frei, auf die Mitglieder der inneren Mitgliedsklasse zuzugreifen. Wenn Sie auf Mitglieder einer inneren Mitgliedsklasse in einer äußeren Klasse zugreifen möchten, müssen Sie zunächst ein Objekt der inneren Mitgliedsklasse erstellen und dann über eine Referenz darauf zugreifen, die auf dieses Objekt verweist:

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成员
        }
    }
}

Innere Mitgliedsklassen existieren abhängig von der äußeren Klasse, das heißt, wenn Sie ein Objekt einer inneren Mitgliedsklasse erstellen möchten, ist die Voraussetzung, dass ein Objekt der äußeren Klasse existieren muss. Die allgemeine Methode zum Erstellen innerer Klassenobjekte für Mitglieder 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 Zugriffsrechte, geschützte Zugriffsrechte, öffentliche Zugriffsrechte und Paketzugriffsrechte haben. Wenn im obigen Beispiel beispielsweise die innere Klasse Inner mit private geändert wird, kann nur innerhalb der externen Klasse darauf zugegriffen werden. Wenn dies der Fall ist, kann überall darauf zugegriffen werden Wird mit protected geändert, kann nur innerhalb der externen Klasse darauf zugegriffen werden. Wenn es sich um die Standardzugriffsberechtigung handelt, kann darauf nur unter demselben Paket zugegriffen werden. Dies unterscheidet sich ein wenig von externen Klassen. Externe Klassen können nur mit öffentlichen und Paketzugriffsberechtigungen geändert werden.

2. Lokale interne Klassen

Eine lokale interne Klasse ist eine in einer Methode oder einem Bereich definierte Klasse. Der Unterschied zwischen ihr und internen Mitgliedsklassen besteht darin, dass sie lokal intern ist Der Zugriff auf Klassen ist auf die Methode oder den Bereich beschränkt.

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

Beachten Sie, dass eine lokale innere Klasse wie eine lokale Variable in einer Methode ist und keine öffentlichen, geschützten, privaten und statischen Modifikatoren haben kann.

3. Anonyme innere Klassen

Anonyme innere Klassen sollten beim Schreiben von Code für die Ereignisüberwachung am häufigsten verwendet werden und erleichtert die Wartung des Codes. Der folgende Code ist ein Android-Ereignis-Listening-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 legt Listener für zwei Schaltflächen fest, und hier werden anonyme innere Klassen verwendet. In diesem Code:

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

ist die Verwendung anonymer innerer Klassen. Der Code muss ein Listener-Objekt für die Schaltfläche festlegen. Durch die Verwendung einer anonymen inneren Klasse kann ein entsprechendes Objekt generiert werden, während die Methoden in der übergeordneten Klasse oder Schnittstelle implementiert werden. Voraussetzung ist jedoch, dass die übergeordnete Klasse oder Schnittstelle zunächst vorhanden sein muss auf diese Weise verwendet. Natürlich ist es auch möglich, wie folgt zu schreiben, was den gleichen Effekt hat wie die oben beschriebene Verwendung anonymer innerer Klassen.

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 Schreibmethode den gleichen Effekt erzielen kann, ist sie langwierig und schwierig zu warten. Daher werden im Allgemeinen anonyme Methoden innerer Klassen zum Schreiben von Ereignisüberwachungscode verwendet. Ebenso können anonyme innere Klassen keine Zugriffsmodifikatoren und statischen Modifikatoren haben.

Anonyme innere Klassen sind die einzigen Klassen ohne Konstruktor. Da es keinen Konstruktor gibt, ist der Einsatzbereich anonymer innerer Klassen sehr begrenzt. Die meisten anonymen inneren Klassen werden für Schnittstellenrückrufe verwendet. Die anonyme innere Klasse wird vom System während der Kompilierung automatisch Oututter$1.class genannt.

Im Allgemeinen werden anonyme innere Klassen verwendet, um andere Klassen zu erben oder Schnittstellen zu implementieren. Sie müssen keine zusätzlichen Methoden hinzufügen, sondern nur geerbte Methoden implementieren oder überschreiben.

4. Statische innere Klassen

Statische innere Klassen sind auch Klassen, die in einer anderen Klasse definiert sind, aber vor der Klasse steht ein zusätzliches Schlüsselwort static. Eine statische innere Klasse muss nicht von einer äußeren Klasse abhängig sein. Dies ähnelt in gewisser Weise den statischen Mitgliedseigenschaften der Klasse und kann keine nicht statischen Mitgliedsvariablen oder -methoden der äußeren Klasse verwenden Es gibt kein Objekt der äußeren Klasse. In diesem Fall können Sie ein Objekt einer statischen inneren Klasse erstellen. Wenn Sie den Zugriff auf nicht statische Mitglieder der äußeren Klasse zulassen, entsteht ein Widerspruch, da die nicht statischen Mitglieder der äußeren Klasse muss an das spezifische Objekt angehängt werden.

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

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

二.深入理解内部类

1.为什么成员内部类可以无条件访问外部类的成员?

在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是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() {
             
        }
    }
}

编译之后,出现了两个字节码文件:

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

反编译Outter$Inner.class文件得到下面信息:

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;
 
}

第11行到35行是常量池的内容,下面逐一第38行的内容:

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

这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

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

从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。

从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

2.为什么局部内部类和匿名内部类只能访问局部final变量?

想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:

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();
    }
}

这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制  的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

我们看到在run方法中有一条指令:

bipush 10

这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

下面再看一个例子:

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();
    }
}

反编译得到:

Detaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen)

我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。

但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

3.静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

三.内部类的使用场景和好处

为什么在Java中需要内部类?总结一下主要有以下四点:

1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,

2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。

3.方便编写事件驱动程序

4.方便编写线程代码

四.常见的与内部类相关的笔试面试题

 1.根据注释填写(1),(2),(3)处的代码

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;
    }
}

从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。

创建静态内部类对象的一般形式为:  外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()

创建成员内部类对象的一般形式为:  外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()

因此,(1),(2),(3)处的代码分别为:

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.下面这段代码的输出结果是什么?

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

最后补充一点知识:关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

1)成员内部类的引用方式必须为 Outter.Inner.

2)构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。

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);
    }
}

更多java知识请关注java基础教程栏目。

Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der internen Java-Klassen (mit relevanten Interviewfragen). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:cnblogs.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen