嵌套类

Java 编程语言允许你在另一个类内部定义类。此类称为嵌套类,示例如下:

class OuterClass {
    ...
    class NestedClass {
        ...
    }
}

术语:嵌套类分为两类:非静态类和静态类。非静态嵌套类称为内部类。声明为静态的嵌套类称为静态嵌套类。

class OuterClass {
    ...
    class InnerClass {
        ...
    }
    static class StaticNestedClass {
        ...
    }
}

嵌套类是其外围类的成员。非静态嵌套类(内部类)可以访问外部类的其他成员,即使这些成员声明为私有。静态嵌套类则无法访问外部类的其他成员。作为外部类的成员,嵌套类可以声明为 private, public, protectedpackage private 。需注意外部类只能声明为 publicpackage private

为什么要使用嵌套类?

使用嵌套类的充分理由包括以下几点:

  • 这是一种将仅在单一位置使用的类进行逻辑分组的方法:若某个类仅对另一个类有用,则将其嵌入该类并保持二者关联是合理的。通过嵌套此类”辅助类”,可使相关包结构更为精简。
  • 它增强了封装性:考虑两个顶级类A和B,其中B需要访问A的成员,而这些成员原本会被声明为私有。通过将类B隐藏在类A内部,A的成员可以声明为私有,而B仍能访问它们。此外,B本身也可以对外部世界隐藏。
  • 这能使代码更易于阅读和维护:将小型类嵌套在顶级类中,使代码更接近其使用位置。

内部类

与实例方法和变量类似,内部类与其外围类的实例相关联,可直接访问该对象的方法和字段。此外,由于内部类与实例相关联,它本身无法定义任何静态成员。
内部类的实例对象存在于外部类的实例中。请考虑以下类:

class OuterClass {
    ...
    class InnerClass {
        ...
    }
}

内部类的实例只能存在于外部类的实例内部,并能直接访问其包含实例的方法和字段。
要实例化一个内部类,必须先实例化其外部类。然后,使用以下语法在外部对象内部创建内部对象:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

内部类有两种特殊类型:局部类和匿名类。

静态嵌套类

与类方法和变量类似,静态嵌套类与其外部类相关联。同样地,静态嵌套类无法直接引用其外部类中定义的实例变量或方法:它只能通过对象引用来使用这些成员。内部类与嵌套静态类的示例对此进行了说明。

注意:静态嵌套类与外部类(及其他类)的实例成员交互的方式,与任何其他顶级类完全相同。实际上,静态嵌套类在行为上就是一个顶级类,为了封装便利而嵌套在另一个顶级类中。《内部类与嵌套静态类示例》也展示了这一点。

静态嵌套类的实例化方式与顶级类相同:

StaticNestedClass staticNestedObject = new StaticNestedClass();

内部类和嵌套静态类示例

以下示例通过 OuterClassTopLevelClass 演示了内部类(InnerClass)、嵌套静态类(StaticNestedClass)和顶级类(TopLevelClass)能够访问 OuterClass 的哪些类成员:

OuterClass.java

public class OuterClass {
    String outerField = "Outer field";
    static String staticOuterField = "Static outer field";

    //内部类和外部类的实例相同,可以访问外部类的所有成员
    class InnerClass {
        void accessMembers() {
            IO.print(outerField);
            IO.println(staticOuterField);
        }
    }

    // 静态嵌套类,不能直接访问外部类的非静态成员
    static class StaticNestedClass {
        void accessMembers(OuterClass outer) {
            // 编译错误,无法直接访问外部类的非静态成员
            //IO.println(outerField);
            IO.println(outer.outerField);
            IO.println(staticOuterField);
        }
    }

    public static void main(String[] args) {
        IO.println("Inner class:");
        IO.println("------------");
        OuterClass outerObject = new OuterClass();
        OuterClass.InnerClass innerObject = outerObject.new InnerClass();
        innerObject.accessMembers();

        IO.println("\nStatic nested class:");
        IO.println("----------------------");
        StaticNestedClass staticNestedObject = new StaticNestedClass();
        staticNestedObject.accessMembers(outerObject);

        IO.println("\nTop-level class:");
        IO.println("------------------");

        TopLevelClass topLevelClass = new TopLevelClass();
        topLevelClass.accessMembers(outerObject);
    }
}

TopLevelClass.java

public class TopLevelClass {
    void accessMembers(OuterClass outer) {
        IO.println(outer.outerField);
        IO.println(OuterClass.staticOuterField);
    }
}

这个例子打印如下:

Inner class:
------------
Outer field
Static outer field

Static nested class:
----------------------
Outer field
Static outer field

Top-level class:
------------------
Outer field
Static outer field

请注意,静态嵌套类与外部类的实例成员交互的方式与任何其他顶级类相同。静态嵌套类 StaticNestedClass 无法直接访问 outerField,因为它是外部类 OuterClass 的实例变量。Java 编译器会在高亮语句处生成错误:

static class StaticNestedClass {
    void accessMembers(OuterClass outer) {
       // Compiler error: Cannot make a static reference to the non-static
       //     field outerField
       IO.println(outerField);
    }
}

要修复此错误,请通过对象引用访问 outerField

IO.println(outer.outerField);

同样地,顶级类 TopLevelClass 也无法直接访问 outerField

Shadowing(覆盖?)

Shadowing 翻译成什么比较合适?变量遮蔽?变量屏蔽?
若某类型声明(如成员变量或参数名)在特定作用域(如内部类或方法定义)中与外部作用域的声明同名,则该声明将遮蔽外部作用域的声明。仅凭名称无法直接引用被遮蔽的声明。以下示例 ShadowTest 演示了此特性:

public class ShadowTest {
    public int x = 0;

    class FirstLevel {
        public int x = 1;
        void methodInFirstLevel(int x) {
            IO.println("x = " + x);
            IO.println("this.x = " + this.x);
            IO.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String[] args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

程序输出如下:

x = 23
this.x = 1
ShadowTest.this.x = 0

此示例定义了三个名为 x 的变量:ShadowTest 类的成员变量、内部类 FirstLevel 的成员变量,以及 methodInFirstLevel() 方法中的参数。作为 methodInFirstLevel() 方法参数定义的变量 x 会遮蔽内部类 FirstLevel 中的变量。因此,在 methodInFirstLevel() 方法中使用变量 x 时,它将指向方法参数。若要引用内部类 FirstLevel 的成员变量,需使用 this 关键字表示外部作用域:

IO.println("this.x = " + this.x);

访问包含更大作用域的成员变量时,需通过其所属的类名进行引用。例如,以下语句从方法 methodInFirstLevel() 中访问类 ShadowTest 的成员变量:

IO.println("ShadowTest.this.x = " + ShadowTest.this.x);

序列化

强烈不建议序列化内部类,包括局部类和匿名类。当Java编译器编译某些构造(如内部类)时,会生成合成构造;这些构造包括类、方法、字段及其他在源代码中没有对应构造的元素。合成构造使Java编译器能够在不修改JVM的情况下实现新的Java语言特性。然而,不同Java编译器实现的合成结构可能存在差异,这意味着.class文件在不同实现中也可能不同。因此,若将内部类序列化后再用其他JRE实现反序列化,可能会引发兼容性问题。

内部类的例子

要了解内部类的用法,首先考虑数组。在下面的示例中,你创建一个数组,用整数值填充它,然后按升序输出数组中偶数索引的值。
以下示例 DataStructure.java 包含:

  • DataStructure 外层类包含一个构造函数,用于创建一个包含连续整数值数组(0、1、2、3 等)的 DataStructure 实例,以及一个打印数组中偶数索引值元素的方法。
  • EvenIterator 内部类实现了 DataStructureIterator 接口,该接口继承自 Iterator 接口。迭代器用于遍历数据结构,通常包含用于检测最后一个元素、获取当前元素以及移至下一个元素的方法。
  • 一个主要方法,该方法实例化一个 DataStructure 对象(ds),然后调用 printEven() 方法来打印数组 arrayOfInts 中具有偶数索引值的元素。
public class DataStructure {

    // Create an array
    private final static int SIZE = 15;
    private int[] arrayOfInts = new int[SIZE];

    public DataStructure() {
        // Fill the array with ascending integer values
        for (int i = 0; i < SIZE; i++) {
            arrayOfInts[i] = i;
        }
    }

    public void printEven() {
        // Print out values of even indices of the array
        DataStructureIterator iterator = this.new EvenIterator();
        while (iterator.hasNext()) {
            IO.print(iterator.next() + " ");
        }
        IO.println();
    }

    interface DataStructureIterator extends java.util.Iterator<Integer> {}

    public class EvenIterator implements DataStructureIterator {

        // Start stepping through the array from the beginning
        private int nextIndex = 0;

        @Override
        public boolean hasNext() {
            // Check if the current element is the last in the array
            return (nextIndex <= SIZE - 1);
        }

        @Override
        public Integer next() {
            // Record a value of an even index of the array
            Integer retValue = arrayOfInts[nextIndex];

            // Get the next even element
            nextIndex += 2;
            return retValue;
        }
    }

    public static void main(String[] args) {

        // Fill the array with integer values and print out only values of even indices
        DataStructure ds = new DataStructure();
        ds.printEven();
    }
}

输出如下:

0 2 4 6 8 10 12 14

请注意,EvenIterator 类直接引用了 DataStructure 对象的 arrayOfInts 实例变量。
你可以使用内部类来实现辅助类,例如本示例中所示的类。要处理用户界面事件,你必须掌握内部类的使用方法,因为事件处理机制大量依赖于它们。

局部和匿名类

还有两种额外的内部类类型。你可以在方法体内声明内部类,这类类称为局部类。你也可以在方法体内声明不命名类名的内部类,这类类称为匿名类。

修饰符

对于内部类,你可以使用与外部类其他成员相同的修饰符。例如,你可以使用访问限定符 private、publicprotected 来限制对内部类的访问,就像你使用它们来限制对其他类成员的访问一样。

Local Classes(局部类)

局部类是在代码块中定义的类,代码块是指由大括号括起的零个或多个语句组成的集合。通常在方法主体中会定义局部类。

本节涵盖以下主题:

  • 局部类的定义
  • 访问外围类的成员
  • 局部类的屏蔽
  • 局部类与内部类的相似之处

声明一个局部类

您可以在任何代码块内定义局部类(更多信息请参阅表达式、语句和代码块)。例如,您可以在方法主体、for循环或if子句中定义局部类。
以下示例 LocalClassExample 用于验证两个电话号码。它在 validatePhoneNumber() 方法中定义了本地类 PhoneNumber

public class LocalClassDemo {

    static String regularExpression = "[^0-9]";

    public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
        final int numberLength = 10;

        class PhoneNumber {

            String formattedPhoneNumber = null;

            PhoneNumber(String phoneNumber) {
                // numberLength = 7;
                String currentNumber = phoneNumber.replaceAll(
                        regularExpression, "");
                if (currentNumber.length() == numberLength) {
                    formattedPhoneNumber = currentNumber;
                } else {
                    formattedPhoneNumber = null;
                }
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }
        }

        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);

        if (myNumber1.getNumber() == null) {
            IO.println("First number is invalid");
        } else {
            IO.println("First number is " + myNumber1.getNumber());
        }
        if (myNumber2.getNumber() == null) {
            IO.println("Second number is invalid");
        } else {
            IO.println("Second number is " + myNumber2.getNumber());
        }
    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }
}

该示例通过以下步骤验证电话号码:首先移除电话号码中除0至9数字外的所有字符,随后检查电话号码是否恰好包含十位数字(北美电话号码的长度)。该示例输出如下内容:

First number is 1234567890
Second number is invalid

访问外围类的成员

局部类可以访问其外围类的成员。在上例中,PhoneNumber() 构造函数访问了成员 LocalClassExample.regularExpression
此外,局部类可以访问局部变量。但局部类仅能访问声明为 final 的局部变量。当局部类访问其外围代码块的局部变量或参数时,会捕获该变量或参数。例如,PhoneNumber() 构造函数能访问局部变量 numberLength,因为该变量声明为 finalnumberLength 即为被捕获的变量。

然而,从 Java SE 8 开始,局部类可以访问其外围代码块中具有 final 或实质上 final 属性的局部变量和参数。初始化后值永不改变的变量或参数即具有实质上 final 的属性。例如,假设变量 numberLength 未声明为 final,若在 PhoneNumber() 构造函数中添加高亮显示的赋值语句,将有效电话号码的长度改为 7 位:

PhoneNumber(String phoneNumber) {
    numberLength = 7;
    String currentNumber = phoneNumber.replaceAll(
        regularExpression, "");
    if (currentNumber.length() == numberLength)
        formattedPhoneNumber = currentNumber;
    else
        formattedPhoneNumber = null;
}

由于这个赋值语句,变量 numberLength 实际上不再是 final 类型。因此,当内部类 PhoneNumber 尝试访问 numberLength 变量时,Java 编译器会生成类似于“从内部类引用的局部变量必须是 final 或实际上是 final”的错误信息:

if (currentNumber.length() == numberLength)

从 Java SE 8 开始,若在方法中声明局部类,该局部类即可访问方法的参数。例如,您可以在 PhoneNumber 局部类中定义以下方法:

public void printOriginalNumbers() {
    IO.println("Original numbers are " + phoneNumber1 +
        " and " + phoneNumber2);
}

方法 printOriginalNumbers() 访问了方法 validatePhoneNumber() 的参数 phoneNumber1phoneNumber2

局部类中声明的类型(如变量)会遮蔽外部作用域中同名的声明。更多信息请参阅遮蔽机制。

局部类与内部类的相似之处

局部类与内部类类似,因为它们无法定义或声明任何静态成员。在静态方法中定义的局部类(例如在 validatePhoneNumber() 静态方法中定义的 PhoneNumber 类)只能引用其外围类的静态成员。例如,若未将成员变量 regularExpression 声明为静态,Java 编译器将报错,提示类似于“非静态变量 regularExpression 无法从静态上下文中引用”。
局部类是非静态的,因为它们能够访问包含它们的代码块的实例成员。因此,它们不能包含大多数类型的静态声明。
你不能在代码块内部声明接口;接口本质上是静态的。例如,以下代码片段无法编译,因为接口 HelloThere 被定义在方法 greetInEnglish() 的主体内部:

public void greetInEnglish() {
    interface HelloThere {
       public void greet();
    }
    class EnglishHelloThere implements HelloThere {
        public void greet() {
            IO.println("Hello " + name);
        }
    }
    HelloThere myGreeting = new EnglishHelloThere();
    myGreeting.greet();
}

在局部类中不能声明静态初始化器或成员接口。以下代码片段无法编译,因为方法 EnglishGoodbye.sayGoodbye() 被声明为静态。当编译器遇到该方法定义时,会生成类似于”修饰符 static 仅允许用于常量变量声明”的错误:

本地类可以包含静态成员,前提是这些成员必须是常量变量。(常量变量是指声明为 final 并使用编译时常量表达式初始化的基本类型或 String 类型的变量。编译时常量表达式通常是字符串或可在编译时求值的算术表达式。更多信息请参阅《理解类成员》。)以下代码片段能够编译,因为静态成员 EnglishGoodbye.farewell 是常量变量:

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static final String farewell = "Bye bye";
        public void sayGoodbye() {
            IO.println(farewell);
        }
    }
    EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
    myEnglishGoodbye.sayGoodbye();
}

