Java内部类详解

427 阅读17分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

Java内部类详解

前言

这一篇写了两天多...
因为当时学Java基础的时候根本没讲这么细也没学这么细......
导致写的进度贼慢... 一直找资料,也一直想例子、打代码,头都秃了😇
本来是想先把JavaSE的内容搞完再发的,但想想还是算了,择日不如撞日吧🤪
最后,希望大家可以多多点评,多多指点。小弟第一次写博客,恳请各位大咖不要手下留情😖
PS:我真的不是鸽了,每天都有在码,只是太少了不敢发出来...😭 JavaSE的内容在做了,只不过因为这篇鸽了几天而已 (别骂了)

一、内部类基础

在Java中,可以将一个类定义在另一个类里面或者一个方法里面,我们将这样的类称为内部类。一般内部类包括以下四种:1. 成员内部类、2. 局部内部类、3. 匿名内部类、4. 静态内部类。

1. 成员内部类

成员内部类是最普通的内部类,它被定义为位于另一个类的内部。

public class OuterClass {
    private int data; // 之前讲过, 方法内的变量必须在使用之前被赋值(但此处并不用)
    public void m(){
        System.out.println("OuterClass m()");
    }
    
    class InnerClass{
        public void OuterClass_M(){
            System.out.println("data: " + data); // 1、3. 调用 外部类的私有成员变量
            OuterClass.this.m(); // 4、5. 访问外部类的同名成员方法
        }
        // 2. 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象
        public void m(){
            System.out.println("InnerClass m()");
        }
    }

    public static void main(String[] args) {
        // 以下形式等价于: OuterClass.InnerClass inner = new OuterClass().new InnerClass();
        OuterClass outer = new OuterClass();
        InnerClass inner = outer.new InnerClass();
//        OuterClass.InnerClass otherInner = new OuterClass.InnerClass(); // 调用静态内部类的方式(非静态的只能用上述的!!!)
        inner.OuterClass_M();
    }
}
Output:
        data: 0
        OuterClass m()

上述代码总结:

  1. 成员内部类可以无条件的访问外部类的所有成员变量和成员方法(包括private成员和静态成员)。即 内部类拥有其外部类的所有元素的访问权限。
  2. 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象。默认情况下访问的是成员内部类的成员。
  3. 当某个外部类的对象创建一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用(这就是为什么内部类会拥有其外部类所有元素的访问权限的原因)。然后,在你访问此外部类的成员时,就是用那个引用来选择外部类的成员。简的来说:普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。
  4. 如果想显式的访问外部类的同名成员时,需要以下面的形式进行访问
    1. 外部类.this.成员变量
    2. 外部类.this.成员方法
  5. 外部类.this ==> 生成对外部类对象的引用(在内部类中使用)

虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中,如果要访问成员内部类的成员时,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。

public class OuterClass {
    OuterClass() {
        System.out.println("OuterClass: 我打算生成一个外部类对象并访问其成员变量");
        System.out.println("该成员变量的值为:" + new InnerClass().innerNumber); // 外部类要想访问内部类的成员变量的话,需要创建一个内部类对象才可以访问
    }

protected class InnerClass {
        int innerNumber = 1;

        InnerClass() {
            System.out.println("InnerClass");
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
    }
}
Output:
        OuterClass: 我打算生成一个外部类对象并访问其成员变量
        InnerClass
        该成员变量的值为:1

小结:

  1. 成员内部类是依附外部类而存在的。 也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
  2. 创建成员内部类对象的方式有如下两种
第一种:     OuterClassName.InnerClassName 变量名 = new OuterClassName().new InnerClassName()

第二种:     OuterClassName 变量名1 = new OuterClassName();
               InnerClassName 变量名2 = 变量名1.new InnerClassName(); 

// 其实它们本质上是一样的,只是第二个是第一个的拆开的表现形式罢了
  1. 内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。然鹅,外部类却只能被public和包访问两种权限修饰。 这使得内部类越来越像外部类的一个成员了,所以内部类才可以像类的成员一样拥有多种权限修饰。即 可以形象的将内部类理解为外部类的一个成员。

2. 局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类它和成员内部类的区别在于局部内部类的访问仅限在于方法内或者该作用域内。

public class OuterClass {
    
