Java Nested Classes(內部類~第一篇英文技術文件翻譯)

翎野君發表於2019-01-19

鄙人最近嘗試著翻譯了自己的第一篇英文技術文件。
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進行反序列化的話,可能會存在相容性的問題。

相關文章