学而实习之 不亦乐乎

Java中的内部类

2020-08-16 11:12:21

一、什么是内部类?

大部分的情况下,类被定义成一个独立的程序单元。在 Java 中,有时候也会把一个类放到另一个类的内部定义,这种定义在其他类内部的类就是内部类。

内部类的作用:
1.更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问。因为有时内部类,对其他外部类来说没有意义,也就没有必要暴露出来。
2.内部类成员可以访问当前外部类的私有数据,因为内部类被当成外部类的成员。但外部类不能访问内部类的实现细节(如成员变量等)。
3.匿名内部类适合用于创建那些仅需要一次使用的类。

内部类与外部类的别:
1.内部类需要定义在外部类之内。
2.内部类比外部类可以多使用三个修饰符:private/protected/static(外部类不可使用这三个修饰符)。
3.非静态内部类不能拥有静态成员。

二、非静态内部类

1.内部类的定义

class Outer {                         // 外部类
    private String msg = "Hello World !";
    class Inner {                     // 定义一个内部类
        public void print() {
            System.out.println(msg);
        }
    }
    public void fun() {
        new Inner().print();            // 实例化内部类对象,并且调用print()方法
    }
}

注意:
1.大部分时候,内部类都被作为成员内部类定义,而不是做为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
2.一个Java文件中定义了多个类,如下:
class A {}
public class B {}
虽然两个类写在同一个文件中,但他们不是内部类,他们依然是两个相互独立的类。

2.程序编译

编译程序,会发现生成两个.class 文件,一个是Outer.class,另一个是Outer$Inner.class。

3.静态成员与非静态成员

根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类。包括不能使用非静态内部类定义变量、实例等。总之不允许在外部类的静态成员中直接使用非静态内部类。
非静态内部类里不能有静态方法、静态成员变量、静态初始化块。

三、静态内部类

如果使用static 修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此被static修饰的内部类被称为类内部类或者静态内部类。

1.静态内部类访问外部类静态成员

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

public class StaticInnerClassTest
{
    private int prop1 = 5;
    private static int prop2 = 9;
    static class StaticInnerClass
    {
        // 静态内部类里可以包含静态成员
        private static int age;
        public void accessOuterProp()
        {
            // 下面代码出现错误:
            // 静态内部类无法访问外部类的实例变量
            // System.out.println(prop1);
            // 下面代码正常
            System.out.println(prop2);
        }
    }
}

2.外部类访问静态内部类成员

虽然外部类不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象做为调用者来访问静态内部类的实例成员。如下代码:

public class AccessStaticInnerClass
{
    static class StaticInnerClass
    {
        private static int prop1 = 5;
        private int prop2 = 9;
    }
    public void accessInnerProp()
    {
        System.out.println(StaticInnerClass.prop1);
        // 下面代码错误,因为应该通过类名访问静态内部类的类成员
        // System.out.println(prop1);
         
        System.out.println(new StaticInnerClass().prop2);
        // 下面代码出现错误,应该通过实例访问静态内部类的实例成员
        // System.out.println(prop2);         
    }
}

3,接口中定义内部类

Java允许在接口中定义内部类,接口里定义的内部类默认使用public static 修饰,也就是说,接口内部类只能是静态内部类。
如果接口内部类指定访问修饰符,则只能指定public 修饰符;如果省略,则默认是public 访问修饰符。

延伸:接口可以定义内部接口,接口的内部接口是接口的成员,同接口的内部类,系统默认添加 public static 修饰。若制定访问修饰符,则只能使用 public 。如:Map 接口中的内部接口 Entry 。

四、使用内部类

1.在外部类使用内部类