    OuterClass(){
        System.out.println("I'm OuterClass");
    }
    
    private void innerTracking(boolean b){
        if (b){
            class InnerClass{ // 局部内部类 作用范围仅限于该作用域(if)中。 PS:局部内部类的访问权限不能是 public、protected、private以及static
                private String id;
                InnerClass(String s){
                    id = s;
                    System.out.println("I'm InnerClass");
                }
                String getSlip() {
                    return id;
                }
            }
            InnerClass inner = new InnerClass("innerTrack");
            System.out.println(inner.getSlip());
        }
        // 以下代码均是超过作用域而无法使用的
//        System.out.println(inner.getSlip());
//        InnerClass internal = new InnerClass("innerTrack");
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.innerTracking(true);
    }
}
Output:
        I'm OuterClass
        I'm InnerClass
        innerTrack

注意:
局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

3. 匿名内部类

匿名内部类是一个没有名字的内部类,是一个特殊的局部内部类它将定义一个内部类以及创建一个内部类的实例(二者结合在一起,一步到位)

  1. 匿名内部类的转变形式与定义方式
// 自定义的一个Contents接口
public interface Contents {
    int value();
}
public class Parcel {

    public Contents contents() { // 自定义的一个Contents接口, 返回Contents对象
        return new Contents() { // 创建一个继承自 Contents 的匿名对象。
            // 且通过new表达式返回的引用被自动向上转型为对Contents的引用
            private int i = 11;

            @Override
            public int value() {
                return i;
            }
        }; // 需要分号!
    }

    public static void main(String[] args) {
        Parcel p = new Parcel();
        Contents c = p.contents();
        System.out.println("该匿名内部类的i值为:" + c.value());
    }
}

上述代码等价于:

public class Parcel2 {

    public Contents contents(){
        return new MyContents();
    }

    class MyContents implements Contents{ // 创建一个继承自Contents的MyContents对象
        private int i = 11;
        @Override
        public int value() {
            return i;
        }
    }

    public static void main(String[] args) {
        Parcel p = new Parcel();
        Contents c = p.contents();
        System.out.println("该匿名内部类的i值为:" + c.value());
    }
}
Output:
        该匿名内部类的i值为:11

在上两则代码中,我们不难看出。匿名内部类省略了具体实现类的的类名、 extends(继承)、 implements(实现)和 class(类)的关键字。

  1. 内部类使用外部类形参时所要注意的要点:

我们给局部(匿名)内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要定义为final。 也就是说:当所在的方法的形参需要被内部类所使用时,该形参必须为final。 如果定义一个局部(匿名)内部类,并且希望它使用一个在其外部定义的对象,那么编译器就会要求将该参数定义为final

// 匿名内部类所要实现的接口
public interface Destination {
    String readLabel();
}
public class Parcel3 {
// 为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。
    public Destination destination (final String dest){
        return new Destination() {
            private String label = dest;
            @Override
            public String readLabel() {
                return label;
            }
        };
    }

    public static void main(String[] args) {
        Parcel p = new Parcel();
        Destination d = p.destination("Hello");
        System.out.println(d.readLabel());
    }
}
Output:
        Hello

为什么局部(匿名)内部类所在的方法的形参需要被使用时,该形参要被设置成final呢?

这是由于,内部类中的属性与外部类中方法的参数,这两者从外表上看是同一个东西, 但实际上却不是,所以他们两者是可以任意变化的。也就是说:在内部类中我对属性的改变并不会影响到外部的形参。 然鹅,从程序员的角度来看这是不可行的😥,毕竟站在程序的角度来看这两个根本就是同一个。如果内部类改变了,而外部方法的形参却没有改变这是难以理解和不可接受的。所以为了保证参数的一致性,就规定使用final来避免形参的改变。

简的来说:内部类将形参的值复制了一份。然鹅,为了避免引用值发生改变。例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

