一、嵌套类
Java编程语言允许在一个类里面定义类,这样的类被成为嵌套类,例如:
class OuterClass {
...
class NestedClass {
...
}
}
嵌套类被划分为两个类型,非静态和静态。非静态的嵌套类被成为内部类,被 `static`修饰的嵌套类为静态嵌套类。
class OuterClass {
...
class InnerClass {
...
}
static class StaticNestedClass {
...
}
}
嵌套类是其封闭类的成员。非静态的嵌套类(内部类)可以访问其封闭类的其他成员,尽管有些被声明为 private
类型;静态嵌套类不能访问其封闭类的其他变量。作为外部类的一个成员,嵌套类可以被声明为 private
、public
、protected
或者 package private
(回想外部类只能被声明为public 或者 package private。)
为什么要使用嵌套类?
使用嵌套类的原因如下所示:
- 它是一种逻辑地将只在一个地方使用的类分组的方法。如果一个类只是作用于一个另外的类,就可以将逻辑键入到它里面使得两个类在一起。嵌套这样的“助手类”使它们的包更加流线型。
- 提升封闭性。想象一下两个顶层类A和B,类B需要访问类A的变量只能声明为除了private之外的类型。通过将类B隐藏到类A里面,类A的变量可以声明为private,并且类B可以访问它们。
它可以产生更有可读性和维护性的代码。把嵌套小类放在顶层的类的空间里,可以让代码更加靠近使用的地方。
内部类
与实例方法和变量一样,内部类和其封闭类的实例相关联,并且能够直接地访问对象的方法和字段。同时,因为内部类是与实例相关联的,所以它本身不能定义任何的静态成员。
内部类的实例对象存在于外部类的实例中。请上例子:class OuterClass {
...
class InnerClass {
...
}
}
内部类的实例只能存在于外部类的实例当中,并且可以直接访问它的封闭类实例的方法和字段。
为了实例化一个内部类,首先就要实例化外部类,然后使用OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
来在外部类对象里面创建内部类对象。
内部类有两个特殊类型:本地类和匿名类。静态嵌套类
与类方法和变量一样,静态嵌套类和它的外部类相关联。和静态类方法一样,静态嵌套类不能直接引用它的封闭类定义的实例变量和方法,它只能通过对象引用来使用它们。
静态嵌套类与外部类(和其他类)的实例成员进行交互,就像任何其他顶级类一样,实际上,静态嵌套类在行为上是一个顶级类,为了方便打包,它被嵌套在另一个顶级类中。
静态嵌套类的实例化方法与顶级类相同。StaticNestedClass staticNestedObject = new StaticNestedClass();
内部类和嵌套静态类的例子
接下来的OuterClass 连同 TopLevelClass一起说明了OuterClass的哪些类成员可以访问内部类( InnerClass )、嵌套静态类( StaticNestedClass )和顶级类( TopLevelClass )
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);
}
}
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);
}
}
为了解决这个错误,通过对象引用来访问outerFieldSystem.out.println(outer.outerField);
类似地,顶层类TopLevelCLass 也不能直接访问outerField。
Shadowing
如果特定作用域中的类型声明(如成员变量或参数名)与封闭作用域中的另一个声明具有相同的名称(如内部类或方法定义),则该声明将隐藏封闭作用域中的声明。您不能仅通过名称来引用被隐藏的声明。下面的例子,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编译器能够实现新的Java语言特性,而无需更改JVM,然而,合成构造可能在不同的Java编译器实现中有所不同,这意味着.class文件也可能在不同的实现中有所不同,因此,如果序列化内部类然后用不同的JRE来反序列化的时候,可能会有兼容性的问题。
二、本地类
本地类是在一组在平衡大括号之间的零或多个语句的块中定义的类。一般在方法体内可以发现本地类的定义。
声明本地类
可以在任何的块中定义本地类,例如可以在方法体内、for循环里面或者if子句里面。
在以下的LocalClassExample类中的validatePhoneNumber()中定义本地类来实现校验两个手机号码的目的。
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");
}
}
在校验手机号码的例子中,首先从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:
PhoneNumber(String phoneNumber) {
numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
因为这条赋值语句,numberLength变量不再是实际的final类型。因此,Java编译器在内部类PhoneNumber想要访问numberLength变量的地方就会产生一个错误的信息,类似于“local variables referenced from an inner class must be final or effectively final”:if (currentNumber.length() == numberLength)
从Java8开始,如果在方法内部定义本地类,那它可以访问方法的参数。例如:你可以再PhoneNumber本地类中定义一下的方法:
public void printOriginalNumbers() {
System.out.println("Original numbers are " + phoneNumber1 +
" and " + phoneNumber2);
}
方法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()方法体里面。
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 被定义为静态,当编译器遇到该方法定义的时候,它会产生一个和“modifier ‘static’ is only allowed in constant variable declaration”相似的错误。
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static void sayGoodbye() {
System.out.println("Bye bye");
}
}
EnglishGoodbye.sayGoodbye();
}
本地类是可以提供是常量变量的静态成员。(常量变量是原始变量,或者是被定义为finnal的String类型,并且在编译时期就被初始化的常量表达式,编译时期,常量表达式通常是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();
}
三、匿名类
匿名类可以让代码变得更简洁,可以同一时间定义和实例化类。它们和本地类很像,除了它们没有名字。
如果你需要仅仅使用本地类一次而已,那就使用匿名类吧。
定义匿名类
匿名类是表达式,然而本地类是类声明,这就意味着可以在另外的表达式中定义类,下面的HelloWorldAnoymousClasses例子在局部变量frenchGreetingh和变量SpanishGreeting的初始化语句中使用匿名类,但是在变量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或实际上为final的局部变量。
- 与嵌套类一样,匿名类中的类型声明(比如变量)掩盖了封闭作用域中具有相同名称的任何其他声明。
对于其成员,匿名类也有与局部类相同的限制。
- 不能在匿名类中定义静态初始化器或者成员接口。
- 匿名类可以具有静态成员,只要它们是常量变量。
注意:你可以在匿名类中声明以下的内容
- 字段
- 额外的方法(尽管不实现超类中的任何方法)
- 实例初始化器
- 本地类
然而,不能在匿名类中定义构造器