用法与普通类类似,区别是:不要在外部类的静态成员中(静态方法和初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员(参考三)。

2.在外部类以外使用非静态内部类

如果要在外部类以外访问内部类(包括静态和非静态),则内部类不能用 private 访问修饰符。
a.省略访问修饰符,只能被外部类和外部类处于同一个包中的其他类所访问。
b.使用 protected 修饰符,可被外部类、外部类的子类和外部类处于同一个包中的其他类所访问。
c.使用 public 修饰符,可在任何地方被访问。

使用方法:

内部类对象的实例化
OuterClass.InnerClass obj = new OuterClass().new InnerClass();
OuterClass.InnerClass1.InnerClass2 obj = new OuterClass().new InnerClass1().new InnerClass2();

当创建一个子类时,子类构造器会调用父类构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器,必须存在一个外部类对象。如:

public class SubClass extends Out.In
{
    //显示定义SubClass的构造器
    public SubClass(Out out)
    {
        //通过传入的Out对象显式调用In的构造器
        out.super("hello");
    }
}

这虽然看起来有点怪,但其实是正常的:非静态内部类 In 的构造器必须使用外部类对象来调用,代码中的 super 代表调用 In 的构造器,而 Out 则代表外部类对象。
因为要创建SubClass对象时,需要先创建一个 Out 对象,非静态内部类 In 必须有一个 Out 对象的引用,SubClass 对象也应该持有对 Out 对象的引用。

非静态内部类 In 对象和 SubClass 对象都必须持有指向 Out 对象的引用。区别是创建两种对象时传入 Out 对象的方式不同:创建非静态内部类 In 时,必须通过 Out 对象调用 new 关键字;当创建 SubClass 对象时,必须使用 Out 对象做为调用者来调用 In 类的构造器。

注意:非静态内部类的子类不一定是内部类,它可以是一个外部类,但它的实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果一个内部类子类的对象存在,则一定存在与之对应的外部类对象。

3.在外部类以外使用静态内部类

因为静态内部类与外部类是类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外创建静态内部类实例的语法如下:
new OuterClass.InnerConstructor()

示例:

class StaticOut
{
    // 定义一个静态内部类,不使用访问控制符,
    // 即同一个包中其他类可访问该内部类
    static class StaticIn
    {
        public StaticIn()
        {
            System.out.println("静态内部类的构造器");
        }
    }
}
public class CreateStaticInnerInstance
{
    public static void main(String[] args)
    {
        StaticOut.StaticIn in = new StaticOut.StaticIn();
        /*
        上面代码可改为如下两行代码:
        使用OuterClass.InnerClass的形式定义内部类变量
        StaticOut.StaticIn in;
        通过new来调用内部类构造器创建静态内部类实例
        in = new StaticOut.StaticIn();
        */
    }
}

由示例可以看出,不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。

因为调用静态内部类的构造器时无须使用外部类对象,所以创建静态内部类的子类也比较简单,如下代码:
public class StaticSubClass extends StaticOut.StaticIn {}

注意:
(1)相比之下,使用静态内部类比使用非静态内部类要简单的多,只要把外部类当成静态内部类的包空间即可。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。
(2)既然内部类是外部类的成员,那么是否可以为外部类定义子类,在子类中再定义一个内部类来重写其父类中的内部类呢?不可以,内部类的类名不再是简单地由内部类的类名组成,它实际上还把外部类的类名作为一个命名空间,作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,即使二者所包含的内部的类名相同,但因为他们所处的外部类空间不同,所以他们不可能完全同名,也就不可能重写。

五、局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类只在该方法里有效。由于局部内部类不能在外部类以外的地方使用,因此局部内部类也不能使用访问修饰符和static修饰符。

注意:对于局部类而言,不管是局部变量还是局部内部类,它们的上一级程序单元都是方法,而不是类,因此使用修饰符没有意义。

示例:

public class LocalInnerClass
{
    public static void main(String[] args)
    {
        // 定义局部内部类
        class InnerBase
        {
            int a;
        }
        // 定义局部内部类的子类
        class InnerSub extends InnerBase
        {
            int b;
        }
        // 创建局部内部类的对象
        var is = new InnerSub();
        is.a = 5;
        is.b = 8;
        System.out.println("InnerSub对象的a和b实例变量是:"
            + is.a + "," + is.b);
    }
}

编译程序,生成了三个 .class 文件:LocalInnerClass.class、LocalInnerClass$1InnerBase.class、LocalInnerClass$1InnerSub.class,说明内部类的 class 文件总是遵循如下命名格式:OuterClass$NInnerClass.class。

注意:
(1)局部内部类的 class 文件名比成员内部类的 class 文件的文件名多了一个数字,这是因为一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同的方法中),所以Java为局部内部类的 class 文件名中增加了一个数字用于区分。
(2)局部内部类是个“鸡肋”,很少用。

六、匿名内部类

匿名内部类适合用于创建那些仅需要一次使用的类。
匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
匿名内部类的定义格式如下:

new 实现接口() | 父类构造器(实参列表)
{
    //匿名内部类的类体部分
}

注意:
(1)匿名内部类不能是抽象类,创建匿名内部类时会立即创建一个该类的实例。
(2)匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但可以定义初始化块,可以通过初始化块来完成构造器需要完成的工作。但同时,如果匿名内部类通过继承父类来创建时,匿名内部类将拥有和父类相似的构造器(即拥有相同形参)。

示例一:

interface Product
{
    double getPrice();
    String getName();
}
public class AnonymousTest
{
    public void test(Product p)
    {
        System.out.println("购买了一个" + p.getName()
            + ",花掉了" + p.getPrice());
    }
    public static void main(String[] args)
    {
        var ta = new AnonymousTest();
        // 调用test()方法时,需要传入一个Product参数,
        // 此处传入其匿名实现类的实例
        ta.test(new Product()
        {
            public double getPrice()
            {
                return 567.8;
            }
            public String getName()
            {
                return "AGP显卡";
            }
        });
    }
}

示例二:

abstract class Device
{
    private String name;
    public abstract double getPrice();
    public Device(){}
    public Device(String name)
    {
        this.name = name;
    }
    // 此处省略了name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }
}
public class AnonymousInner
{
    public void test(Device d)
    {
        System.out.println("购买了一个" + d.getName()
            + ",花掉了" + d.getPrice());
    }
    public static void main(String[] args)
    {
        var ai = new AnonymousInner();
        // 调用有参数的构造器创建Device匿名实现类的对象
        ai.test(new Device("电子示波器")
        {
            public double getPrice()
            {
                return 67.8;
            }
        });
        // 调用无参数的构造器创建Device匿名实现类的对象
        var d = new Device()
        {
            // 初始化块
            {
                System.out.println("匿名内部类的初始化块...");
            }
            // 实现抽象方法
            public double getPrice()
            {
                return 56.2;
            }
            // 重写父类的实例方法
            public String getName()
            {
                return "键盘";
            }
        };
        ai.test(d);
    }
}

正如上面的程序,定义匿名内部类无须class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。由于匿名内部类不能是抽象类,所以必须实现它的抽象父类或者接口里包含的所有抽象方法。

注意:
Java8 之前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用 final 修饰,Java8 之后,变得更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final 修饰符。