  1. 匿名内部类的初始化

我们一般都是利用构造器来完成对某一实例的初始化工作的。但由于匿名内部类没有构造器。我们该怎么初始化匿名内部类呢?——答案是 使用实例代码块(构造代码块)来初始化匿名内部类。 因为实例代码块能够达到为匿名内部类创建一个构造器的效果。

// 创建一个OuterClass的匿名内部类所要继承的InnerClass类
public class InnerClass {
    private String name;
    public String getName() {
        return name;
    }
}
public class OuterClass {
    // 1. 验证实例代码块会添加到构造器的前面(JVM自动添加)
    OuterClass() {
        System.out.println("初始化OuterClass构造器");
    }

    // 2. 验证成员属性的初始化与实例代码块的执行顺序有关(按顺序执行)
    {
        i = 666; // 如果 int i = 6;放置在实例代码块的上面 则输出666,反之输出6
        System.out.println("OuterClass实例代码块构造完成");
    }

    int i = 6;

    public InnerClass getInnerClass(final String name) {
        return new InnerClass() {
            String name_;

            // 3. 在匿名内部类中使用实例代码块来完成初始化工作
            {
                name_ = name;
                System.out.println("InnerClass实例代码块构造完成");
            }

            public String getName() {
                return name_;
            }
        };
    }

    public static void main(String[] args) {
        OuterClass out = new OuterClass();
        System.out.println("i = " + out.i);

        InnerClass inner_1 = out.getInnerClass("Cat");
        System.out.println(inner_1.getName());

        InnerClass inner_2 = out.getInnerClass("Rat");
        System.out.println(inner_2.getName());
    }
}
Output:
        OuterClass实例代码块构造完成
        初始化OuterClass构造器
        i = 6
        InnerClass实例代码块构造完成
        Cat
        InnerClass实例代码块构造完成
        Rat

由以上代码我们可以知道:(前两条为构造代码块的知识点)

  1. 如果类中有构造代码块,Java编译器在编译时会先将实例代码块中的代码移到构造器中执行,构造函数中原有的代码最后执行。即 实例代码块会将块中的代码放置在构造器里的最前面,然后优先执行。
  2. 成员属性的初始化和构造代码块的执行顺序是根据原码中的位置执行的
  3. 虽然匿名内部类没有构造器,但是可以使用实例代码块(构造代码块)来初始化匿名内部类。 以达到相同的效果。

小结:

  1. 匿名内部类表达了:
    1. 创建一个继承自ClassName的匿名类的对象。
    2. 且通过new表达式返回的应用被自动向上转型为ClassName的引用。
  2. 匿名内部类只是为了获得一个对象的实例,而不需要知道其实际类型。
  3. 匿名类用于继承其他类或是实现其他接口。它并不需要增加额外的方法,只是对继承的方法进行实现或是覆盖。
  4. 在匿名内部类末尾的分号,并不是用来标记内部类是否结束的。事实上,它是用来标记表达式(return)是否结束的,只不过它恰巧包含了匿名内部类罢了😫...
  5. 当所在方法的形参需要被匿名内部类使用时,那么这个形参就必须为final
  6. 如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。🙄
  7. 匿名内部类是唯一一种没有构造器的类。
  8. 虽然匿名内部类没有构造器,但是要想匿名内部类起到与构造器一样的作用的话,可以使用实例初始化。只不过它是有局限的——你不能重载实例初始化方法,所以你仅有一个这样的“构造器”!

4. 静态内部类

所谓的静态内部类,就是使用static关键字修饰的成员内部类。

  1. 普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。(然鹅,当内部类是static时,就不是这样了😑。)
class Outer {
    int x = 1;
    static int y = 2;

    Outer() {
        System.out.println("I'm Outer. x = " + x);
    }

    static class Inner {
        Inner() {
            System.out.println("I'm static class "Inner"");
        }

