内部クラスという言葉といえば、聞いたことはあっても聞き慣れない人も多いのではないでしょうか。その理由は、コードを書く際に利用できるシナリオがそれほど多くなく、最も一般的なユースケースはイベント監視がある場合であり、使用されていても内部クラスの使用法がまとめられることはほとんどないからです。今日調べてみましょう。
1. 内部クラスの基本
Java では、このようにクラスを別のクラスまたはメソッド内で定義できます。内部クラスと呼ばれます。広義の内部クラスには、一般的にメンバー内部クラス、ローカル内部クラス、匿名内部クラス、静的内部クラスの 4 種類が含まれます。まず、これら 4 つの内部クラスの使用法を理解しましょう。
1. メンバーの内部クラス
メンバーの内部クラスは最も一般的な内部クラスであり、次の形式で別のクラス内に配置されるように定義されます。
class Circle { double radius = 0; public Circle(double radius) { this.radius = radius; } class Draw { //内部类 public void drawSahpe() { System.out.println("drawshape"); } } }DrawクラスはCircleクラスのメンバーのようなもので、Circleは外部クラスと呼ばれているようです。メンバーの内部クラスは、外部クラスのすべてのメンバー プロパティとメンバー メソッド (プライベート メンバーと静的メンバーを含む) に無条件でアクセスできます。
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); //外部类的静态成员 } } }ただし、メンバー内部クラスに外部クラスと同じ名前のメンバー変数またはメソッドがある場合、デフォルトではメンバー内部クラスのメンバーが隠蔽されることに注意してください。アクセスされます。同じ名前の外部クラスのメンバーにアクセスする場合は、次の形式でアクセスする必要があります:
外部クラス.this.メンバー変数外部クラス.this.Member メソッドメンバー内部クラスは外部クラスのメンバーに無条件にアクセスできますが、外部クラスはメンバー内部クラスのメンバーにそれほど自由にアクセスできません。外部クラス内のメンバー内部クラスのメンバーにアクセスする場合は、まずメンバー内部クラスのオブジェクトを作成し、次にこのオブジェクトを指す参照を通じてそのオブジェクトにアクセスする必要があります。
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成员 } } }メンバー内部クラスclass は外部クラスに依存して存在します。つまり、メンバー内部クラスのオブジェクトを作成する場合は、外部クラスのオブジェクトが存在することが前提となります。メンバー内部クラス オブジェクトを作成する一般的な方法は次のとおりです。
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() { } } }内部クラスには、プライベート アクセス権、保護されたアクセス権、パブリック アクセス権、およびパッケージ アクセス権を設定できます。たとえば、上記の例では、メンバーの内部クラス Inner が private で変更されている場合は、外部クラス内でのみアクセスできますが、public で変更されている場合はどこからでもアクセスできます。 protected で変更された場合、アクセスのみ可能 同じパッケージ内または外部クラスの継承によりアクセス可能; デフォルトのアクセス権限の場合、同じパッケージ内のみアクセス可能これは外部クラスとは少し異なり、外部クラスはパブリック アクセス許可とパッケージ アクセス許可でのみ変更できます。
2. ローカル内部クラス
ローカル内部クラスは、メソッドまたはスコープで定義されたクラスです。メンバー内部クラスとの違いは、ローカル内部クラス アクセスはメソッドまたはスコープに限定されます。
class People{ public People() { } } class Man{ public Man(){ } public People getWoman(){ class Woman extends People{ //局部内部类 int age =0; } return new Woman(); } }ローカル内部クラスはメソッド内のローカル変数に似ており、public、protected、private、および static 修飾子を持つことができないことに注意してください。
3. 匿名内部クラス
コードを書くときに匿名内部クラスが最も一般的に使用されるべきです。イベント監視コードを書くときに匿名内部クラスを使用すると、便利なだけではありません。コードの保守が容易になります。次のコードは、Android イベント リスニング コードです。
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 } });このコードは、匿名の内部クラスを使用して、2 つのボタンのリスナーを設定します。このコードでは:
new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub } }は匿名の内部クラスを使用しています。コードでは、ボタンにリスナー オブジェクトを設定する必要があります。匿名内部クラスを使用すると、親クラスまたはインターフェイスでメソッドを実装するときに対応するオブジェクトを生成できますが、その前提として、親クラスまたはインターフェイスが最初に存在する必要があります。このように使用できます。もちろん、次のように記述することも可能で、上記の匿名内部クラスを使用するのと同じ効果があります。
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 } }この記述方法でも同じ効果が得られますが、時間がかかり、保守が難しいため、イベント監視コードの記述には匿名の内部クラス メソッドが一般的に使用されます。同様に、匿名内部クラスはアクセス修飾子と静的修飾子を持つことができません。 匿名内部クラスは、コンストラクターを持たない唯一のクラスです。コンストラクターがないため、匿名内部クラスの使用範囲は非常に限られており、ほとんどの匿名内部クラスはインターフェイスのコールバックに使用されます。匿名内部クラスには、コンパイル中にシステムによって自動的に Oututter$1.class という名前が付けられます。 一般に、匿名内部クラスは、他のクラスを継承したり、インターフェイスを実装したりするために使用されます。追加のメソッドを追加する必要はなく、継承されたメソッドを実装または書き換えるだけです。
4. 静的内部クラス
静的内部クラスも別のクラスで定義されたクラスですが、クラスの前に追加のキーワード static があります。静的内部クラスは、外部クラスに依存する必要はありません。これは、クラスの静的メンバー プロパティに似ていますが、外部クラスの非静的メンバー変数やメソッドは使用できません。これは理解しやすいためです。外部クラスのオブジェクトはありません。この場合、静的な内部クラスのオブジェクトを作成できます。外部クラスの非静的メンバーへのアクセスを許可すると、非静的メンバーが存在するため、矛盾が生じます。外部クラスの を特定のオブジェクトにアタッチする必要があります。
public class Test { public static void main(String[] args) { Outter.Inner inner = new Outter.Inner(); } } class Outter { public Outter() { } static class Inner { public Inner() { } } }
二.深入理解内部类
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() { } } }
编译之后,出现了两个字节码文件:
反编译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为正整数)。
根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。
上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:
当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:
我们看到在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(); } }
反编译得到:
我们看到匿名内部类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基础教程栏目。
以上がJava 内部クラスの詳細な説明 (関連するインタビューの質問付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。