局部类的使用频率远低于静态内部类和匿名内部类。

匿名类

匿名类能让你的代码更简洁。它们允许你在声明类的同时直接实例化该类。它们类似于局部类,但没有名称。当你需要仅使用局部类一次时,可以采用这种方式。

声明匿名类

局部类是类声明,而匿名类则是表达式,这意味着你需要在另一个表达式中定义该类。以下示例 HelloWorldAnonymousClasses 在局部变量 frenchGreetingspanishGreeting 的初始化语句中使用了匿名类,但变量 englishGreeting 的初始化则使用了局部类:

public class HelloWorldAnonymousClasses {

    interface HelloWorld {

        public void greet();

        public void greetSomeone(String someone);
    }

    public void sayHello() {

        class EnglishGreeting implements HelloWorld {

            String name = "world";

            public void greet() {
                greetSomeone("world");
            }

            public void greetSomeone(String someone) {
                name = someone;
                IO.println("Hello " + name);
            }
        }

        HelloWorld englishGreeting = new EnglishGreeting();
        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";

            public void greet() {
                greetSomeone("tout le monde");
            }

            public void greetSomeone(String someone) {
                name = someone;
                IO.println("Salut " + name);
            }
        };
        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";

            public void greet() {
                greetSomeone("mundo");
            }