        public void print_y() {
//            int z = x; // 2. 静态内部类不能使用外部类的非静态成员
            System.out.println("The static y: " + y);
        }
    }
}

public class Test {
    public static void main(String[] args) {
//        Outer.Inner inner = new Outer.new Inner(); // 1、3. 不能用 创建实例内部类对象的方式来创建静态内部类
        Outer.Inner inner = new Outer.Inner();
        inner.print_y();
    }
}
  1. 要创建静态内部类对象,并不需要其外部类对象
  2. 不能从静态内部类的对象中访问非静态的外部类对象
  3. 不能用创建实例内部类对象的方式来创建静态内部类。 (对应第 1 点)因为你并不需要对外部类对象的引用,不需要这种多余的操作。
  4. 静态内部类不依赖于外部类。 也就说,可以在不创建外部类对象的情况下创建内部类的对象
  5. 静态内部类是不持有指向外部类对象的引用的

二、探讨内部类的继承问题

内部类是很少用来作为继承用的。但是当用来继承的话,要注意:构造器中必须有指向外部类对象的引用,并通过这个引用调用super() 即调用外部类的构造器

0. 内部类的继承(介绍)

package InnerClassDemo;

class OuterClass {
    OuterClass() {
        System.out.println("1、我是无参构造器 —— OuterClass");
    }

    class InnerClass {
        InnerClass() {
            System.out.println("2、我是无参构造器 —— InnerClass");
        }

        InnerClass(OuterClass outer) {
            System.out.println("\t" + "这是我的外部类对象:" + outer);
        }
    }
}

public class Demo extends OuterClass.InnerClass {

//    Demo(){} // 编译不通过。 要在构造器里添加上外部类对象(所继承的内部类的外部类对象)的引用(因为不在同一个类下,内部类无法隐式的获取外部类对象的引用)

    Demo() {
        new OuterClass().super(); // 外部类对象的引用(父类对象的引用).super(); 这一句必须放在第一行!!!
        System.out.println("3、我是无参构造器 —— Demo");
    }

    Demo(OuterClass out) {
        out.super(); // 当子类与内部类父类不在同一个外部类下时,一定要使用 “外部类对象的引用.super()”,来构造外部类对象
        System.out.println("4、有外面的人进来了 —— " + out + "\n");
        System.out.println("Demo:有人陪我玩了!你好呀~" + "\n" + out + ": 你好");
    }

    public static void main(String[] args) {
        // PS: 注意构造链!
        System.out.println("执行d1主线开始:");
        Demo d1 = new Demo(); // 由于内部类对象好比如外部类对象的一个成员。所以外部类要先初始化出来

        System.out.println("\n" + "执行outer支线开始:");
        OuterClass outer = new OuterClass();

        System.out.println("\n" + "执行d2主线开始:");
        Demo d2 = new Demo(new OuterClass()); // 此处传OuterClass对象只是为了让程序好看点罢了。 传outer的话就不会再次初始化outer(毕竟已经初始化过了嘛)

        OuterClass.InnerClass inner = outer.new InnerClass(outer);
    }
}
Output:
        执行d1主线开始:
        1、我是无参构造器 —— OuterClass
        2、我是无参构造器 —— InnerClass
        3、我是无参构造器 —— Demo

        执行outer支线开始:
        1、我是无参构造器 —— OuterClass

        执行d2主线开始:
        1、我是无参构造器 —— OuterClass
        2、我是无参构造器 —— InnerClass
        4、有外面的人进来了 —— test.OuterClass@4554617c

        Demo:有人陪我玩了!你好呀~
        test.OuterClass@4554617c: 你好
                这是我的外部类对象:test.OuterClass@74a14482

PS:以上代码看不懂的话也没事,只要知道:子类的构造器中必须要有指向父类的外部类对象的引用 ,并通过这个引用调用super()构造器(有参、无参都可以)。


内部类的继承问题有三种形式。以下讨论的都是非静态的内部类! PS:注意public class 与 package 是否是与我的例子一致。不一致要修改成你的,或创成我的😘

1. 子类是内部类且与父类位于同一个外部类

package InnerClassDemo;

public class Demo2 {
    // 成员内部类 父类
    class InnerClassParent {
        InnerClassParent() {
            System.out.println("内部类 —— 父类");
        }
    }

