系列中的上一篇
当前教程
嵌套类
系列中的下一篇

系列中的上一篇: 关于类的更多信息

系列中的下一篇: 枚举

嵌套类

 

嵌套类

Java 编程语言允许您在一个类中定义另一个类。这样的类称为嵌套类,这里进行了说明

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

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

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

嵌套类是其封闭类的成员。非静态嵌套类(内部类)可以访问封闭类的其他成员,即使它们被声明为private。静态嵌套类无法访问封闭类的其他成员。作为OuterClass的成员,嵌套类可以声明为privatepublicprotected或包私有。回想一下,外部类只能声明为public或包私有。

为什么要使用嵌套类?

使用嵌套类的令人信服的理由包括以下内容

  • 这是一种逻辑地对仅在一个地方使用的类进行分组的方式:如果一个类只对另一个类有用,那么将其嵌入该类中并将两者放在一起是合乎逻辑的。嵌套这种“辅助类”使它们的包更加简洁。
  • 它增强了封装:考虑两个顶级类AB,其中B需要访问A的成员,否则这些成员将被声明为私有。通过将类B隐藏在类A中,A的成员可以声明为private,而B可以访问它们。此外,B本身可以对外部世界隐藏。
  • 它可以使代码更易读和更易维护:将小型类嵌套在顶级类中,可以将代码放置在更靠近使用它的位置。

内部类

与实例方法和变量一样,内部类与封闭类的实例相关联,并可以直接访问该对象的​​方法和字段。此外,由于内部类与实例相关联,因此它本身不能定义任何静态成员。

内部类的实例对象存在于外部类的实例中。考虑以下类

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

InnerClass的实例只能存在于OuterClass的实例中,并且可以直接访问其封闭实例的方法和字段。

要实例化内部类,您必须首先实例化外部类。然后,使用以下语法在外部对象中创建内部对象

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

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

静态嵌套类

与类方法和变量一样,静态嵌套类与其外部类相关联。与静态类方法一样,静态嵌套类不能直接引用其封闭类中定义的实例变量或方法:它只能通过对象引用使用它们。内部类和嵌套静态类示例演示了这一点。

注意:静态嵌套类与外部类的实例成员(以及其他类)的交互方式与任何其他顶级类相同。实际上,静态嵌套类在行为上是顶级类,它已被嵌套在另一个顶级类中以方便打包。内部类和嵌套静态类示例也演示了这一点。

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

StaticNestedClass staticNestedObject = new StaticNestedClass();

内部类和嵌套静态类示例

以下示例OuterClass以及TopLevelClass演示了哪个OuterClass的类成员可以被内部类(InnerClass)、嵌套静态类(StaticNestedClass)和顶级类(TopLevelClass)访问

OuterClass.java

public class OuterClass {

    String outerField = "Outer field";
    static String staticOuterField = "Static outer field";

    class InnerClass {
        void accessMembers() {
            System.out.println(outerField);
            System.out.println(staticOuterField);
        }
    }

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

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

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

        System.out.println("\nTop-level class:");
        System.out.println("--------------------");
        TopLevelClass topLevelObject = new TopLevelClass();
        topLevelObject.accessMembers(outerObject);
    }
}

TopLevelClass.java

public class TopLevelClass {

    void accessMembers(OuterClass outer) {
        // Compiler error: Cannot make a static reference to the non-static
        //     field OuterClass.outerField
        // System.out.println(OuterClass.outerField);
        System.out.println(outer.outerField);
        System.out.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
       System.out.println(outerField);
    }
}

要解决此错误,请通过对象引用访问outerField

System.out.println(outer.outerField);

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

遮蔽

如果特定范围(例如内部类或方法定义)中的类型声明(例如成员变量或参数名称)与封闭范围中的另一个声明具有相同的名称,则该声明会遮蔽封闭范围的声明。您不能仅通过名称来引用被遮蔽的声明。以下示例ShadowTest演示了这一点

public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.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来表示封闭范围

System.out.println("this.x = " + this.x);

通过所属类的类名来引用封闭更大范围的成员变量。例如,以下语句从方法methodInFirstLevel()访问类ShadowTest的成员变量

