鄙人最近尝试着翻译了自己的第一篇英文技术文档。
Java Nested Classes Reference From Oracle Documentation
嵌套类-Nested Classes
在Java中我们可以在一个类的内部,再定义另外一个类,其中里面的那个类被称为嵌套类,示例如下。
class OuterClass { ... class NestedClass { ... } }
术语:嵌套类有两种类型:静态和非静态,当嵌套类被static修饰时,被称为静态嵌套类(static nested classes),没有被static修饰时的嵌套类被称作内部类(inner classes)
class OuterClass { ... static class StaticNestedClass { ... } class InnerClass { ... } }
嵌套类是外部基类(即外部类)的成员,非静态嵌套类(内部类)可以获取到外围基类的其他成员,其中也包括被声明为private的成员。静态嵌套类则不可以获取基类的其他成员。当做为作为外部类的成员,嵌套类可以被定义为private,public,protected或者package private。如果我们需要在其他外部类中使用内部类,则一定要将嵌套类声明为public或者 package private。
为什么使用嵌套类-Why Use Nested Classes?
使用嵌套类有以下几个明显的优势:
- 当仅会在一处用到某个类时,通过嵌套类可以在逻辑上与基类(外部类)保持一种紧密的联系关系:当一个类只会在另一个类中使用,那么就可以把这个类嵌入到另外一个类中,可以使得两者之间有着紧密的联系,嵌套类又称之为'辅助类'。
- 通过合理的使用可以使得整个包下的类定义更加的简洁:更强的封装性:A和B两个类,B作为A类的嵌套类,如果不将其中B类B类设置为private的话,那么B类就拥有访问A类成员的权限。
- 更好的可读性和更高的可维护性:在编码时内部的嵌套类总是需要和最外层类保持一种形式上的关联关系。
静态嵌套类-Static Nested Classes
静态嵌套类不能直接引用外部基类的实例变量和实例方法,对于这样的实例变量仅可以通过对象引用来获取。
通过使用外围基类名称来获取静态嵌套类
OuterClass.StaticNestedClass
如果我们想创建一个静态嵌套类的对象,则可以使用如下的方式
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
内部类-Inner Classes
内部类可以通过外部类实例,直接获取基类对象的变量和方法,同理因为内部类是通过实例引用来和外部类建立关系的,所以在内部类中不能定义任何的静态成员。只有当外部类实例对象被创建出来之后,才可以实例化内部类。
class OuterClass { ... class InnerClass { ... } }
内部类实例只能存在于外部类实例中,并且可以直接访问其外部类实例的方法和字段。
在实例化内部类前,要先实例化外部类实例。可以通过如下方式,通过外部对象实例来创建内部类对象。
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
内部类有两种类型:局部类(local classes) 和 匿名类(anonymous classes).
局部类-Local Classes
局部类是一种被定义在代码块中的类,局部类通常时定义在方法体中。
如何声明局部类:
可以在任何一个方法之中定义一个局部类,如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,就拿到了外部类中的regularExpression成员。
另外,局部类中也能使用局部变量,但是在局部类中只能使用被final修饰后的变量,当一个局部类要使用定义在外部代码块中的局部变量或者参数时,他会俘获(这个变量就是他的了)这个变量或者参数。
比如,PhoneNumber的构造方法中,能够/会,俘获numberLength,因为这个变量在外围块中被声明为final,这样的话numberLength 就成为了一个被俘获的变量了,有了主人。
但是在java 1.8版本中局部类能够使用定义在外部块中的final或者effectively final的变量或者参数,如果一个变量或者参数的值在初始化后便不会被改变,则被称为effectively final。
比如在下面的代码中,变量numberLength没有被显示的声明为final,在初始化后有在方法中又将numberLength的值修改为7:
PhoneNumber(String phoneNumber) { numberLength = 7; String currentNumber = phoneNumber.replaceAll( regularExpression, ""); if (currentNumber.length() == numberLength) formattedPhoneNumber = currentNumber; else formattedPhoneNumber = null; }
因为这个赋值语句numberLength = 7,变量numberLength 便不再是 effectively final了,在这种情形下,内部类尝试在if (currentNumber.length() == numberLength)
这行代码中获取numberLength时,编译器时会提示"local variables referenced from an inner class must be final or effectively final"
。
在java8中,如果在方法中声明了局部类,那么可以在局部类中拿到方法的入参,就像下面的方法:
public void printOriginalNumbers() { System.out.println("Original numbers are " + phoneNumber1 + " and " + phoneNumber2); }
局部类中的printOriginalNumbers方法获取到了方法validatePhoneNumber中的phoneNumber1 和phoneNumber2两个参数变量。
局部类与内部类的相似点
局部类像内部类一样,二者都不能定义和声明静态成员,在静态方法validatePhoneNumber中定义的PhoneNumber局部类,只能引用外部类中的静态成员。
如果将变量regularExpression定义为非静态,那么在java编译器编译的时候会提示"non-static variable regularExpression cannot be referenced from a static context."错误信息。
因为要获取外围代码块中的实例成员,所以局部类不能时静态的,所以在局部类中不能包含有静态声明。
不能在代码块中,尝试定义或者声明接口,因为接口本质上就是静态的,比如下面的代码是不能编译成功的,因为在greetInEnglish方法内部包含有HelloThere接口:
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(); }
当然在局部类中也不能声明静态方法,下面的代码同样,在编译时会报"modifier 'static' is only allowed in constant variable declaration"
,因为EnglishGoodbye.sayGoodbye这个方法被声明为静态方法了。
public void sayGoodbyeInEnglish() { class EnglishGoodbye { public static void sayGoodbye() { System.out.println("Bye bye"); } } EnglishGoodbye.sayGoodbye(); }
局部类中只有变量时常量的时候,才可能会出现有静态成员变量的情况,下面的代码中有静态成员但也可以编译通过,因为静态变量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(); }
匿名类-Anonymous Classes
匿名类可以使你的代码看上去更加的精简,可以在声明一个匿名类的同时对它进行初始化,除了没有类名以外,它跟局部类很像,对于只会使用一次的局部类的场景我们可以用匿名类来代替。
局部类就是一个类,而匿名类则更像是一个表达式,那么我们便可以在另外的表达式中使用匿名类。
下面的例子中 HelloWorldAnonymousClasses通过使用匿名类创建局部变量frenchGreeting 和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或者effectively final的局部变量
- 匿名类中的声明的类型变量,会覆盖掉外部区域中的同名的变量
对于匿名类中的成员,匿名类具有跟局部类相同的限制
- 不能在匿名类中声明静态代码块,或者再定义内部成员接口
- 匿名类中仅当变量为常量时,才可以出现静态成员
小结,在匿名类中可以声明如下内容
- 列表项目
- 字段
- 额外的方法(即使不实现任何父类的方法)
- 实例代码块
- 局部类
但是,不可以在匿名类中声明构造方法
匿名类的一个实例
匿名类在java GUI中使用的较为频繁
import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class HelloWorld extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { primaryStage.setTitle("Hello World!"); Button btn = new Button(); btn.setText("Say 'Hello World'"); btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { System.out.println("Hello World!"); } }); StackPane root = new StackPane(); root.getChildren().add(btn); primaryStage.setScene(new Scene(root, 300, 250)); primaryStage.show(); } }
变量覆盖问题-Shadowing
在内部类或者方法定义中声明的变量类型跟外围区域有相同的名称,那么内部的声明会覆盖掉外部区域中的声明,不能直接通过变量名拿到外部区域中定义的变量,如下所示:
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中的x。因为当你在方法methodInFirstLevel中使用变量x时,实际上使用的的是方法参数的值。
如果想引用内部类FirstLevel中的x,需要使用this关键字,来代表引用的时内部类中方法外围的x。
System.out.println("this.x = " + this.x);
如果向引用最外面的基类变量x,则需要指明外部类的类名
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
序列化问题-Serialization
我们强烈不建议对内部类、局部类及匿名类,实现序列化。
当Java编译器编译内部类的构造方法时,会生成synthetic constructs。即一些在源码中未曾出现过的类、方法、字段和其他的构造方法也会被编译出来。
synthetic constructs方式,可以在不改变JVM的前提下,只通过java编译器就可以实现java的新特性。然而,不同的编译器实现synthetic constructs的方式有所不同,这也就意味着,对于同样的.java源码,不同的编译器会编译出来不同的.class文件。
因此,对于一个内部类序列化后,使用不同的JRE进行反序列化的话,可能会存在兼容性的问题。