重拾Java基础知识:内部类操作

前言

一个定义在另一个类中的类,叫作内部类。

内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类内部。但是,你将会了解到,内部类远不止如此,它了解外部类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样(而且 Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。

最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,"Why inner classes?"就应该使得内部类的益处明确显现了。

创建成员内部类

创建成员内部类的方式就如同你想的一样——把类的定义置于外部类的里面:

public class Outer {
    
    
    class Inner{
    
    
        private String label;
        public Inner(String param){
    
    
            label = param;
        }
    }
    public void getInner(String param){
    
    
        Inner inner = new Inner(param);
        System.out.println(inner.label);
    }

    public static void main(String[] args) {
    
    
        Outer o = new Outer();
        o.getInner("hello");
        /*Output:
		hello
		*/
    }
}

链接外部类

到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时此对象与制造它的外部对象(enclosing object)之间就有了一种联系,所以它能访问其外部对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外部类的所有元素的访问权

public class Outer {
    
    
    public int[] item;
    public Outer(int size){
    
    
        item = new int[size];
    }
    class Inner{
    
    
        private int length;
        {
    
    
            length=item.length;
        }
    }
    public Inner getInner(){
    
    
        Inner inner = new Inner();
        System.out.println(inner.length);
        return inner;
    }

    public static void main(String[] args) {
    
    
        Outer outer = new Outer(10);
        Inner inner = outer.getInner();
        /*Output:
		10
		*/
    }
 }

所以内部类自动拥有对其外部类所有成员的访问权(包括private成员和静态成员)。这是如何做到的呢?当某个外部类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用,如果编译器访问不到这个引用就会报错。然后,在你访问此外部类的成员时,就是用那个引用来选择外部类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外部类的对象相关联的情况下才能被创建(就像你应该看到的,内部类是非 static 类时)。不过绝大多数时候这都无需程序员操心

使用 .this 和 .new

当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,可以使用外部类的名字后面紧跟圆点和 this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。格式如下:

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

下面的示例展示了如何使用 .this

public class Outer {
    
    
    void f() {
    
     System.out.println("Outer.f()"); }
    class Inner{
    
    
        public void getOuterMethod() {
    
    
            Outer.this.f();
        }
    }
    public Inner getInner(){
    
    
        return new Inner();
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        Inner inner = outer.getInner();
        inner.getOuterMethod();
        /*
		Outer.f()
		*/
    }
}

有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 new 表达式中提供对其他外部类对象的引用,这时需要使用 .new 语法,格式如下:

外部类变量.new 内部类

要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 Outer ,而是必须使用外部类的对象来创建该内部类对象,就像在下面的程序中所看到的那样。

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

创建外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用

内部类与向上转型

当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节

我们可以创建前一个示例的接口:

public interface Contents {
    
    
    int value();
}

当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:

public class Outer {
    
    
    private class Inner implements Contents{
    
    
        private int i;
        public Inner(int val){
    
    
            i = val;
        }
        @Override
        public int value() {
    
    
            return i;
        }
    }
    public Contents getInner(int val){
    
    
        return new Inner(val);
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        Inner inner = outer.new Inner(12);
    }
}

在Outer中,内部类 Inner是 private,所以除了 Outer,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected(除非是继承自它的子类);

private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会

局部内部类

到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。
这么做有两个理由:

  1. 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。

public class Outer {
    
    
    public Contents getParam(int value){
    
    
        final class Inner implements Contents{
    
    
            private int values;
            public Inner(int val){
    
    
                values = val;
            }
            @Override
            public int value(int param) {
    
    
                return values;
            }
        }
        return new Inner(value);
    }

    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        Contents param = outer.getParam(101);
    }
}

Inner 类是 getParam() 方法的一部分,所以,在 getParam() 之外不能访问 Inner ,注意出现在 return 语句中的向上转型-返回的是 Contents 的引用,它是 Inner 的基类。一旦 getParam() 方法执行完毕,并不意味着Inner 就不可用了。

下面的例子展示了如何在任意的作用域内嵌入一个内部类:

public class Outer {
    
    
    private void actionScope(Boolean bool){
    
    
        if(bool){
    
    
            class Inner{
    
    
                private String name;
                public Inner(String value){
    
    
                    name = value;
                }
            }
            Inner inner = new Inner("hello");
            String name = inner.name;
            System.out.println(name);//Output: hello
        }
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        outer.actionScope(true);
    }
}

嵌入在 if 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。他除了定义在作用域内,和其他的类没有区别。

匿名内部类

下面的例子看起来有点奇怪:

public class Outer {
    
    
    public Contents contents(){
    
    
        return new Contents() {
    
    
            private int i = 10;
            @Override
            public int value(int param) {
    
    
                return i;
            }
        };
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        Contents contents = outer.contents();
    }
}

contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。

这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:

public class Outer {
    
    
    class Inner implements Contents{
    
    
        private int i = 10;
        @Override
        public int value(int param) {
    
    
            return i;
        }
    }
    public Contents contents(){
    
    
        return new Inner();
    } 
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        Contents contents = outer.contents();
    }
}

如果你的基类需要一个有参数的构造器,可以定义一个具有具体实现的普通类重写一个有参构造器,就像下面例子一样:

public class Wrapping {
    
    
    private String name;
    public Wrapping(String value){
    
    
        name = value;
    }
    public String getName(){
    
    
        return name;
    }
}

定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final的(初始化后不会改变,所以可以被当作 final,如果参数没有在匿名类中使用可以不用final定义),即使不加 final, Java 8 的编译器也会为我们自动加上 final,以保证数据的一致性

public class Outer {
    
    
    public Wrapping getName(final String name){
    
    
        return new Wrapping(name){
    
    
            @Override
            public String getName() {
    
    
                return name;
            }
        };
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        String name= outer.getName("jack Ma").getName();
        System.out.println(name);//Output: jack Ma
    }
}

匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果

public class Outer {
    
    
    public Wrapping getName(final String name){
    
    
        return new Wrapping(name){
    
    
        	//构造器初始化
            {
    
    
                System.out.println("INIT");
            }
            @Override
            public String getName() {
    
    
                return name;
            }
        };
    }
    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        String name= outer.getName("jack Ma").getName();
        System.out.println(name);
        /*
		INIT
		jack Ma
		*/
    }
}

对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器

匿名内部类与正规的继承有些受限,因为匿名内部类只能扩展类或实现接口,而且如果是实现接口,也只能实现一个接口

静态内部类

如果不需要内部类对象与其外部类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。想要理解 static 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:

  1. 要创建嵌套类的对象,并不需要其外部类的对象。
  2. 不能从嵌套类的对象中访问非静态的外部类对象。

嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,并且它不能使用外部类的非static成员变量或者方法。

public class Outer {
    
    
    static class Inner implements Contents {
    
    
        static int i;

        @Override
        public int value() {
    
    
            return i;
        }

        public Inner(int val) {
    
    
            i = val;
        }
    }

    public static void main(String[] args) {
    
    
        Outer.Inner inner= new Outer.Inner(100);
        System.out.println(inner.value());//Output: 100
    }
}

在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外部类对象。嵌套类就没有这个特殊的 this 引用,这使得它类似于一个 static 方法。

为什么需要内部类

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

为了看到更多的细节,直接以抽象的类或具体的类,使用内部类实现多重继承:

public class Space {
    
    }

public abstract class Interior {
    
    }

public class Outer extends Space{
    
    
    Interior getInterior(){
    
    
        return new Interior() {
    
    };
    }
    static void testSpace(Space space){
    
    }
    static void testInterior(Interior interior){
    
    }

    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        testSpace(outer);
        testInterior(outer.getInterior());
    }
}

如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
  2. 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
  3. 创建内部类对象的时刻并不依赖于外部类对象的创建
  4. 内部类并没有令人迷惑的"is-a”关系,它就是一个独立的实体。

闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外部类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。

在JAVA中,闭包是通过“接口+内部类”实现,而非指针,它比指针更灵活、更安全。见下例:

创建一个Teacher 接口,且拥有一个work()方法

