Java编程笔记7:内部类

Java编程笔记7:内部类

5c9c3b3b392ac581.jpg

图源:PHP中文网

所谓的内部类,其实就是定义在类中的类。这和Java编程笔记6:接口 - 魔芋红茶’s blog (icexmoon.xyz)中提到的接口嵌套的方式有点相似。不过内部类比接口嵌套更常见,也更有用。

内部类

定义内部类并不困难:

package ch7.inner_class;

import java.util.Random;

import util.Fmt;

class OulterClass {
    protected class InnerClass {
        private int num;

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

        @Override
        public String toString() {
            return Fmt.sprintf("InnerClass(%d)", num);
        }

    }

    public InnerClass getInnerClassInstance() {
        Random random = new Random();
        return new InnerClass(random.nextInt(100));
    }
}

public class Main {
    public static void main(String[] args) {
        OulterClass oc = new OulterClass();
        OulterClass.InnerClass ic = oc.getInnerClassInstance();
        System.out.println(ic);
        // InnerClass(84)
    }
}

需要注意的是,这里获取内部类实例的方式是通过“外部类”OulterClassgetInnerClassInstance方法,而非直接在main函数中通过new OulterClass.InnerClass()创建,是因为后者其实是无法执行的,具体原因后边会解释。

同时可以看到,内部类可以使用protected声明,并不局限于public和包访问权限,事实上你甚至可以将内部类声明为private,并且在某些情况下这样做还很有用,这点在后面会同样举例说明。

在内部类所属的外部类之外,要使用内部类就需要用OlterClass.InnerClass这样的方式,或者也可以用import语句进行导入,这点和嵌套接口的使用方式是一致的。

链接到外部类

之所以说内部类很有用,是因为内部类会“隐含”一个外部类实例的引用,利用这个引用我们可以直接访问外部类的属性和方法。

Java编程笔记6:接口 - 魔芋红茶’s blog (icexmoon.xyz)中【完全解耦】小节中,我举过一个用适配器模式让NumberGenerator实现Printable的例子,当然这很棒,很具扩展性。但是使用设计模式往往会带来另一个后果——类的数量会大大增加。在那个例子中并不明显,但是随着你用适配器模式给NumberGenerator添加一个又一个适配器类,类数量膨胀是可以预期的,而且如果按照Java项目一般性的一个类文件只包含一个类定义的做法,很快你就能看到一个目录下包含了一堆xx.java文件。

实际上在Java中更好的做法是用内部类来实现适配器模式:

package ch7.decouple4;

import java.util.Random;

public class NumberGenerator {
    private static Random random = new Random();

    public Printable getPrinter(int printTimes) {
        return new NGPrintAdapter(printTimes);
    }

    public int getNumber() {
        return random.nextInt(100);
    }

    private class NGPrintAdapter implements Printable {
        private int printTimes;

        public NGPrintAdapter(int printTimes) {
            if (printTimes < 0) {
                throw new Error();
            }
            this.printTimes = printTimes;
        }

        @Override
        public void print() {
            for (int i = 0; i < printTimes; i++) {
                System.out.print(getNumber() + " ");
            }
            System.out.println();
        }

    }
}

在这个改写后的例子中,适配器类NGPrintAdapterNumberGenerator的内部类,这样做的好处在于:

  1. 不需要额外使用单独的代码文件存放适配器类。
  2. 明确了适配器类和适配目标类的从属关系。
  3. 适配器类作为内部类,可以直接调用适配目标对象的属性和方法,在这个示例中,适配器类的print方法中直接调用了外部类NumberGenerator实例的getNumber()方法。
  4. 可以用private修饰适配器类,让适配器类的实现对外部隐藏。客户端代码仅仅需要的是一个实现了Printable的类型,并不关心具体是怎么实现的。

最后看一下测试代码,几乎没有怎么修改:

