一、嵌套类

Java编程语言允许在一个类里面定义类,这样的类被成为嵌套类,例如:

  1. class OuterClass {
  2. ...
  3. class NestedClass {
  4. ...
  5. }
  6. }
  1. 嵌套类被划分为两个类型,非静态和静态。非静态的嵌套类被成为内部类,被 `static`修饰的嵌套类为静态嵌套类。
  1. class OuterClass {
  2. ...
  3. class InnerClass {
  4. ...
  5. }
  6. static class StaticNestedClass {
  7. ...
  8. }
  9. }

嵌套类是其封闭类的成员。非静态的嵌套类(内部类)可以访问其封闭类的其他成员,尽管有些被声明为 private类型;静态嵌套类不能访问其封闭类的其他变量。作为外部类的一个成员,嵌套类可以被声明为 privatepublicprotected 或者 package private (回想外部类只能被声明为public 或者 package private。)

为什么要使用嵌套类?

使用嵌套类的原因如下所示:

  • 它是一种逻辑地将只在一个地方使用的类分组的方法。如果一个类只是作用于一个另外的类,就可以将逻辑键入到它里面使得两个类在一起。嵌套这样的“助手类”使它们的包更加流线型。
  • 提升封闭性。想象一下两个顶层类A和B,类B需要访问类A的变量只能声明为除了private之外的类型。通过将类B隐藏到类A里面,类A的变量可以声明为private,并且类B可以访问它们。
  • 它可以产生更有可读性和维护性的代码。把嵌套小类放在顶层的类的空间里,可以让代码更加靠近使用的地方。

    内部类

    与实例方法和变量一样,内部类和其封闭类的实例相关联,并且能够直接地访问对象的方法和字段。同时,因为内部类是与实例相关联的,所以它本身不能定义任何的静态成员。
    内部类的实例对象存在于外部类的实例中。请上例子:

    1. class OuterClass {
    2. ...
    3. class InnerClass {
    4. ...
    5. }
    6. }

    内部类的实例只能存在于外部类的实例当中,并且可以直接访问它的封闭类实例的方法和字段。
    为了实例化一个内部类,首先就要实例化外部类,然后使用

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

    来在外部类对象里面创建内部类对象。
    内部类有两个特殊类型:本地类和匿名类。

    静态嵌套类

    与类方法和变量一样,静态嵌套类和它的外部类相关联。和静态类方法一样,静态嵌套类不能直接引用它的封闭类定义的实例变量和方法,它只能通过对象引用来使用它们。
    静态嵌套类与外部类(和其他类)的实例成员进行交互,就像任何其他顶级类一样,实际上,静态嵌套类在行为上是一个顶级类,为了方便打包,它被嵌套在另一个顶级类中。
    静态嵌套类的实例化方法与顶级类相同。
    StaticNestedClass staticNestedObject = new StaticNestedClass();

    内部类和嵌套静态类的例子

    接下来的OuterClass 连同 TopLevelClass一起说明了OuterClass的哪些类成员可以访问内部类( InnerClass )、嵌套静态类( StaticNestedClass )和顶级类( TopLevelClass )

    1. public class OuterClass {
    2. String outerField = "Outer field";
    3. static String staticOuterField = "Static outer field";
    4. class InnerClass {
    5. void accessMembers() {
    6. System.out.println(outerField);
    7. System.out.println(staticOuterField);
    8. }
    9. }
    10. static class StaticNestedClass {
    11. void accessMembers(OuterClass outer) {
    12. // Compiler error: Cannot make a static reference to the non-static
    13. // field outerField
    14. // System.out.println(outerField);
    15. System.out.println(outer.outerField);
    16. System.out.println(staticOuterField);
    17. }
    18. }
    19. public static void main(String[] args) {
    20. System.out.println("Inner class:");
    21. System.out.println("------------");
    22. OuterClass outerObject = new OuterClass();
    23. OuterClass.InnerClass innerObject = outerObject.new InnerClass();
    24. innerObject.accessMembers();
    25. System.out.println("\nStatic nested class:");
    26. System.out.println("--------------------");
    27. StaticNestedClass staticNestedObject = new StaticNestedClass();
    28. staticNestedObject.accessMembers(outerObject);
    29. System.out.println("\nTop-level class:");
    30. System.out.println("--------------------");
    31. TopLevelClass topLevelObject = new TopLevelClass();
    32. topLevelObject.accessMembers(outerObject);
    33. }
    34. }
    1. public class TopLevelClass {
    2. void accessMembers(OuterClass outer) {
    3. // Compiler error: Cannot make a static reference to the non-static
    4. // field OuterClass.outerField
    5. // System.out.println(OuterClass.outerField);
    6. System.out.println(outer.outerField);
    7. System.out.println(OuterClass.staticOuterField);
    8. }
    9. }

    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编译器在突出显示的语句处产生一个错误。

  1. static class StaticNestedClass {
  2. void accessMembers(OuterClass outer) {
  3. // Compiler error: Cannot make a static reference to the non-static
  4. // field outerField
  5. System.out.println(outerField);
  6. }
  7. }