public interface Teacher {
    
    
    void work();
}

创建一个Programmer类,并且也拥有一个work()方法

public class Programmer {
    
    
    public String name;

    public Programmer(){
    
    }

    public Programmer(String name) {
    
    
        this.name = name;
    }

    public void work(){
    
    
        System.out.println(name + "在码代码");
    }
}

需求:Teacher和Programmer都可以教书,但是Programmer主要以编码为主,Teacher以教书育人为主,如何实现同一个角色包含两种不同的工作内容呢?

可以定义一个Engineer类继承Programmer类,通过一个仿闭包的内部类实现Teacher接口

public class Engineer extends Programmer {
    
    
    Engineer(String name){
    
    
        super(name);
    }
    private void teach(){
    
    
        System.out.println(this.name +"正在讲课");
    }
   private class Closure implements Teacher{
    
    

        @Override
        public void work() {
    
    
            //回调
            Engineer.this.teach();
        }
    }
    //返回一个非静态内部类引用,允许外部类通过该非静态内部类引用来回调外部类的方法
    public Teacher getTeacher(){
    
    
        return new Closure();
    }

    public static void main(String[] args) {
    
    
            Engineer engineer =new Engineer("李刚");
            engineer.work();
            engineer.getTeacher();
    }
}

上面的Engineer类继承Programmer类,所以它可以直接调用基类的work方法,该类也包含教学的teach()方法,但这个方法与Teacher接口没有任何关系,Engineer 也不能当成Teachable使用。此时创建了一个Closure内部类,它实现了Teachable接口,并实现了教学的work方法通过 回调 Engineer 类的teach()方法实现。

注意,在 Engineer 中除了 getTeacher() 以外,其他成员都是 private 的。 内部类 Closure 实现了 Teacher,以提供一个返回 Callee2 的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 Teacher 的引用,都只能调用 getTeacher(),除此之外没有其他功能(不像指针那样,允许你做很多事情)。

继承内部类

因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:

public class InheritInner extends WithInner.Inner {
    
    
    InheritInner(WithInner withInner){
    
    
        withInner.super();
    }

    public static void main(String[] args) {
    
    
        WithInner withInner = new WithInner();
        InheritInner inheritInner = new InheritInner(withInner);
    }
}

InheritInner 只继承自内部类,而不是外部类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外部类对象的引用。此外,必须在构造器内使用如下语法,程序才能编译通过:

外部类.super();

内部类可以被覆盖么?

内部类可以被覆盖吗?这看起来似乎是个很有用的思想,但是“覆盖”内部类就好像它是外部类的一个方法,其实并不起什么作用:

class Egg {
    
    
    private Yolk y;
    protected class Yolk {
    
    
        public Yolk() {
    
    
            System.out.println("Egg.Yolk()");
        }
    }
    Egg() {
    
    
        System.out.println("New Egg()");
        y = new Yolk();
    }
}
public class BigEgg extends Egg {
    
    
    public class Yolk {
    
    
        public Yolk() {
    
    
            System.out.println("BigEgg.Yolk()");
        }
    }
    public static void main(String[] args) {
    
    
        new BigEgg();
        /*Output:
		New Egg()
		Egg.Yolk()
		*/
    }
}

你可能认为既然创建了 BigEgg 的对象,那么所使用的应该是“覆盖后”的 Yolk 版本,但从输出中可以看到实际情况并不是这样的。这个例子说明,当继承了某个外部类的时候,内部类并没有发生什么特别神奇的变化。

当然,明确地继承某个内部类也是可以的:

class Egg2 {
    
    
    protected class Yolk {
    
    
        public Yolk() {
    
    
            System.out.println("Egg2.Yolk()");
        }
        public void f() {
    
    
            System.out.println("Egg2.Yolk.f()");
        }
    }
    private Yolk y = new Yolk();
    Egg2() {
    
     System.out.println("New Egg2()"); }
    public void insertYolk(Yolk yy) {
    
     y = yy; }
    public void g() {
    
     y.f(); }
}
public class BigEgg2 extends Egg2 {
    
    
    public class Yolk extends Egg2.Yolk {
    
    
        public Yolk() {
    
    
            System.out.println("BigEgg2.Yolk()");
        }
        @Override
        public void f() {
    
    
            System.out.println("BigEgg2.Yolk.f()");
        }
    }
    public BigEgg2() {
    
     insertYolk(new Yolk()); }
    public static void main(String[] args) {
    
    
        Egg2 e2 = new BigEgg2();
        e2.g();
        /*Output:
		Egg2.Yolk()
		New Egg2()
		Egg2.Yolk()
		BigEgg2.Yolk()
		BigEgg2.Yolk.f()
		*/
    }
}

BigEgg2.Yolk 通过 extends Egg2.Yolk 明确地继承了此内部类,并且覆盖了其中的方法。insertYolk() 方法允许 BigEgg2 将它自己的 Yolk 对象向上转型为 Egg2 中的引用 y。所以当第二次调用 时变成了子类的方法。

内部类标识符

由于编译后每个类都会产生一个**.class** 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象)。

你可能猜到了,内部类也必须生成一个**.class** 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外部类的名字,加上==“**$",再加上内部类的名字==。

Outer.class
Outer$Inner.class

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符

Inner$1.class

如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与“$”的后面

Inner$Inmost.class

虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是 java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)

知识拓展

局部内部类和匿名内部类的区别?

前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外部类的一部分;但是它可以访问当前代码块内的常量,以及此外部类的所有成员。下面的例子对局部内部类与匿名内部类的创建进行了比较。

public class Outer{
    
    
    public Contents localMethod(final int val){
    
    
        class Inner implements Contents{
    
    
            Inner(){
    
    
                System.out.println("localInit");
            }
            @Override
            public int value() {
    
    
                System.out.println("localValue = "+val);
                return val;
            }
        }
        return new Inner();
    }
    public Contents anonymityMethod(final int val){
    
    
        return new Contents() {
    
    
            {
    
    
                System.out.println("anonymityInit");
            }
            @Override
            public int value() {
    
    
                System.out.println("anonymityValue = "+val);
                return val;
            }
        };
    }

    public static void main(String[] args) {
    
    
        Outer outer = new Outer();
        outer.localMethod(100).value();
        outer.anonymityMethod(99).value();
        /* Output:
		localInit
		localValue = 100
		anonymityInit
		anonymityValue = 99
		*/
    }
}

我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。另一个理由就是,需要不止一个该内部类的对象。

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

链接内部类的时候我们知道外部类创建内部类的时候,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用,通过内部类标识符也了解了编译器在进行编译的时候,会将外部类和内部类各生成一个.class文件。下面将我所学到的知识与大家分享:

public class Outer {
    
    
    public Outer() {
    
    
    }

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

    protected class Inner {
    
    
        public Inner() {
    
    

        }
    }

    public static void main(String[] args) {
    
    
        Outer outer=new Outer();
        outer.getInner();
    }
}

编译之后,在项目的目录里面找到class存放地址,会发现出现了两个字节码文件:
在这里插入图片描述
如果想自己去研究class文件可以,不知道怎么反编译的可以点击这里,反编译Outer$Inner.class文件,得到以下信息。

Classfile /D:/study/out/production/study/com/study/test/inner/Outer$Inner.class
  Last modified 2020-9-6; size 476 bytes
  MD5 checksum 8c4f31d2319c0264ea1c480a95c0d17a
  Compiled from "Outer.java"