            public void greetSomeone(String someone) {
                name = someone;
                IO.println("Hola " + name);
            }
        };

        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String[] args) {
        HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }
}

如前所述,匿名类是一种表达式。匿名类表达式的语法类似于构造函数的调用,区别在于其中包含一段代码块定义的类。
考虑 frenchGreeting 对象的实例化过程:

HelloWorld frenchGreeting = new HelloWorld() {
    String name = "tout le monde";
    public void greet() {
        greetSomeone("tout le monde");
    }
    public void greetSomeone(String someone) {
        name = someone;
        IO.println("Salut " + name);
    }
};

匿名类表达式由以下部分组成:

  • new 操作符
  • 要实现的接口名称或要扩展的类名称。在此示例中,匿名类实现了接口 HelloWorld
  • 括号中包含构造函数的参数,就像普通的类实例创建表达式一样。注意:当你实现接口时,没有构造函数,因此使用一对空括号,如本例所示。
  • 主体,即类声明主体。更具体地说,在主体中允许方法声明,但不允许语句。
  • 由于匿名类定义是一个表达式,它必须作为语句的一部分存在。在此示例中,匿名类表达式是实例化 frenchGreeting 对象的语句组成部分。(这解释了为何闭合大括号后需要分号。)

访问外围作用域的局部变量,以及声明和访问匿名类的成员

与局部类类似,匿名类可以捕获变量;它们对包含作用域的局部变量具有相同的访问权限:

  • 匿名类可以访问其外围类的成员。
  • 匿名类无法访问其外围作用域中未声明为final或实质上为final的局部变量。
  • 与嵌套类类似,在匿名类中声明的类型(如变量)会遮蔽外部作用域中所有同名声明。更多信息请参阅遮蔽机制。

匿名类在其成员方面也与局部类具有相同的限制:

  • 在匿名类中不能声明静态初始化器或成员接口。
  • 匿名类可以包含静态成员,前提是这些成员必须是常量变量。

请注意,您可以在匿名类中声明以下内容:

  • 字段
  • 额外方法(即使它们未实现任何超类的成员方法)
  • 实例初始化器
  • 局部类

然而,你无法在匿名类中声明构造函数。