public class Main {
    public static void main(String[] args) {
        Printable p1 = new NumberSequence(new int[] { 1, 2, 3 });
        Printable p2 = new CharSequence(new char[] { 'a', 'b', 'c' });
        p1.print();
        p2.print();
        NumberGenerator ng = new NumberGenerator();
        Printable p3 = ng.getPrinter(6);
        p3.print();
        // 1 2 3 
        // a b c
        // 40 26 82 31 78 6
    }
}

this和new

某些时候你可能希望在内部类中直接获取外部类实例的引用,可以使用this关键字实现:

package ch7.inner_class2;

import java.util.Random;

import util.Fmt;

class OulterClass {
    protected class InnerClass {
		...
        public OulterClass getOulterClassInstance() {
            return OulterClass.this;
        }

    }
	...
}

public class Main {
    public static void main(String[] args) {
        OulterClass oc = new OulterClass();
        OulterClass.InnerClass ic = oc.getInnerClassInstance();
        System.out.println(ic);
        // InnerClass(84)
        OulterClass oc2 = ic.getOulterClassInstance();
        System.out.println(oc == oc2);
        // true
    }
}

或者内部类的属性名或方法名与外部类冲突时,也可以用this关键字显式调用以作区分:

package ch7.inner_class3;

import java.util.Random;

import util.Fmt;

class OulterClass {
    protected class InnerClass {
        private int num;

        public InnerClass(int num) {
            this.num = num;
            System.out.println("print inner:" + this.toString());
            System.out.println("print outler:" + OulterClass.this.toString());
            // print inner:InnerClass(66)
            // print outler:OulterClass()
        }

        @Override
        public String toString() {
            return Fmt.sprintf("InnerClass(%d)", num);
        }
		...
    }
	...
    @Override
    public String toString() {
        return "OulterClass()";
    }

}
...

前面这些例子都说明了内部类与外部类的紧密关系——内部类实例包含了对外部类实例的引用。这也是为什么一开始说的,无法通过普通的new OulterClass.InnerClass()方式来创建内部类,因为你没有给它指定所需要的外部类实例。

换句话说,只要我们指定一个外部类实例,就可以创建一个持有其引用的对应的内部类:

...
public class Main {
    public static void main(String[] args) {
        OulterClass oc = new OulterClass();
        OulterClass.InnerClass ic = oc.new InnerClass(10);
        System.out.println(ic);
        // InnerClass(10)
    }
}

oc.new InnerClass(10)这样的方式的确看起来很怪异,但它所表达的的确是用一个外部类实例oc创建一个内部类InnerClass的实例。

此外,oc.new之后的InnerClass没有添加外部类名称,事实上你也不能那么做,这是因为oc作为外部类的实例,已经很明确了,不需要额外指定外部类名。

在方法和作用域内的内部类

事实上,除了可以在类中定义类以外,还可以在方法和作用域中定义类,这样的类被称作局部内部类

package ch7.func_inner;

public class Main {
    public static void main(String[] args) {
        class LocalInnerClass{
            @Override
            public String toString() {
                return this.getClass().getName();
            }
        }
        LocalInnerClass lic = new LocalInnerClass();
        System.out.println(lic);
        // ch7.func_inner.Main$1LocalInnerClass
    }
}

上面的示例中,LocalInnerClass的作用域被局限在main函数中,也就是说其它地方的代码都无法使用它。但这并不意味着main函数结束后这个类定义就会被销毁,事实上只是外部代码无法访问而已,如果main函数被再次调用,就可以重新使用这个类定义。

类似的,你可以在任何类型的作用域中定义内部类,比如条件语句:

package ch7.func_inner2;

import java.util.Random;

import util.Fmt;