public class com.study.test.inner.Outer$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#18         
   #2 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #3 = Class              #21            // com/study/test/inner/Outer$Inner
   #4 = Class              #22            // java/lang/Object
  //此处省略常量池的内容
  #23 = Utf8               ()V
  #24 = Utf8               com/study/test/inner/Outer
{
    
    
  final com.study.test.inner.Outer this$0;
    descriptor: Lcom/study/test/inner/Outer;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public com.study.test.inner.Outer$Inner(com.study.test.inner.Outer);
    descriptor: (Lcom/study/test/inner/Outer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Lcom/study/test/inner/Outer;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 13: 0
        line 15: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/study/test/inner/Outer$Inner;
            0      10     1 this$0   Lcom/study/test/inner/Outer;
}

在 第18行可以看到这一个final修饰的变量,也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用

final com.study.test.inner.Outer this$0;

下面我们可以看到内部类的无参构造里面还是会默认添加一个外部类对象的引用,所以成员内部类可随意访问外部类的成员。也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

 public com.study.test.inner.Outer$Inner(com.study.test.inner.Outer);

其实还有一种办法可以看到内部类是如何访问外部类的,将class文件反编译为可视化代码,也就一目了然了。

public class Outer$Inner {
    
    
    public Outer$Inner(Outer this$0) {
    
    
        this.this$0 = this$0;
    }
}

为什么局部内部类和匿名内部类的形参为final变量?

我们之前在匿名内部类的时候讲过:外部定义的对象,那么编译器会要求其参数引用是 final的,即使不加上java8也会自动给我们加上。那么我们就来分析一下:为什么要设置为final?这样的好处是什么?

public class Outer {
    
    
    public void getInner(final String val,int i){
    
    
        class Inner{
    
    
          private void InnerMethod(){
    
    
              System.out.println("val:"+val+",i:"+i);
            }
        }
    }
    public static void main(String[] args) {
    
    
        Outer outer=new Outer();
        outer.getInner("hello",1);
    }
}

还是和以前一样会编译两个class文件,局部内部类和匿名内部类会生成一个简单的数字标识符,不过不懂生成原理的可以回到内部类标识符重新学习。

在这里插入图片描述

从上面代码中看好像 val 和 i 参数应该是被内部类直接调用。Java采用了 复制 的手段来解决这个问题,我们把class反编译的字节码信息打印出来就知道了。

Compiled from "Outer.java"
class com.study.test.inner.Outer$1Inner {
    
    
  final java.lang.String val$val;

  final int val$i;

  final com.study.test.inner.Outer this$0;

  com.study.test.inner.Outer$1Inner();
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lcom/study/test/inner/Outer;
       5: aload_0
       6: aload_2
       7: putfield      #2                  // Field val$val:Ljava/lang/String;
      10: aload_0
      11: iload_3
      12: putfield      #3                  // Field val$i:I
      15: aload_0
      16: invokespecial #4                  // Method java/lang/Object."<init>":()V
      19: return
}

下面两段代码可以知道内部类在进行编译的时候并不是调用的外部方法传递进来的参数。而是会将这两个参数在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量直接将相应的字节码嵌入到执行字节码中,从而是方法中的参数和内部类参数独立开

7: putfield      #2                  // Field val$val:Ljava/lang/String;
12: putfield      #3                  // Field val$i:I

这样可能还是太模糊了,我们将class文件反编译成可视化代码,这样就非常清楚的知道他们是如何独立开的。

public class Outer {
    
    
    public Outer() {
    
    
    }

    public void getInner(String val, int i) {
    
    
        class Inner {
    
    
            Inner(String var2, int var3) {
    
    
                this.val$val = var2;
                this.val$i = var3;
            }

            private void InnerMethod() {
    
    
                System.out.println("val:" + this.val$val + ",i:" + this.val$i);
            }
        }

    }
}

外部类和内部类从外表上看是同一个参数,经过讲解知道他们其实是两个不同的变量,感觉好像在内部类改变参数似乎没什么影响,但是会造成数据不一致性。所以在java8之前为什么要强制使用final修饰,就是为了避免引用值发生改变。

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

之前在静态内部类讲解过:不需要内部类对象与其外部类对象之间有联系,可以在不创建外部类对象的情况下创建内部类的对象,静态内部类是不持有指向外部类对象的引用的。

public class Outer {
    
    
    static class Inner{
    
    
    }
    public static void main(String[] args) {
    
    
        Inner outer = new Inner();
    }
}

反编译可以看到:

Compiled from "Outer.java"
class Outer$Inner {
    
    
  Outer$Inner();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
}

猜你喜欢

转载自blog.csdn.net/qq_39940674/article/details/108330273
今日推荐