Java基础:内部类

前言

在开发过程中不想创建类文件或者想把逻辑相关的类放在一起,就在类中创建类,这就是本节要讨论——内部类。

正题

在开始编写文章前,有几个问题需要思考一下:

  • 什么是内部类?
  • 内部类的作用
  • 如何创建内部类对象
  • 内部类的其他特性

1. 什么是内部类?

将一个类的定义放在另一个类的定义内部,这就是内部类。

2. 内部类的作用

  • 内部类方法可以访问该类定义所在的外围类作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。

3. 如何创建内部类对象

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

public class OutClass {
    void f()
    {
        System.out.println("OutClass.f()");
    }

    public class Inner {
        public OutClass outer()
        {
            return OutClass.this;
        }
    }

    public static void main(String[] args) {
        OutClass outClass = new OutClass();
        outClass.new Inner();
    }
}

.new 表达式对应的字节码:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: new           #5                  // class back/mdj/com/test/OutClass
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: new           #7                  // class back/mdj/com/test/OutClass$Inner
        11: dup
        12: aload_1
        13: dup
        14: invokevirtual #8                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        17: pop
        18: invokespecial #9                  // Method back/mdj/com/test/OutClass$Inner."<init>":(Lback/mdj/com/test/OutClass;)V
        21: pop
        22: return
}

Inner.class 对应的字节码:

{
  final back.mdj.com.test.OutClass this$0;
    descriptor: Lback/mdj/com/test/OutClass;
    flags: ACC_FINAL, ACC_SYNTHETIC
}

字段二级制数据:10 10 00 05 00 06

在常量池里维护一个指向外部类的 final 对象 this$0,在内部类的构造函数里会把外围类对象引用赋值给该对象,从而使内部类维护外部类的一个引用。所以,在拥有外部类对象之前是不可能创建内部类对象的,这是因为内部类对象会暗暗的连接到创建它的外部类对象上。

一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。

每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

注意:当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密的捕获一个指向那个外围类对象的引用,此后在你访问外围类成员时,就是用那个引用来选择外围类的成员。必须要用外部类的对象来创建内部类对象。

4. 内部类的其他特性

4.1 多重继承

如果没有内部类提供的可以继承多个具体的或抽象的类的能力,一些设计编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口实现了部分问题,而内部类有效的实现了“多重继承”,也就是说,内部类允许继承多个非接口类型。

public class D {
}

public abstract class E {
}

public class F extends E{
    public D makeD()
    {
        return new D();
    }
}

public class G 
{
    static void takesD(D d){}
    static void takesE(E e){}
    public static void main(String args[]) {
        F f = new F();
        takesD(f.makeD());
        takesE(f);
    }
}

java语言单继承特性,以上场景通过内部类来实现多重继承。

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

上面只是简单的在类中创建内部类,然而内部类的语法覆盖了大量其他难以理解的技术,例如,可以在一个方法或者任意作用域内定义内部类,这么做有两大理由:

  1. 实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 你需要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共的。
public interface Destination {
    String readLabel();
}

public class Parcel1 {
    public Destination destination(String s)
    {
        class PDestination implements  Destination
        {
            private String label;

            private PDestination(String s)
            {
                label = s;
            }

            @Override
            public String readLabel() {
                return label;
            }
        }

        return new PDestination(s);
    }

    public static void main(String args[]) {
        Parcel1 parcel1 = new Parcel1();
        parcel1.destination("child");
    }
}

PDestination类时destination()方法的一部分,而不是Parcel1的一部分。所以在destination()方法之外不能访问PDestination。虽然 PDestination 构造方法是 private,但是 Java 虚拟机会自动转为包访问权限:

  com.inner.Parcel1$1PDestination(com.inner.Parcel1, java.lang.String);
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Lcom/inner/Parcel1;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: aload_0
        10: aload_2
        11: putfield      #3                  // Field label:Ljava/lang/String;
        14: return

4.3 内部类覆盖

public class Egg {
    private York y;
    protected class York {
        public York() {
            System.out.println("Egg.York");
        }
    }

    public Egg() {
        System.out.println("Egg");
        y = new York();
    }
}

public class BigEgg extends Egg {
    public class York {
        public York() {
            System.out.println("BigEgg.York");
        }
    }

    public static void main(String args[]) {
        BigEgg bigEgg = new BigEgg();
    }
}

运行结果:

Egg
Egg.York

从结果可以看出:当继承某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立地两个实体,各自在自己的命名空间内。

4.4 继承内部类

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

public class D {
    public class A{}
}

public class C extends D.A{
    C(D d) {
        d.super();
    }
}

可以看到 C 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用,此外必须在构造器内使用如下语法:enclosingClassReference.super();

4.5 接口内部类

正常情况下,不能在接口内放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动的是 public 和 static。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口。

public interface AInterface {
    void A();

    class B implements AInterface {

        @Override
        public void A() {
            System.out.println("A()");
        }

        public static void main(String args[]) {
            new B().A();
        }
    }
}

B.class 对应的字节码:

Constant pool:
   #1 = Methodref          #8.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #20.#21        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #22            // A()
   #4 = Methodref          #23.#24        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #25            // back/mdj/com/test/AInterface$B
   #6 = Methodref          #5.#19         // back/mdj/com/test/AInterface$B."<init>":()V
   #7 = Methodref          #5.#28         // back/mdj/com/test/AInterface$B.A:()V
   #8 = Class              #29            // java/lang/Object
   #9 = Class              #30            // back/mdj/com/test/AInterface
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               A
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               SourceFile
  #18 = Utf8               AInterface.java
  #19 = NameAndType        #10:#11        // "<init>":()V
  #20 = Class              #31            // java/lang/System
  #21 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #22 = Utf8               A()
  #23 = Class              #34            // java/io/PrintStream
  #24 = NameAndType        #35:#36        // println:(Ljava/lang/String;)V
  #25 = Utf8               back/mdj/com/test/AInterface$B
  #26 = Utf8               B
  #27 = Utf8               InnerClasses
  #28 = NameAndType        #14:#11        // A:()V
  #29 = Utf8               java/lang/Object
  #30 = Utf8               back/mdj/com/test/AInterface
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (Ljava/lang/String;)V
{
  public back.mdj.com.test.AInterface$B();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0

  public void A();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String A()
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #5                  // class back/mdj/com/test/AInterface$B
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: invokevirtual #7                  // Method A:()V
        10: return
      LineNumberTable:
        line 18: 0
        line 19: 10
}

其中 InnerClasses 属性值:00 1B 00 00 00 0A 00 01 00 05 00 09 00 1A 00 09

从 InnerClasses 属性的访问标志位:00 09 可知该内部类是 public 和 static 的。从内部类的字节码可以获知:接口内部类没有指向一个对接口的引用。

4.6 匿名内部类

public interface Contents {
    int value();
}

public class Parcel {
    public Contents contents()
    {
        return new Contents() {
            private int i = 8;
            public int value() {
                return i;
            }
        };
    }
    public static void main(String args[]) {
        Parcel parcel = new Parcel();
        parcel.contents();
    }
}

使用 javac 命令编译Parcel文件,会自动生成 Parcel$1.class 文件,对应函数 contents() 创建的匿名内部类:

class Parcel$1 implements Contents {
    private int i;

    Parcel$1(Parcel var1) {
        this.this$0 = var1;
        this.i = 8;
    }

    public int value() {
        return this.i;
    }
}

在代码里创建内部类,但是在字节码层次上,每一个内部类都有一个对应的 class 文件。在某种程度上,内部类方便了开发(不需要新建一个继承某个接口的类),但是在字节码层次上,虚拟机替我们完成了创建一个新类的工作。

Contents.class 对应的字节码:

Constant pool:
   #1 = Fieldref           #4.#22         // com/inner/Parcel$1.this$0:Lcom/inner/Parcel;
   #2 = Methodref          #5.#23         // java/lang/Object."<init>":()V
   #3 = Fieldref           #4.#24         // com/inner/Parcel$1.i:I
   #4 = Class              #25            // com/inner/Parcel$1
   #5 = Class              #27            // java/lang/Object
   #6 = Class              #28            // com/inner/Contents
   #7 = Utf8               i
   #8 = Utf8               I
   #9 = Utf8               this$0
  #10 = Utf8               Lcom/inner/Parcel;
  #11 = Utf8               <init>
  #12 = Utf8               (Lcom/inner/Parcel;)V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               value
  #16 = Utf8               ()I
  #17 = Utf8               SourceFile
  #18 = Utf8               Parcel.java
  #19 = Utf8               EnclosingMethod
  #20 = Class              #29            // com/inner/Parcel
  #21 = NameAndType        #30:#31        // contents:()Lcom/inner/Contents;
  #22 = NameAndType        #9:#10         // this$0:Lcom/inner/Parcel;
  #23 = NameAndType        #11:#32        // "<init>":()V
  #24 = NameAndType        #7:#8          // i:I
  #25 = Utf8               com/inner/Parcel$1
  #26 = Utf8               InnerClasses
  #27 = Utf8               java/lang/Object
  #28 = Utf8               com/inner/Contents
  #29 = Utf8               com/inner/Parcel
  #30 = Utf8               contents
  #31 = Utf8               ()Lcom/inner/Contents;
  #32 = Utf8               ()V
{
  final com.inner.Parcel this$0;
    descriptor: Lcom/inner/Parcel;
    flags: ACC_FINAL, ACC_SYNTHETIC

  com.inner.Parcel$1(com.inner.Parcel);
    descriptor: (Lcom/inner/Parcel;)V
    flags:
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Lcom/inner/Parcel;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: aload_0
        10: bipush        8
        12: putfield      #3                  // Field i:I
        15: return
      LineNumberTable:
        line 6: 0
        line 7: 9

  public int value();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field i:I
         4: ireturn
      LineNumberTable:
        line 10: 0
}

contents() 方法将返回值的生成与表示这个这个返回值的类的定义结合在一起,另外这个类时匿名的,它没有名字。更糟糕的是,看起来似乎是你正要创建一个 Contents 对象。这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。

在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此这与别的地方使用的分号是一致的。

下面看看内部类的一些特性:

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

4.7 嵌套类

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

public class OuterClass {
	
	public static class Inner {}

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

但是如果你创建的是嵌套类(静态内部类),那么它就不需要外部类对象的引用。

嵌套类意味着:

  • 要创建嵌套类对象并不需要创建外围类对象。
  • 不能从嵌套类对象中访问非静态的外围类对象

嵌套类与普通内部类还有一个区别:普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类(内部类对象依附于外围类,在拥有外部类对象之前是不可能创建内部类对象,内部维护一个指向外围类对象的引用,而 static 数据和字段只属于类不属于对象,所以不能再内部类中包含 static 数据)。

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

当继承了某个外围类的时候,内部类并没有发生什么特别的神奇变化,这两个内部类是完全独立地个体,各自在自己的命名空间内。

java内部类作用

参考文章:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值