为了解决这个错误,通过对象引用来访问outerField
System.out.println(outer.outerField);
类似地,顶层类TopLevelCLass 也不能直接访问outerField。

Shadowing

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

  1. public class ShadowTest {
  2. public int x = 0;
  3. class FirstLevel {
  4. public int x = 1;
  5. void methodInFirstLevel(int x) {
  6. System.out.println("x = " + x);
  7. System.out.println("this.x = " + this.x);
  8. System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
  9. }
  10. }
  11. public static void main(String... args) {
  12. ShadowTest st = new ShadowTest();
  13. ShadowTest.FirstLevel fl = st.new FirstLevel();
  14. fl.methodInFirstLevel(23);
  15. }
  16. }

例子的输出结果如下:
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编译器能够实现新的Java语言特性,而无需更改JVM,然而,合成构造可能在不同的Java编译器实现中有所不同,这意味着.class文件也可能在不同的实现中有所不同,因此,如果序列化内部类然后用不同的JRE来反序列化的时候,可能会有兼容性的问题。


二、本地类

本地类是在一组在平衡大括号之间的零或多个语句的块中定义的类。一般在方法体内可以发现本地类的定义。

声明本地类

可以在任何的块中定义本地类,例如可以在方法体内、for循环里面或者if子句里面。
在以下的LocalClassExample类中的validatePhoneNumber()中定义本地类来实现校验两个手机号码的目的。

  1. public class LocalClassExample {
  2. static String regularExpression = "[^0-9]";
  3. public static void validatePhoneNumber(
  4. String phoneNumber1, String phoneNumber2) {
  5. final int numberLength = 10;
  6. // Valid in JDK 8 and later:
  7. // int numberLength = 10;
  8. class PhoneNumber {
  9. String formattedPhoneNumber = null;
  10. PhoneNumber(String phoneNumber){
  11. // numberLength = 7;
  12. String currentNumber = phoneNumber.replaceAll(
  13. regularExpression, "");
  14. if (currentNumber.length() == numberLength)
  15. formattedPhoneNumber = currentNumber;
  16. else
  17. formattedPhoneNumber = null;
  18. }
  19. public String getNumber() {
  20. return formattedPhoneNumber;
  21. }
  22. // Valid in JDK 8 and later:
  23. // public void printOriginalNumbers() {
  24. // System.out.println("Original numbers are " + phoneNumber1 +
  25. // " and " + phoneNumber2);
  26. // }
  27. }
  28. PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
  29. PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
  30. // Valid in JDK 8 and later:
  31. // myNumber1.printOriginalNumbers();
  32. if (myNumber1.getNumber() == null)
  33. System.out.println("First number is invalid");
  34. else
  35. System.out.println("First number is " + myNumber1.getNumber());
  36. if (myNumber2.getNumber() == null)
  37. System.out.println("Second number is invalid");
  38. else
  39. System.out.println("Second number is " + myNumber2.getNumber());
  40. }
  41. public static void main(String... args) {
  42. validatePhoneNumber("123-456-7890", "456-7890");
  43. }
  44. }