    // 成员内部类 子类
    class InnerClassChild extends InnerClassParent {
        InnerClassChild() {
            System.out.println("内部类的子类是内部类且位于同一外部类");
        }
    }

    public static void main(String[] args) {
        Demo2 d = new Demo2();
        InnerClassChild innerChild = d.new InnerClassChild();
    }
}
Output:
        内部类 —— 父类
        内部类的子类是内部类且位于同一外部类

要创建子类对象,必须先加载父类再加载子类(这里子类、父类都是初次使用,尚未初始化)。然后初始化父类成员并调用父类构造方法,最后再初始化子类成员并调用子类的构造方法

2. 子类是内部类且与父类位于不同的外部类

package InnerClassDemo;

class InnerClassParent {
    class InnerClassChild {
        InnerClassChild(){
            System.out.println("内部类 —— 子类");
        }
    }
}

public class Demo3 {
    class InnerClassChildDemo extends InnerClassParent.InnerClassChild {
        InnerClassChildDemo(InnerClassParent per) {
            per.super(); // 必须使用
            System.out.println("成员内部类的子类为内部类, 且与父类不在同一个外部类");
        }
    }

    public static void main(String[] args) {
        Demo3 d = new Demo3();
        InnerClassParent innerParent = new InnerClassParent();
        InnerClassChildDemo innerChild = d.new InnerClassChildDemo(innerParent);
    }
}
Output:
        内部类 —— 子类
        成员内部类的子类为内部类, 且与父类不在同一个外部类

3. 子类不是内部类

package InnerClassDemo;

public class Demo4 {
    class InnerClassParent {
        InnerClassParent() {
            System.out.println("内部类 —— 父类");
        }
    }
}

class InnerClassChild extends Demo4.InnerClassParent {
    InnerClassChild(Demo4 d) {
        d.super(); // 必须使用
        System.out.println("内部类的子类不是内部类");
    }

    public static void main(String[] args) {
        Demo4 d = new Demo4();
        InnerClassChild innerChild = new InnerClassChild(d);
    }
}
Output:
        内部类 —— 父类
        内部类的子类不是内部类

小结:

  1. 成员内部类的非静态子类
    1. 可以是位于同一个外部类的子类
    2. 也可以是位于不同外部类的子类
    3. 还可以是一般类
  2. 当子类与内部类父类不在同一个外部类下时,一定要使用 “外部类对象的引用.super()”,来构造外部类对象

三、使用内部类的场景

  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;
    }
}
(1):
    Test test = new Test();    
    Test.Bean1 bean1 = test.new Bean1(); 

(2):
    Test.Bean2 b2 = new Test.Bean2();  

(3):
    Bean bean = new Bean();     
    Bean.Bean3 bean3 =  bean.new Bean3();  
  1. 下面这段代码的输出结果是?
public class Test01 {
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.new InnerClass().print();
    }
}

class OuterClass {
    private int a = 1;

    class InnerClass {
        private int a = 2;

        public void print() {
            int a = 3;
            System.out.println("局部变量:" + a); // 就近原则, 局部变量a
            System.out.println("内部类变量:" + this.a); // 调用InnerClass对象(this)的变量a
            System.out.println("外部类变量:" + OuterClass.this.a); // 调用OuterClass外部类对象(OuterClass.this)的变量a
        }
    }
}
Output:
        局部变量:3
        内部类变量:2
        外部类变量:1

五、Reference

  1. Java内部类详解 - Matrix海子 - 博客园
  2. Java学习笔记22---内部类之成员内部类的继承问题 - 蝉蝉 - 博客园
  3. java提高篇(十)-----详解匿名内部类 - chenssy - 博客园
  4. 内部类和匿名内部类的用法 - 业百科
  5. 什么是匿名内部类,如何使用匿名内部类_Weihaom_的博客-CSDN博客_匿名内部类
  6. Java编程思想 第4版
  7. Java语言程序设计与数据结构(基础篇) 原书第11版