public class Main {
    public static void main(String[] args) {
        Random random = new Random();
        int a = random.nextInt(100);
        int b = random.nextInt(100);
        if (a > b) {
            class LocalInnerClass {
                @Override
                public String toString() {
                    return this.getClass().getName();
                }
            }
            LocalInnerClass lic = new LocalInnerClass();
            System.out.println(lic);
        }
        Fmt.printf("a:%d,b:%d\n", a, b);
        // ch7.func_inner2.Main$1LocalInnerClass
        // a:35,b:5
    }
}

匿名内部类

在Java中,匿名内部类是最常见和使用的一种内部类。

所谓的匿名类,就是“没有名字的类”,而匿名内部类,就是没有名字的内部类。

继续前边NumberGenerator的例子,如果用匿名内部类改写:

package ch7.nonname;

import java.util.Random;

public class NumberGenerator {
    private static Random random = new Random();

    public Printable getPrinter(int printTimes) {
        if (printTimes <= 0) {
            return null;
        }
        return new Printable() {

            @Override
            public void print() {
                for (int i = 0; i < printTimes; i++) {
                    System.out.print(getNumber() + " ");
                }
                System.out.println();
            }

        };
    }

    public int getNumber() {
        return random.nextInt(100);
    }

}

这里的new Printable(){...}语法就是创建一个实现了Printable的匿名类,并将其实例化。

匿名类往往需要从外部获取信息,比如上面的示例中,匿名类就需要获取一个打印次数(printTimes),该信息由getPrinter方法的printTimes参数保存,比较神奇的是我们不需要任何方式,将其“传递”给匿名类,只需要直接在匿名类中像使用自身属性那样使用即可。

其实如果上边的代码在JavaSE8之前的版本中编译,是无法通过编译的。这是因为Java中的匿名类,其实和PHP、Python、Go等支持函数式编程的语言中的函数一样,都是用闭包来实现的。而闭包所使用的外部数据,实际上是不能改变的(或者直接由外部传入),在Java中,这体现为——匿名类使用的外部数据都必须被定义为final。也就是说在JavaSE8之前,我们上面的例子要写成:

package ch7.nonname2;

import java.util.Random;

public class NumberGenerator {
    private static Random random = new Random();

    public Printable getPrinter(final int printTimes) {
		...
        return new Printable() {
			for (int i = 0; i < printTimes; i++) {
                    ...
            }
            ...
        };
    }
	...
}

否则就无法通过编译。

大概是很多人被这种规定坑?在JavaSE8中添加了一种新特性,叫做effectively final。具体来说就是,如果一个变量在定义后没有发生过改变,那么该变量就是effectively final的

对于这种effectively final变量,Java可以进行优化——在需要的时候给其自动添加上final声明。

现在再来看上边的例子,printTimes作为getPrinter的参数,实际上除了在匿名类中使用之外,没有任何其他使用,更别说修改了,所以必然是effectively final的。所以在因为匿名类的使用而需要我们给其添加final声明的时候,Java编译器可以“自动”帮我们完成。

所以也就不存在JavaSE8之前我们必须手动添加上final声明的那种限制了。

但是,了解这种新特性的原理后很容易就明白这样产生的另一个问题,如果匿名类使用的外部变量被修改过,不是effectively final的,那是不是编译器就没办法帮我们自动完成添加final声明的工作?答案是Yes。

package ch7.nonname3;

import java.util.Random;

public class NumberGenerator {
    private static Random random = new Random();

    public Printable getPrinter(int printTimes) {
        if (printTimes <= 0) {
            return null;
        }
        printTimes = random.nextInt(10);
        return new Printable() {

            @Override
            public void print() {
                // Local variable printTimes defined in an enclosing scope must be final or effectively final
                for (int i = 0; i < printTimes; i++) {
                    System.out.print(getNumber() + " ");
                }
                System.out.println();
            }

        };
    }

    public int getNumber() {
        return random.nextInt(100);
    }

}

上面的代码是无法通过编译的,报错信息很明确——privateTimes变量应当是final或者effectively final类型。

要解决这种问题也很容易,定义一个匿名类专用的final类型就是了:

package ch7.nonname4;

import java.util.Random;

public class NumberGenerator {
    private static Random random = new Random();

    public Printable getPrinter(int printTimes) {
		...
        printTimes = random.nextInt(10);
        final int newTimes = printTimes;
        return new Printable() {

            @Override
            public void print() {
                for (int i = 0; i < newTimes; i++) {
                    System.out.print(getNumber() + " ");
                }
                System.out.println();
            }

        };
    }
	...
}

大多数情况下匿名类都用于实现某个接口,但实际上同样可以继承自某个类:

package ch7.nonname5;

import util.Fmt;

abstract class Tank{
    protected String name;
    public Tank(String name){
        this.name = name;
    }
    abstract public void fire();
    abstract public void move();
}

class TankFactory{
    public static Tank buildTank(String name){
        return new Tank(name) {

            @Override
            public void fire() {
                Fmt.printf("Tank(%s) is firing.\n", this.name);
            }

            @Override
            public void move() {
                Fmt.printf("Tank(%s) is moving.\n", this.name);
            }
            
        };
    }
}

public class Main {
    public static void main(String[] args) {
        Tank tank = TankFactory.buildTank("99");
        tank.move();
        tank.fire();   
        // Tank(99) is moving.
        // Tank(99) is firing.
    }
}

Tank是一个抽象基类,TankFactorybuildTank方法可以直接返回一个继承自Tank的匿名类生成的实例。需要注意的是Tank有一个构造函数,必须接收一个String参数,所以匿名类也需要用new Tank(name){...}的方式构建,其中name就是传递给基类Tank构造函数的参数。

因为匿名类是没有名字的,自然我们就没法在其中定义构造函数,所以也只能通过上面这种方式给基类构造函数传递参数。

虽然基类构造函数传参的问题可以通过上面的方式解决,但是构造函数承担的初始化工作要怎么办?

不知道还你还记不记得Java编程笔记2:初始化和清理 - 魔芋红茶’s blog (icexmoon.xyz)中提到的初始化块,或许初始化块对于普通类来说可有可无,但是对于匿名类来说,其是可以执行复杂初始化语句的唯一方式:

package ch7.nonname6;

import util.Fmt;

...
class TankFactory {
    private static int num = 1;

    public static Tank buildTank(String name) {
        final int newNum = num;
        num++;
        return new Tank(name) {
            private int num;
            {
                this.num = newNum;
                Fmt.printf("Tank(name:%s,num:%d) is build.\n", this.name, this.num);
            }
			...
        };
    }
}

public class Main {
    public static void main(String[] args) {
        Tank t1 = TankFactory.buildTank("99");
        Tank t2 = TankFactory.buildTank("M1A1");
        // Tank(name:99,num:1) is build.
        // Tank(name:M1A1,num:2) is build.
    }
}

虽然匿名类的确很有用,但匿名类有很多限制:

  1. 引用的外部变量必须是final或者effectively final的。
  2. 只能实现一个接口或者继承自一个基类,且不能同时实现。
  3. 不能包含静态属性和方法。

再谈工厂模式

实际我们前面提到的工厂模式很适合使用匿名类来实现:

package ch7.factory3;

public class HeavyTank implements Tank {
    public static Factory factory = new Factory() {

        @Override
        public Tank buildTank() {
            HeavyTank tank = new HeavyTank();
            tank.buildSites();
            tank.buildBarbette();
            tank.buildWeaponSystem();
            tank.ready();
            return tank;
        }
        
    };

	...
}
package ch7.factory3;

public class LightTank implements Tank {
    public static Factory factory = new Factory() {

        @Override
        public Tank buildTank() {
            LightTank tank = new LightTank();
            tank.buildSites();
            tank.buildBarbette();
            tank.buildWeaponSystem();
            tank.ready();
            return tank;
        }

    };
	...
}
package ch7.factory3;