在校验手机号码的例子中,首先从phone number中移除除了数字0到9之外的全部字符。然后检查phone number 是否确切地包含是个数字(北美的电话号码长度)。这例子打印的信息如下:
First number is 1234567890
Second number is invalid

访问封闭类的成员

本地类可以访问其封闭类的成员。在之前的例子中,PhoneNumber 构造器访问了LocalCLassExample的成员变量regularExpression。此外,本地类可以访问局部变量。然而只能访问被声明为 final 的的局部变量。当本地类访问局部变量或者封闭块的参数的时候,它会捕获那个变量或者参数,例如:PhoneNumber的构造器可以访问局部变量numberLength,因为numberLength变量被声明为final;numberLength变量是个可捕获的变量。
然而,在Java8开始,本地类可以访问final 或者实际上是final的局部变量和封闭块的参数。一旦在被初始化之后,值就永远不改变的变量或者参数就是实际final。例如:假设numberLength变量没有被声明为final ,然后你添加赋值语句 numberLength = 7 到PhoneNumber 的构造器中来改变校验电话号码的长度为7:

  1. PhoneNumber(String phoneNumber) {
  2. numberLength = 7;
  3. String currentNumber = phoneNumber.replaceAll(
  4. regularExpression, "");
  5. if (currentNumber.length() == numberLength)
  6. formattedPhoneNumber = currentNumber;
  7. else
  8. formattedPhoneNumber = null;
  9. }

因为这条赋值语句,numberLength变量不再是实际的final类型。因此,Java编译器在内部类PhoneNumber想要访问numberLength变量的地方就会产生一个错误的信息,类似于“local variables referenced from an inner class must be final or effectively final”:
if (currentNumber.length() == numberLength)
从Java8开始,如果在方法内部定义本地类,那它可以访问方法的参数。例如:你可以再PhoneNumber本地类中定义一下的方法:

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

方法printOriginalNumbers 访问访问validatePhoneNumber方法的参数phoneNumber1 和 phoneNumber2

Shadowing 和本地类

局部类中类型的声明(比如变量)会影响封闭作用域中同名的声明。

本地类和内部类相似

本地类和内部类相似,因为两者都不能定义或者声明任何的静态成员。本地类在静态方法内部,比如:Phone Number类定义在静态方法validatePhoneNumber()里面,只能引用封闭类的静态成员。例如:如果不将成员变量regularExpression定义为static,Java编译器就会产生和“non-static variable regularExpression cannot be referenced from a static context.”类似的错误。
本地类不是static的,因为他们可以访问封闭块的实例成员,因此他们不能包含大多数类型的static 声明。不能在块中定义接口,接口本质上是static的。
例如:下面的代码节选肯定不能被编译,因为HelloThere接口被定义在greetInEnglish()方法体里面。

  1. public void greetInEnglish() {
  2. interface HelloThere {
  3. public void greet();
  4. }
  5. class EnglishHelloThere implements HelloThere {
  6. public void greet() {
  7. System.out.println("Hello " + name);
  8. }
  9. }
  10. HelloThere myGreeting = new EnglishHelloThere();
  11. myGreeting.greet();
  12. }

你不能在本地类中定义静态的初始化项或者成员接口。下面的代码节选肯定不能被编译,因为EnglishGoodBye.sayGoodbye 被定义为静态,当编译器遇到该方法定义的时候,它会产生一个和“modifier ‘static’ is only allowed in constant variable declaration”相似的错误。

  1. public void sayGoodbyeInEnglish() {
  2. class EnglishGoodbye {
  3. public static void sayGoodbye() {
  4. System.out.println("Bye bye");
  5. }
  6. }
  7. EnglishGoodbye.sayGoodbye();
  8. }

本地类是可以提供是常量变量的静态成员。(常量变量是原始变量,或者是被定义为finnal的String类型,并且在编译时期就被初始化的常量表达式,编译时期,常量表达式通常是string 或者在编译时期可以运算的算术表达式)。下面的代码节选肯定能编译,因为静态成员EnglishGoodbye.farewell 是一个常量变量。

  1. public void sayGoodbyeInEnglish() {
  2. class EnglishGoodbye {
  3. public static final String farewell = "Bye bye";
  4. public void sayGoodbye() {
  5. System.out.println(farewell);
  6. }
  7. }
  8. EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
  9. myEnglishGoodbye.sayGoodbye();
  10. }

