赞
踩
目录
5、嵌套类的实现原理以及为什么要使用 final 修饰局部变量?
Java 允许你在另一个类中定义一个类。这样的类被称为嵌套类,如下所示:
- class OuterClass {
- ...
- class NestedClass {
- ...
- }
- }
嵌套类分为两类,静态嵌套类和非静态嵌套类,其中非静态嵌套类称为内部类。
- class OuterClass {
- ...
- class InnerClass {
- ...
- }
- static class StaticNestedClass {
- ...
- }
- }
嵌套类是其外部类中的成员。非静态嵌套类(内部类)可以访问外部类的其他成员(包括 private 的成员),而静态嵌套类不能访问外部类的其他成员。作为 OuterClass(外部类)的成员,嵌套类可以声明为 private、public、protected 或 package private。(请记住,外部类只能声明为 public 或 package private。) // package private 为包私有访问权限
使用嵌套类的重要原因包括:
跟方法和变量一样,内部类与它外部类的实例相关联,并可以直接访问外部类的方法和属性。此外,由于内部类与外部类的实例关联,所以内部类本身并不能定义任何静态成员。
内部类实例存在于外部类实例中:
- class OuterClass {
- ...
- class InnerClass {
- ...
- }
- }
要实例化内部类,就必须先实例化外部类。然后,在外部类对象中创建内部类的对象:
- // 创建外部类
- OuterClass outerObject = new OuterClass();
- // 创建内部类
- OuterClass.InnerClass innerObject = outerObject.new InnerClass();
Java 内部类有两种特殊类型:局部类和匿名类。
跟方法和变量一样,静态嵌套类也与其外部类相关联(注意不是外部类的实例)。但是,静态嵌套类不能直接引用外部类中定义的变量或方法,如果想引用,只能通过外部类对象的引用使用它们。
静态嵌套类可以像其他独立类(顶级类)一样与其外部类的实例成员进行交互,而且实际上,静态嵌套类在行为上就是一个独立的类,只不过是为了打包方便而嵌套在另一个独立的类中(顶级类)。
使用创建独立类相同的方式创建一个静态嵌套类的实例:
StaticNestedClass staticNestedObject = new StaticNestedClass();
如果内部类中的属性声明与外部类中的另一个属性的声明具有相同的名称,那么该声明会将外部内中的声明进行遮蔽。这种情况下,不能仅通过属性的名称对其进行引用。下面的例子 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
内部类是可以被序列化的,但是 Java 官方极力不推荐对内部类进行序列化。因为当 Java 编译器在编译某些构造(比如内部类)时,它会创建一些合成构造;合成构造可以使 Java 编译器在不改变 JVM 的情况下实现新的 Java 语言特性。但是这些合成构造中的类、方法、属性以及其他的构造在源代码中并没有相对应的构造形式。而且,合成构造在不同的 Java 编译器之间的实现可能会有所不同,因此这意味着字节码文件在不同的编译器之间也可能有所不同。// 如果字节码不一致,反序列化就可能会有问题
下面是 DataStructure.java 的内部类程序示例:
- public class DataStructure {
-
- // 创建一个数组
- private final static int SIZE = 15;
- private int[] arrayOfInts = new int[SIZE];
-
- public DataStructure() {
- // 填充数组
- for (int i = 0; i < SIZE; i++) {
- arrayOfInts[i] = i;
- }
- }
-
- public void printEven() {
-
- // 打印出数组的偶数下标的值
- DataStructureIterator iterator = this.new EvenIterator();
- while (iterator.hasNext()) {
- System.out.print(iterator.next() + " ");
- }
- System.out.println();
- }
-
- interface DataStructureIterator extends java.util.Iterator<Integer> { }
-
- // 内部类实现DataStructureIterator接口,该接口扩展了Iterator<Integer>接口
- private class EvenIterator implements DataStructureIterator {
-
- // 从头开始逐级遍历数组
- private int nextIndex = 0;
-
- public boolean hasNext() {
- // 检查当前元素是否是数组中的最后一个元素
- return (nextIndex <= SIZE - 1);
- }
-
- public Integer next() {
-
- // 记录数组的偶数下标值
- Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);
-
- // 获取下一个偶数元素
- nextIndex += 2;
- return retValue;
- }
- }
-
- public static void main(String s[]) {
- // 用整数值填充数组并只打印出偶数下标的值
- DataStructure ds = new DataStructure();
- ds.printEven();
- }
- }
在上述程序中,EvenIterator.class 可以直接引用 DataStructure.class 对象中的 arrayOfInts 实例变量。// 如何定义和使用内部类
此外,内部类还有另外两种类型,局部类和匿名类。在方法体中声明的内部类,称为局部类。在方法体中声明而不命名的内部类,称为匿名类。
局部类可以定义在任何的代码块中。例如,可以在方法体、for 循环或 if 判断语句中定义局部类。在下边的示例程序中 LocalClassExample.calss 在它的方法 validatePhoneNumber 中定义了局部类 PhoneNumber.calss:
- public class LocalClassExample {
-
- static String regularExpression = "[^0-9]";
-
- public static void validatePhoneNumber( String phoneNumber1, String phoneNumber2) {
-
- final int numberLength = 10;
- // 适用于JDK 8及以后版本:
- // 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;
- }
-
- // 适用于JDK 8及以后版本:
- // 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 之外的所有字符,之后,它会检查电话号码是否恰好包含 10 位数字。这个程序会输出如下信息:
- First number is 1234567890
- Second number is invalid
局部类可以访问局部变量,但是局部类只能访问声明为 final 的局部变量。从Java SE 8开始,局部类可以访问没有声明为 final 的局部变量和参数,不过这些变量和参数实际上还是 final 的,因为这些变量在初始化后,值就从未进行改变。
局部类和内部类一样,它们不能定义或声明任何静态成员。
(1)不能在方法中声明接口,因为接口本质上就是静态的。如下程序中的代码将不能编译:
- 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();
- }
(2)不能在局部类中声明静态方法。如下程序中的代码将不能编译:
- public void sayGoodbyeInEnglish() {
- class EnglishGoodbye {
- public static void sayGoodbye() { // 静态方法将不能编译通过
- System.out.println("Bye bye");
- }
- }
- EnglishGoodbye.sayGoodbye();
- }
(3)不过局部类有时候又可以有静态成员,前提是这些成员是常量变量(声明为 final 的属性)。所指的常量,它的类型要是基本数据类型或者 String 类型,并且声明为 final ,这些属性会在编译时使用常量表达式进行初始化。编译时的常量表达式通常是在编译时就可以求值的字符串或算术表达式,所以下面代码的编译可以通过:// 属性需要显示的声明为 final
- 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();
- }
局部类是对类的声明,而匿名类则只是一个表达式。这意味着你可以在表达式去中定义一个类。示例程序如下:
- 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 实例化程序语句的一部分。(这解释了为什么右大括号后面还会有一个分号)
编译器会默认为成员内部类添加一个指向外部类对象的引用,因此可以在成员内部类中随意访问外部类的成员。
成员内部类依赖于外部类,在没有创建外部类对象的情况下,则无法对引用进行初始化赋值,也就无法创建成员内部类的对象。
内部类与 final 变量
前面我们知道,局部内部类和匿名内部类都只能访问局部的 final 变量,但具体是为什么,我们还不清楚,所以这里解释一下,示例的代码程序如下:
- public class InnerClassTest {
-
- // a和b虽然可以不加final,但实际上它们都是final修饰的变量
- public void test(final int b){
- final int a = 20;
- new Thread(){
- @Override
- public void run() {
- System.out.println(a);
- System.out.println(b);
- }
- }.start();
- }
-
- public static void main(String[] args) {
- InnerClassTest innerClassTest = new InnerClassTest();
- innerClassTest.test(10);
- }
- }
这样设计的原因,主要是为了解决变量的生命周期问题。
在上示例程序中,test() 方法中拥有一个局部变量 a,在该方法执行完成后,局部变量 a 的生命周期便结束了,但是这个时候,如果 Thread 类中的 run() 方法还在使用变量 a,那么便引发了变量的生命周期问题。// 还没使用,变量就被回收了
所以,Java 采用了复制的方法对上述问题进行了解决,所谓复制,就是程序中存在两个 a,一个是在 test() 方法中的 a,一个是被复制的 a(run() 方法中的 a),它们的值一样,但却分别独立。这个时候为了保证两个 a 的值一致和值不被篡改,使用 final 关键字对其进行修饰。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。