public class Main {
    public static void main(String[] args) {
        Factory factory = HeavyTank.factory;
        Tank tank1 = factory.buildTank();
        factory = LightTank.factory;
        Tank tank2 = factory.buildTank();
        // Heavy Tank sites is build.
        // Heavy Tank barbette is build.
        // Heavy Tank weapon system is build.
        // Heavy Tank build work is all over.
        // Light Tank sites is build.
        // Light Tank barbette is build.
        // Light Tank weapon system is build.
        // Light Tank build work is all over.
    }
}

这里使用匿名类创建的实例作为Tank子类对应的工厂。

为什么需要内部类

虽然前面已经说了使用内部类会带来的一些好处,但似乎没有一个案例是必须使用内部类的,所以必须说明为什么一定需要内部类。

Java编程笔记6:接口 - 魔芋红茶’s blog (icexmoon.xyz)中我们说过,Java是不支持多继承的,但一个类可以通过继承一个类和实现多个接口的方式实现某种程度上的多继承。某种程度上讲,这是一种妥协——相对于真正实现多继承的语言(如Python)来说。

事实上并不是一定无法在Java中实现多继承,通过内部类可以“曲线救国”:

package ch7.why;

class BaseA{}

class BaseB{}

class MyClass extends BaseA{
    public BaseB getBaseB(){
        return new BaseB();
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        BaseB bb = mc.getBaseB();
        BaseA ba = mc;
    }
}

就像上面演示的那样,通过使用内部类,可以在“事实上”实现让MyClass类同时具备两个基类的用途。这类问题也只能用内部类解决。

除此之外,在Java编程笔记6:接口 - 魔芋红茶’s blog (icexmoon.xyz)中提到的继承多个接口时可能产生的“方法冲突问题”,同样可以用内部类来解决:

package ch7.why2;

interface InterA {
    void test();
}

interface InterB {
    int test();
}

class MyClass implements InterA {

    @Override
    public void test() {
        System.out.println("test() is called.");
    }

    public InterB getInterB() {
        return new InterB() {

            @Override
            public int test() {
                return 0;
            }

        };
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        InterB ib = mc.getInterB();
        InterA ia = mc;
    }
}

多层嵌套

虽然并不常见,但内部类可以多层嵌套,比如:

package ch7.why3;

class A {
    private void funcA() {
        System.out.println("funcA() is called.");
    }

    class B {
        private void funcB() {
            System.out.println("funcB() is called.");
        }

        class C {
            public void funcC(){
                funcA();
                funcB();
            }
        }
    }

}

public class Main {
    public static void main(String[] args) {
        A a = new A();
        A.B b = a.new B();
        A.B.C c = b.new C();
        c.funcC();
    }
}

无论嵌套层次有多深入,嘴里侧的内部类都可以访问外部所有类的所有成员,包括private成员。

嵌套类

可以定义一种“静态内部类”,其行为就是普通的定义在类中的类,除了使用的时候要通过外部类名之外,和普通的类没有任何区别:

package ch7.static1;

class OulterCls{
    static class InnerCls{
        private String privateAttr = "private_attr";
        protected String protectedAttr = "protected_attr";
        public String publicAttr = "public_attr";
        static String staticAttr = "static_attr";
        public static void staticFunc(){
            System.out.println("staticFunc() is called.");
        }

        public void normalFunc(){
            System.out.println("normalFunc() is called.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        OulterCls.InnerCls innerCls = new OulterCls.InnerCls();
        innerCls.staticFunc();
        innerCls.normalFunc();
        // staticFunc() is called.
        // normalFunc() is called.
    }
}

上面的例子中,静态内部类InnerCls具备所有普通类的特性,就像在Java编程笔记6:接口 - 魔芋红茶’s blog (icexmoon.xyz)中介绍的嵌套接口那样,除了使用时需要使用外部类名以外,和普通类没有任何区别。所以这种内部类也称作“嵌套类”。

显然,嵌套类并没有前面介绍的普通内部类那种与外部类实例的关联关系,自然也无法使用外部类实例的引用。但好处是不需要外部类实例就可以单独创建。

继承内部类

大多数情况下都不会涉及内部类的继承,但如果你的确需要这么做,依然是可以实现的:

package ch7.extends1;

class OulterCls{
    class InnerCls{}
}

class InnerChild extends OulterCls.InnerCls{