三、匿名类

匿名类可以让代码变得更简洁,可以同一时间定义和实例化类。它们和本地类很像,除了它们没有名字。
如果你需要仅仅使用本地类一次而已,那就使用匿名类吧。

定义匿名类

匿名类是表达式,然而本地类是类声明,这就意味着可以在另外的表达式中定义类,下面的HelloWorldAnoymousClasses例子在局部变量frenchGreetingh和变量SpanishGreeting的初始化语句中使用匿名类,但是在变量englishGreeting的初始化中使用了本地类:

  1. public class HelloWorldAnonymousClasses {
  2. interface HelloWorld {
  3. public void greet();
  4. public void greetSomeone(String someone);
  5. }
  6. public void sayHello() {
  7. class EnglishGreeting implements HelloWorld {
  8. String name = "world";
  9. public void greet() {
  10. greetSomeone("world");
  11. }
  12. public void greetSomeone(String someone) {
  13. name = someone;
  14. System.out.println("Hello " + name);
  15. }
  16. }
  17. HelloWorld englishGreeting = new EnglishGreeting();
  18. HelloWorld frenchGreeting = new HelloWorld() {
  19. String name = "tout le monde";
  20. public void greet() {
  21. greetSomeone("tout le monde");
  22. }
  23. public void greetSomeone(String someone) {
  24. name = someone;
  25. System.out.println("Salut " + name);
  26. }
  27. };
  28. HelloWorld spanishGreeting = new HelloWorld() {
  29. String name = "mundo";
  30. public void greet() {
  31. greetSomeone("mundo");
  32. }
  33. public void greetSomeone(String someone) {
  34. name = someone;
  35. System.out.println("Hola, " + name);
  36. }
  37. };
  38. englishGreeting.greet();
  39. frenchGreeting.greetSomeone("Fred");
  40. spanishGreeting.greet();
  41. }
  42. public static void main(String... args) {
  43. HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses();
  44. myApp.sayHello();
  45. }
  46. }

匿名类的语法

正如之前提及的,匿名类是一个表达式。匿名类表达式的语法和构造器的调用很像,只是在代码块中包含了类的定义。讨论一下frenchGreeting 对象的实例化:

  1. HelloWorld frenchGreeting = new HelloWorld() {
  2. String name = "tout le monde";
  3. public void greet() {
  4. greetSomeone("tout le monde");
  5. }
  6. public void greetSomeone(String someone) {
  7. name = someone;
  8. System.out.println("Salut " + name);
  9. }
  10. };

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

  • new 操作符
  • 实现的接口或者继承的类的名字。在这个例子中,匿名类实现了HelloWorld接口。
  • 包含构造函数参数的圆括号,只是和寻常的类实例创建的表达式一样。注意:当你实现一个接口的时候是没有构造函数的,所以使用空得圆括号对,就像这个例子。
  • 类声明主体。更明确地说是在主体中,允许方法声明,但不允许语句。

因为匿名类得定义是一个表达式,它一定是语句的一部分。在这个例子中,匿名类的表达式是frenchGreeting对象实例语句的一部分。(这也解释了为什么在右大括号的后面有分号)

访问封闭类的局部变量以及定义和访问匿名类的成员

和本地类一样,匿名类可以捕获变量,它们对外围作用域的局部变量具有相同的访问权限:

  • 匿名类可以访问其封闭类的成员。
  • 匿名类不能访问其封闭作用域中未声明为final或实际上为final的局部变量。
  • 与嵌套类一样,匿名类中的类型声明(比如变量)掩盖了封闭作用域中具有相同名称的任何其他声明。

对于其成员,匿名类也有与局部类相同的限制。

  • 不能在匿名类中定义静态初始化器或者成员接口。
  • 匿名类可以具有静态成员,只要它们是常量变量。

注意:你可以在匿名类中声明以下的内容

  • 字段
  • 额外的方法(尽管不实现超类中的任何方法)
  • 实例初始化器
  • 本地类

然而,不能在匿名类中定义构造器