浅析 Java 内部类

时间:2021-12-04 19:53:19

内部类是 Java 中庞大而又复杂的一块知识领域,它有很多特性我们平时可能都没有使用到,而笔者只能尽我所能分享某些方面,如有不正,请谅解并留言指正。笔者将围绕以下几点由浅入深展开讨论:
- 内部类的定义及分类
- 从实例中得出各种内部类的特性
- 为什么要使用内部类

内部类的定义及分类

  1. 内部类的定义
    顾名思义,在一个类的内部定义的类,就可以称为内部类,包含其的类,自然就称为外部类(外围类)。
  2. 内部类的分类
    • 在外部类的成员位置定义的内部类:成员内部类(私有内部类)、静态内部类(嵌套内部类)
    • 在外部类的方法里面定义的内部类:局部内部类(方法内部类)、匿名内部类

成员内部类

我们先来看一段代码

package com.george.j2se.ic;

/*** * @author george * @email qtozeng@qq.com * @date 2016-4-4 */
class Outter {

    private Integer f1 = 1;

    private static String f2 = "static field";

    class MemInner {
        private Integer f3;

        public MemInner(Integer f3) {
            this.f3 = f3;
        }

        private void foo3() {
            //【1】
            System.out.println("foo3(): MemInner Class private method");
        }

        public void foo4() {
            //【3】
            System.out.println("foo4(): new Outter().f1: " + new Outter().f1);
            System.out.println("foo4(): Outter.f1: " + f1);
            //【4】
            System.out.println("foo4(): Ouuter.f2: " + Outter.f2);
        }

        public void foo5() {
            //【5】
            System.out.println("foo5(): MemInner Class public method");
        }

        //【2】错误,成员内部类中不能包含有静态属性(方法&变量)
        /*public static void foo5() { System.out.println("MemInner Class static method"); }*/
    }

    public void foo1() {
        //【6】
        new MemInner(f1).foo3();
        //foo3(); //错误,必须先创建内部类的实例才能访问
    }
}

public class MemInnerTest{
    public static void main(String[] args) {
        Outter outter = new Outter();
        outter.foo1();
        Outter.MemInner mi = outter.new MemInner(1);
        //mi.foo3(); //错误,不能访问内部类的私有成员函数
        mi.foo5();
        mi.foo4();
        Outter.MemInner mi2 = new Outter().new MemInner(2);
    }
}

执行输出:

foo3(): MemInner Class private method foo5(): MemInner Class public method foo4(): new Outter().f1: 1
foo4(): Outter.f1: 1
foo4(): Ouuter.f2: static field
  • 【1】当其它类创建了成员内部类的实例时,其不能够访问内部类的私有成份,而外部类在创建了内部类实例的情况下能够访问内部类的实例成份,包括私有权限范围的。
  • 【2】成员内部类不能够含有静态成份,同学可以从类的加载顺序来给出理由。
  • 【3】成员内部类可以创建外部类的实例来访问外部类的私有非静态成份,或者直接访问外部类的私有非静态成份,即外部类对内部类可见(原因后面会剖析)。
  • 【4】成员内部类可以说不创建外部类的实例来访问外部类的私有静态成份,或者创建外部类的实例以访问其私有静态成份。
  • 【5】其它类创建成员内部类的实例后,可以访问其私有成份。创建的方式为:
① Outter.MemInner mi = new Outter().new MemInner();
② Outter o = new Outter(); MemInner mi = o.new MemInner();
  • 【6】外部类可以在创建成员内部类情况下,无条件访问内部类的任何成份,即内部类对外部类可见。
  • 【0】要牢记一点,成员内部类与外部类没有继承关系。

那么问题来了

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

这可能需要询问编译器了,我们发现当我们执行 javac MemInner.java 时,得到了三个字节码文件 Outter.class、Outter$MemInner.class、MemInnerTest.class。我们再利用 javap -verbose Outter$MemInner命令来查看一下内部类的字节码指令,得到如下字节码指令。

浅析 Java 内部类

⒈ 我们可以知道,编译器把内部类 MemInner.java 仍然被编译成一个独立的类,而不是作为外部类 Outter.java 编译结果的一部分。那么它们之间的联系呢?总不可能毫无干系吧!

⒉ 我们可以清楚的看到,在 Outter$MemInner.class 文件中,其构造方法中把外部类的引用作为隐含参数传递给了内部类的一个隐含的成员变量,并且构造方法中对其进行了赋值。这样就容易解释为什么内部类可以在不创建外部类实例情况下而能访问外部类的实例成份。但这也不能解释内部类能访问外部类的私有成份呐?

⒊ 我们按图索骥,来仔细查看一下 MemInner.foo4() 的字节码:

浅析 Java 内部类

我们仔细观察MemInner.foo4()字节码文件中用红色框住的部分字节码指令。其分别对应源文件中的 new Outter().f1System.out.println("foo4(): Outter.f1: " + f1);我们可以看到,在创建了外部类实例时,其使用 invokestatic 指令调用的是外部类的 Outter.access$000这样一个方法,同时把从内部类构造方法中接收来看外部类的引用作为参数传递过去了;另外,在直接访问外部类的 f1 成员时,使用 invokestatic指令调用外部类的 Outter.access$000,同样也把外部类的引用作为参数传递过去了。那 Outter.access$000 又是个什么东西呢?为什么通过它就可以直接访问外部类的私有成员呢?我们接着又来查看 Outter.class 中的内容。

浅析 Java 内部类

  1. 我们可以看到 access$000 它确实是有接收一个类实例引用,这样通过 getfield其就可以得到类的私有字段了。

  2. 同样,下面的 access$100 它与 access$000 所不同的是,它没有接收这样一个引用,但也能够通过getstatic` 访问到类中的静态字段。这点我们也不难理解了。大家还可以结合 Outter$MemInner.class 中访问外部类的静态字段来串接其中的原理过程。

浅析 Java 内部类

现在我们就能理解为什么内部类在不创建外部类实例情况下能够访问外部类的任何公有字段,并且也能访问外部类的私有字段。

2. 为什么外部类在创建了内部类的情况下,内部类对其是可见的?

有了刚刚探究的经验,我们可以继续沿用上面的方法,即按图索骥。

[1] 我们首先查看外部类中访问内部类的私有字段的代码编译而字节码片段。

浅析 Java 内部类

我们看到红色框住的字节码指令,它也是通过调用内部类字节码的 Outter$MemInner.access$200,同时也将手动实例化内部类的引用作为参数进行传递。

[2] 那我们接着查看 MemInner.class 的字节码内容。

浅析 Java 内部类

我们看到同 Outter.class 一样,其也有这样一段指令。它也接受一个自身实例的引用作为参数。最终通过 invokespecial 指令调用 foo3() 方法。这就能够解释外部类能够在创建实例的情况下调用内部类的任何权限的成份。另外,这也让我们理解了为什么外部类只有在创建内部类后才能访问内部类的任何字段,那是因为与内部类编译而成的字节码不同,外部类编译而成的字节码中不含有内部类的引用。

⒊ 最后给大家总结一下,以上的结论。

与笔者讨论到这里,是不是怀有一种如释重负而又轻松愉悦心情,那是探究知识的成果哦。废话少说,总结如下:

  1. 编译器在编译内部类时,会把外部类的引用作为参数隐式的传入到内部类的构造方法中。

  2. 无论是外部类还是内部类,其都提供访问私有变量或者方法的静态方法,不同的是,外部类需要显式的创建内部类实例,才能够调用这个静态方法。

⒋ 一个小小的思考 :) :) :)

我们刚刚讨论的内部类访问修饰符为默认,如果将其改为 private。那外部类还能创建内部类的实例吗?如果可以,那又是为什么呢?(同学不妨也沿用这种探究的方法)。

静态内部类

我们直接上代码,从代码中找出其特性

package com.george.j2se.ic.si;

import com.george.j2se.ic.si.Outter.StaticInner;

/*** * @author george * @email qtozeng@qq.com * @date 2016-4-4 */
class Outter{

    private Integer f1 = 1;

    private static String f2 = "outter static field f2";

    static class StaticInner{
        private Integer f3 = 2;

        private static String f4 = "staticInner static field f4";

        public void foo3(){
            //【1】 错误,不能访问外部类的非静态字段
            //System.out.println("outter field f1: " + f1);
            //foo1();

            //【1】
            System.out.println("outter static field f2: " + f2);
            foo2();
        }

        public static void foo4(){
            //【1】 错误,不能访问外部类的非静态字段
            //System.out.println("outter field f1: " + f1);
            System.out.println("outter field f1: " + new Outter().f1);
            //foo1();

            //【1】
            System.out.println("outter static field f2: " + f2);
            foo2();
        }

        public void foo(){
            System.out.println("static method foo");
        }
    }

    public void foo1(){
        //【2】
        System.out.println(StaticInner.f4);
        new StaticInner().foo3();
         StaticInner.foo4();
    }

    public static void foo2(){
        //【2】
        new StaticInner().foo();
    }
}

public class StaticInnerTest {
    public static void main(String[] args) {
        Outter o = new Outter();
        //【3】
        Outter.StaticInner.foo4();
        StaticInner.foo4();
        new StaticInner().foo3();
    }
}

运行输出:

outter static field f2: outter static field f2
static method foo outter static field f2: outter static field f2
static method foo outter field f1: 1
outter static field f2: outter static field f2
static method foo 
⒈ 实例中需要注意的地方

【1】静态内部类与成员内部类基本相似,不同的是,其方法中不能直接访问外部类的非静态字段(这点可以从类加载顺序可以得出)包括内部类的实例方法。只能访问其静态方法。但要注意的是,内部类可以通过创建外部类实例来访问外部类的实例成份。
【2】外部类可以在创建内部类实例情况下访问其实例方法,也可以直接用类名+方法名的方式来访问静态方法。
【3】其它类访问内部类的方式与外部类基本一致。

⒉ 再看字节码

浅析 Java 内部类

⒈ 我们可以看到,其与成员内部类不同的是,编译器没有在构造方法中传递外部类实例的引用。也正是因为这个原因,静态内部类才不能直接访问外部类的实例成份。

浅析 Java 内部类

⒉ 同样,静态内部类之所以能够访问到外部类的私有成份,是因为在外部类提供了访问其私有成份的静态的方法。这与我们之前讨论的成员内部类访问外部类的私有成份的原因是相似的。

浅析 Java 内部类

⒊ 与之前一样,外部类也可以通过静态内部类字节码中的一个特殊的静态方法来访问静态内部类中的私有成份。

局部内部类

先上代码

package com.george.j2se.ic.li;

class Outter{

    private Integer f1 = 1;

    private static String f2 = "outter static field f2";

    public void foo(){

        final String temp = "temp filed in foo()";

        //【1】可以用 abstract、final 来修饰局部内部类
        class LocalInner{
            private Integer f3 = 2;

            //【2】
            //private static String f4 = "localInner static field f4";

            public  void foo3(){
                //【3】
                System.out.println(new Outter().f1);
                System.out.println(f2);
                foo2();
                //【4】
                System.out.println(temp);
            }
        }
    }

    public void foo2(){
        System.out.println("outter method foo2() ");
    }
}

因为局部内部类对其它类来说,是不可见的,所以这里就不需要有运行输出了。我们来得出如下几点:

【1】局部内部类不能有访问修饰符(因为它无论什么访问权限,对外均不可见),但可以由 abstract、final 来修饰。
【2】局部内部类中不能含有静态成份。
【3】局部内部类中可以无条件访问外部类成份。
【4】局部内部类如果要访问其所在的方法中的局部变量,则此局部变量要加上 final 修饰符。

再看编译字节码

局部内部类为什么能无条件访问外部类成份

结合 Outter.class、Outter$LocalInner.class 文件,我们发现外部类同样在编译的时候加入了访问其私有成份的隐式静态方法,而在局部内部类的构造方法中,也传入了外部类实例的引用作为参数。这就能够解释了。

我这里就把 Outter$LocalInner.class的构造方法中的部分字节码截图
浅析 Java 内部类

局部类访问局部变量为什么加上 final 修饰符

我们可以这样来思考,首先当JVM解释到字节码时需要创建LocalInner.java实例之后,Outter.java实例可能会已经全部运行完毕,并且其在堆中占用的空间就会被回收,且垃圾回收机制很有可能释放掉局部变量 temp。那么LocalInner.java实例还要用temp变量怎么办呢?这时编译器就想出一个办法,为了保证局部内部类对局部变量 temp 的可见性,其给了局部内部类传递了一个 temp 备份,这样即使局部变量 temp 被回收掉了,这个备份就能保证对局部内部类 LocalInner.java的仍然可见。这似乎很完美。:) :) :)
但问题又来了,局部变量可是在不断变化的呀,编译器它不会让备份的 temp 变量也时刻变化,所以它选择给局部变量加上 final 关键字,这样就能保证局部变量与备份变量的一致性了。:) :) :)
同学可以看 LocalInner.foo3() 的字节码文件

浅析 Java 内部类

这条指令表示将字符串从常量池中压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器会在局部内部类的常量池中添加一个内容相等的字面量或直接将其字节码嵌入到执行的字节码中去。这样一来就验证了之前的说法。

匿名内部类

先看一段代码

package com.george.j2se.ic.ai;

interface A{
    void foo1();
}

abstract class B{
    private Integer b;

    public B(Integer b){
        this.b = b;
    }
    void foo2(){
        //..
    }
}
class Outter{

    public void foo(final int count){

        //【1】
        new Thread(){
            @Override
            public void run() {
                for(int i = 0; i < count; i++){
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }

        }.run();

        //【2】
        new A(){

            @Override
            public void foo1() {
                System.out.println("the interface A implemetation foo1() ");
            }

        }.foo1();


        //【3】
        new B(100){

            @Override
            void foo2() {
                System.out.println("the class B override foo2() ");
            }
        }.foo2();
    }

}

所谓匿名内部类,其本质就是继承了该类或者实现了该接口的子类匿名的对象,从以上代码可以得出如下几点:

【0】匿名内部类因为没有名称,所以不可能有构造方法。另外,这也使得其被使用范围有限,匿名内部类基本上用作接口回调,且一般会实现、覆盖方法,而不会新增方法。
【1】同局部内部类一样,匿名内部类要使用局部变量也需要将其用 final 修饰。
【3】在实例化匿名内部类时,也可以给构造器传递参数。

内部类使用注意事项

内部类中 this 与 new 异同

package com.george.j2se.ic;

class Outer {
    public Integer num = 10;

    class Inner {
        public Integer num = 20;

        public void show() {
            Integer num = 30;
            System.out.println(num);
            System.out.println(this.num);
            // System.out.println(new Outer().num);
            System.out.println(Outer.this.num);
        }
    }
}

public class InnerClassTest {
    public static void main(String[] args) {
        Outer.Inner oi = new Outer().new Inner();
        oi.show();
    }
}

执行输出:

30
20
10

从上面代码中,可以看出,如何精确的访问到各种作用域的变量。再附上一段网友提供的代码,其充分说明了,在内部类中使用 this 得到时创建该内部类时使用的外部类对象的引用,而new则是创建了一个新的外部类的对象引用。

package com.george.j2se.ic;

public class InnerClassTest2 {
    private int num;

    public InnerClassTest2() {

    }

    public InnerClassTest2(int num) {
        this.num = num;
    }

    private class Inner {
        public InnerClassTest2 getInnerClassTest2() {
            return InnerClassTest2.this;
        }

        public InnerClassTest2 newInnerClassTest2() {
            return new InnerClassTest2();
        }
    }

    public static void main(String[] args) {
        InnerClassTest2 test = new InnerClassTest2(5);
        InnerClassTest2.Inner inner = test.new Inner();
        InnerClassTest2 t2 = inner.getInnerClassTest2();
        InnerClassTest2 t3 = inner.newInnerClassTest2();
        System.out.println(t2.num);
        System.out.println(t3.num);
    }
}

执行输出:

5
0

内部类的创建问题

package com.george.j2se.ic;

public class InnerClassTest3{
    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;
    }
}
                                                -- 以上代码来自网上,在 【相关博客】 中已给出链接(第一条)

所填写代码段分别如下:

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

只要清楚,若要创建内部类,一般要先创建外部类的实例。而静态内部类可不创建外部类实例。另外如果下述填写代码所在的方法为外部类的实例方法,则可不必先创建外部类实例。

内部类的继承问题

package com.george.j2se.ic;

class WithInner {
    class Inner{
         public void foo(){
             System.out.println("inner method foo...");
         }
    }
}
class InheritInner extends WithInner.Inner {

    // InheritInner(); //【1】是不能通过编译的,一定要加上形参
    InheritInner(WithInner wi) {
        wi.super(); //【2】必须有这句调用
    }

    @Override
    public void foo() {
        System.out.println("inheritInner method foo...");
    }

    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner obj = new InheritInner(wi);
        obj.foo();
    }
}
                                                                    -- 以上代码出处《Thinking In Java》

【1】可以看出成员内部类的引用方式必须为 Outter.Inner.
【2】构造器一定要将外部类作为形参传入
【3】构造器中必须要用outter.super()代码段。

为什么要使用内部类

对于很多技术,我们可能都是先实践,从实践过程中参悟事情的本质,而对于计算机类的学科来说更是如此,不是先知道为什么才去动手实践,而是先实践才知道为什么。至于原因,个人有两点,其一计算机是实践性的学科,很多结论都是从实践中来;其二,实践让人更有心得,更有利于去理解事情的本质。

⒈ 每个内部类都能独立地继承一个接口,而无论外部类是否已经继承了某个接口,这似乎实现了类的多继承;

⒉ 匿名内部类在开发中主要就是要考虑要继承或者需要被实现的类使用频率的问题,如果只需要偶尔使用,就应该使用匿名内部类来完成,这很方便,否则就应该单 独写一个子类或者实现类;

⒊ 使用内部类可以非常方便的编写事件驱动程序(与2类似);

⒋ 使用内部类可以方便的访问创建它的类的任何成份,即内部类提供了某种进入外部类的窗户(某网友所提到的一点)。

参考书籍: 《Thinking In Java》

最后,给出相关优秀博客链接:
1. Java内部类详解
2. 内部类详解
3. 内部类详解(很详细)
4. Java为什么要使用内部类?