    public InnerChild(OulterCls oc) {
        oc.super();
    }

}

public class Main {
    public static void main(String[] args) {
        OulterCls oc = new OulterCls();
        InnerChild ic =  new InnerChild(oc);
    }
}

这里InnerChild作为一个直接继承OulterCls的内部类InnerCls的类,必须实现一个包含OulterCls参数的构造函数,原因是内部类InnerCls必须包含一个外部类OulterCls实例的引用。

上面这些不难理解,但为什么需要在构造函数中调用oc.super()这样的诡异写法?

在理想情况中,我们可能希望使用super(oc)这样的写法,因为InnerChild的父类是InnerCls,如果用super调用其构造函数,并传入一个OulterCls实例,自然就建立了内部类和外部类实例的联系。但是内部类InnerChild没有这样的构造函数,也不能有,因为这种联系的建立是编译器隐式建立的,而非用构造器显式创建。所以Java就提供了一个在效果上和super(oc)作用等价的替代方式:oc.super()

如果是在同一个类中继承内部类,就不存在上面的问题:

package ch7.extends2;

class OulterCls {
    class InnerCls {
    }

    class InnerChild extends InnerCls {

    }
}

public class Main {
    public static void main(String[] args) {
        OulterCls oc = new OulterCls();
        OulterCls.InnerChild ic = oc.new InnerChild();
    }
}

定义和使用都和普通内部类没有什么区别,只不过多了一个基类。

这点实际上是我的突发奇想,《Thinking in Java》中并没有相关描述,但实际测试中发现是可行的。

覆盖内部类

普通方法可以在继承的时候进行覆盖(重写),内部类(定义)是否可以同样被覆盖?

package ch7.override1;

class ParentOulter {
    public class Inner {
        @Override
        public String toString() {
            return "ParentOulter.Inner";
        }
    }

    public Inner getInnerInstance() {
        return new Inner();
    }
}

class ChildOulter extends ParentOulter {
    public class Inner {
        @Override
        public String toString() {
            return "ChildOulter.Inner";
        }
    }

}

public class Main {
    public static void main(String[] args) {
        ChildOulter co = new ChildOulter();
        System.out.println(co.getInnerInstance());
        // ParentOulter.Inner
    }
}

在上面的示例中,ChildOulter继承了ParentOulter,并且重新定义了内部类Inner,如果内部类也会像普通方法那样被覆盖,且会存在类似的“多态机制”的话,那么调用ChildOulter实例的getInnerInstance方法应当会返回一个ChildOulter.Inner实例,但结果显然并不是。所以内部类是不能被覆盖的,每个外部类都拥有独立的内部类。

但我们可以用另一种方式让外部类进行继承的同时让内部类建立“某种联系”:

package ch7.override2;

class ParentOulter {
    public class Inner {
        public Inner() {
            System.out.println("ParentOulter.Inner is build.");
        }

        @Override
        public String toString() {
            return "ParentOulter.Inner";
        }
    }

    public Inner getInnerInstance() {
        return new Inner();
    }
}

class ChildOulter extends ParentOulter {
    public class Inner extends ParentOulter.Inner {
        public Inner() {
            super();
            System.out.println("ChildOulter.Inner is build.");
        }

        @Override
        public String toString() {
            return "ChildOulter.Inner";
        }
    }