System.out.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< Integer>接口。迭代器用于遍历数据结构,通常具有用于测试最后一个元素、检索当前元素和移动到下一个元素的方法。
  • 一个实例化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()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
    }

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

    // Inner class implements the DataStructureIterator interface,
    // which extends the Iterator<Integer> interface

    private class EvenIterator implements DataStructureIterator {

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

        public boolean hasNext() {

            // Check if the current element is the last in the array
            return (nextIndex <= SIZE - 1);
        }

        public Integer next() {

            // Record a value of an even index of the array
            Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);

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

    public static void main(String s[]) {

        // 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实例变量。

您可以使用内部类来实现辅助类,例如本示例中所示的类。要处理用户界面事件,您必须了解如何使用内部类,因为事件处理机制大量使用了它们。

局部类和匿名类

还有两种类型的内部类。您可以在方法体中声明内部类。这些类称为局部类。您也可以在方法体中声明内部类,而不命名该类。这些类称为匿名类。

修饰符

您可以对内部类使用与对外部类的其他成员相同的修饰符。例如,您可以使用访问说明符privatepublicprotected来限制对内部类的访问,就像您使用它们来限制对其他类成员的访问一样。

 

局部类

局部类是在块中定义的类,块是一组在平衡括号之间的零个或多个语句。您通常会在方法体中找到定义的局部类。

本节涵盖以下主题

  • 声明局部类
  • 访问封闭类的成员
  • 遮蔽和局部类
  • 局部类类似于内部类

声明局部类

您可以在任何块中定义局部类(有关详细信息,请参阅表达式、语句和块)。例如,您可以在方法体、for 循环或 if 子句中定义局部类。

以下示例 LocalClassExample 验证了两个电话号码。它在 validatePhoneNumber() 方法中定义了局部类 PhoneNumber

public class LocalClassExample {

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

    public static void validatePhoneNumber(
        String phoneNumber1, String phoneNumber2) {

        final int numberLength = 10;

        // Valid in JDK 8 and later:

        // 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;
            }

            // Valid in JDK 8 and later:

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

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

        // Valid in JDK 8 and later:

//        myNumber1.printOriginalNumbers();

        if (myNumber1.getNumber() == null)
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.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实际最终的局部变量和参数。从未更改其值(在初始化后)的变量或参数是实际最终的。例如,假设变量 numberLength 未声明为 final,并且您在 PhoneNumber() 构造函数中添加了突出显示的赋值语句以将有效电话号码的长度更改为 7 位数字

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

由于此赋值语句,变量 numberLength 不再是实际最终的。因此,Java 编译器会生成类似于“从内部类引用的局部变量必须是 final 或实际最终的”的错误消息,其中内部类 PhoneNumber 尝试访问 numberLength 变量

if (currentNumber.length() == numberLength)

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

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

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

局部类中的类型声明(例如变量)会遮蔽封闭作用域中具有相同名称的声明。有关更多信息,请参见遮蔽。

局部类类似于内部类

局部类类似于内部类,因为它们不能定义或声明任何静态成员。静态方法中的局部类(例如在静态方法 validatePhoneNumber() 中定义的类 PhoneNumber)只能引用封闭类的静态成员。例如,如果未将成员变量 regularExpression 定义为 static,则 Java 编译器会生成类似于“非静态变量 regularExpression 不能从静态上下文中引用”的错误。

局部类是非静态的,因为它们可以访问封闭块的实例成员。因此,它们不能包含大多数类型的静态声明。

您不能在块内声明接口;接口本质上是静态的。例如,以下代码片段无法编译,因为接口 HelloTheregreetInEnglish() 方法的主体内部定义

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

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

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static void sayGoodbye() {
            System.out.println("Bye bye");
        }
    }
    EnglishGoodbye.sayGoodbye();
}

局部类可以具有静态成员,前提是它们是常量变量。(常量变量是声明为 final 并使用编译时常量表达式初始化的原始类型或 String 类型的变量。编译时常量表达式通常是字符串或可以在编译时计算的算术表达式。有关更多信息,请参见了解类成员。)以下代码片段可以编译,因为静态成员 EnglishGoodbye.farewell 是一个常量变量

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static final String farewell = "Bye bye";
        public void sayGoodbye() {
            System.out.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;
                System.out.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;
                System.out.println("Salut " + name);
            }
        };

        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.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;
        System.out.println("Salut " + name);
    }
};

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

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

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

与局部类一样,匿名类可以捕获变量;它们对封闭作用域的局部变量具有相同的访问权限

  • 匿名类可以访问其封闭类的成员。
  • 匿名类不能访问其封闭作用域中未声明为 final 或实际最终的局部变量。
  • 与嵌套类一样,匿名类中的类型声明(例如变量)会遮蔽封闭作用域中具有相同名称的任何其他声明。有关更多信息,请参见遮蔽。

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

  • 您不能在匿名类中声明静态初始化器或成员接口。
  • 匿名类可以具有静态成员,前提是它们是常量变量。

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

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

但是,您不能在匿名类中声明构造函数。


上次更新: 2021 年 9 月 23 日


系列中的上一篇
当前教程
嵌套类
系列中的下一篇

系列中的上一篇: 关于类的更多信息

系列中的下一篇: 枚举