    @Override
    public ParentOulter.Inner getInnerInstance() {
        return new Inner();
    }

}

public class Main {
    public static void main(String[] args) {
        ChildOulter co = new ChildOulter();
        System.out.println(co.getInnerInstance());
        // ParentOulter.Inner is build.
        // ChildOulter.Inner is build.
        // ChildOulter.Inner
    }
}

在外部类存在继承关系的同时,我们让内部类同样建立了继承关系。这种方式相当微妙,有点像之前在Java编程笔记5:多态 - 魔芋红茶’s blog (icexmoon.xyz)中介绍的“返回值协变”。

局部内部类VS匿名内部类

局部内部类和匿名内部类都可以在作用域和函数中使用,不过存在略微差别:

package ch7.local;

import java.util.Random;

interface Printable {
    void print();
}

public class Main {
    private static Printable getPrinter1() {
        return new Printable() {
            private int num;
            {
                num = random.nextInt(100);
            }
            private static Random random = new Random();

            @Override
            public void print() {
                System.out.println("non-name class's num:" + num);
            }

        };
    }

    private static Printable getPrinter2() {
        class NumberPrinter implements Printable {
            private int num;
            private static Random random = new Random();

            public NumberPrinter() {
                num = random.nextInt(100);
            }

            @Override
            public void print() {
                System.out.println("local inner class's num:" + num);
            }

        }
        return new NumberPrinter();
    }

    public static void main(String[] args) {
        Printable p1 = getPrinter1();
        Printable p2 = getPrinter2();
        p1.print();
        p2.print();
        // non-name class's num:87
        // local inner class's num:93
    }
}

主要的区别在局部内部类可以拥有构造函数,甚至可以重载构造函数,而匿名类则不行。此外局部内部类还可以进行多继承,匿名类同样不行。但是匿名类在写法上更简单。

总而言之,在绝大多数仅需要一个“简单实现”(比如只需要返回一个实现了某某接口的对象)时,只需要使用匿名类就可以了,这样编码效率更高。但如果功能复杂,匿名类无法满足,就可以尝试用局部内部类来进行替代,后者拥有完整的类功能。

内部类标识符

在执行Java程序时,Java编译器会将源码编译为.class结尾的字节码文件,然后再执行字节码。

在这个过程中,每个字节码文件包含一个类定义,如果一个.java文件中包含三个类定义,就会产生三个.class文件,内部类也是如此。

package ch7.flag;

import java.util.Random;

interface Printable {
    void print();
}

public class Main {
    private static Printable getPrinter1() {
        return new Printable() {
			...
            @Override
            public void print() {
                System.out.println("non-name class's num:" + num);
                System.out.println(getClass().getName());
            }

        };
    }

    private static Printable getPrinter2() {
        class NumberPrinter implements Printable {
			...
            @Override
            public void print() {
                System.out.println("local inner class's num:" + num);
                System.out.println(getClass().getName());
            }

        }
        return new NumberPrinter();
    }

    public static void main(String[] args) {
        Printable p1 = getPrinter1();
        Printable p2 = getPrinter2();
        p1.print();
        p2.print();
        // non-name class's num:38
        // ch7.flag.Main$1
        // local inner class's num:90
        // ch7.flag.Main$1NumberPrinter
    }
}

这里修改了之前的示例,让其输出内部类的类名,可以发现内部类的类名都是以<OulterClsName>$<number><InnerClsName>的方式命名的,其中$作为区分内部类和外部类名称的特殊符号。对于匿名内部类,是没有类名的,直接在$后使用一个编译器分配的数字表示。

事实上类名会直接作为生成的字节码文件名,比如上边的源码生成的字节码文件中就包含:Main$1.classMain$1NumberPrinter.class

我个人觉得之所以Java会有这么复杂的内部类设计,很大程度上是因为JavaSE8之前的函数式编程语法的缺失导致的,不得不使用这种艰涩的方式来提供类似的支持,在Python和Go中就很少看到类似的用法,因为直接传递函数对象会让代码变得更为简洁。

谢谢阅读。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值