1. 程式人生 > 實用技巧 >2020重新出發,JAVA基礎,內部類

2020重新出發,JAVA基礎,內部類

@目錄

內部類

在類內部可定義成員變數和方法,且在類內部也可以定義另一個類。如果在類 Outer 的內部再定義一個類 Inner,此時類 Inner 就稱為內部類(或稱為巢狀類),而類 Outer 則稱為外部類(或稱為宿主類)。

內部類可以很好地實現隱藏,一般的非內部類是不允許有 private 與 protected 許可權的,但內部類可以。內部類擁有外部類的所有元素的訪問許可權。

內部類可以分為:例項內部類、靜態內部類和成員內部類,每種內部類都有它特定的一些特點

內部類也可以分為多種形式,與變數非常類似,如圖所示。

內部類的特點如下:

  1. 內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class檔案,但是前面冠以外部類的類名和$符號。
  2. 內部類不能用普通的方式訪問。內部類是外部類的一個成員,因此內部類可以自由地訪問外部類的成員變數,無論是否為 private。
  3. 內部類宣告成靜態的,就不能隨便訪問外部類的成員變數,仍然是隻能訪問外部類的靜態成員變數。

內部類的使用方法非常簡單,例如下面的程式碼演示了內部類最簡單的應用。

public class Test {    
    public class InnerClass {        
        public int getSum(int x,int y) {            
            return x + y;        
        }    
    }    
    
    public static void main(String[] args) {        
        Test.InnerClass ti = new Test().new InnerClass();        
        int i = ti.getSum(2,3);       
        System.out.println(i);    // 輸出5    
    }
}

有關內部類的說明有如下幾點。

  • 外部類只有兩種訪問級別:public 和預設;內部類則有 4 種訪問級別:public、protected、 private 和預設。
  • 在外部類中可以直接通過內部類的類名訪問內部類。
InnerClass ic = new InnerClass();    // InnerClass為內部類的類名
  • 在外部類以外的其他類中則需要通過內部類的完整類名訪問內部類。
Test.InnerClass ti = newTest().new InnerClass();    // Test.innerClass是內部類的完整類名
  • 內部類與外部類不能重名。

提示:內部類的很多訪問規則可以參考變數和方法。另外使用內部類可以使程式結構變得緊湊,但是卻在一定程度上破壞了 Java 面向物件的思想。

例項內部類

例項內部類是指沒有用 static 修飾的內部類,有的地方也稱為非靜態內部類

public class Outer {    
    class Inner {        
        // 例項內部類    
    }
}

上述示例中的 Inner 類就是例項內部類。

例項內部類有如下特點。

1)在外部類的靜態方法和外部類以外的其他類中,必須通過外部類的例項建立內部類的例項

2)在例項內部類中,可以訪問外部類的所有成員。

提示:如果有多層巢狀,則內部類可以訪問所有外部類的成員。

3)在外部類中不能直接訪問內部類的成員,而必須通過內部類的例項去訪問。如果類 A 包含內部類 B,類 B 中包含內部類 C,則在類 A 中不能直接訪問類 C,而應該通過類 B 的例項去訪問類 C。

4)外部類例項與內部類例項是一對多的關係,也就是說一個內部類例項只對應一個外部類例項,而一個外部類例項則可以對應多個內部類例項。

如果例項內部類 B 與外部類 A 包含有同名的成員 t,則在類 B 中 t 和 this.t 都表示 B 中的成員 t,而 A.this.t 表示 A 中的成員 t。

5)在例項內部類中不能定義 static 成員,除非同時使用 final 和 static 修飾。

靜態內部類

靜態內部類是指使用 static 修飾的內部類。示例程式碼如下:

public class Outer {    
    static class Inner {        
        // 靜態內部類    
    }
}

上述示例中的 Inner 類就是靜態內部類。靜態內部類有如下特點。

1)在建立靜態內部類的例項時,不需要建立外部類的例項。

2)靜態內部類中可以定義靜態成員和例項成員。外部類以外的其他類需要通過完整的類名訪問靜態內部類中的靜態成員,如果要訪問靜態內部類中的例項成員,則需要通過靜態內部類的例項。

3)靜態內部類可以直接訪問外部類的靜態成員,如果要訪問外部類的例項成員,則需要通過外部類的例項去訪問

區域性內部類

區域性內部類是指在一個方法中定義的內部類。示例程式碼如下:

public class Test {    
    public void method() {        
        class Inner {            
            // 區域性內部類        
        }    
    }
}

區域性內部類有如下特點:

1)區域性內部類與區域性變數一樣,不能使用訪問控制修飾符(public、private 和 protected)和 static 修飾符修飾

2)區域性內部類只在當前方法中有效

3)區域性內部類中不能定義 static 成員

4)區域性內部類中還可以包含內部類,但是這些內部類也不能使用訪問控制修飾符(public、private 和 protected)static 修飾符修飾。

5)在區域性內部類中可以訪問外部類的所有成員。

6)在區域性內部類中只可以訪問當前方法中 final 型別的引數與變數。如果方法中的成員與外部類中的成員同名,則可以使用 <OuterClassName>.this.<MemberName> 的形式訪問外部類中的成員。

匿名內部類

匿名類是指沒有類名的內部類,必須在建立時使用 new 語句來宣告類。其語法形式如下:

new <類或介面>() {    
    // 類的主體
};

這種形式的 new 語句宣告一個新的匿名類,它對一個給定的類進行擴充套件,或者實現一個給定的介面。

使用匿名類可使程式碼更加簡潔、緊湊,模組化程度更高。

匿名類有兩種實現方式:

  • 繼承一個類,重寫其方法。
  • 實現一個介面(可以是多個),實現其方法。

下面通過程式碼來說明。

public class Out {
    void show() {
        System.out.println("呼叫 Out 類的 show() 方法");
    }
}
public class TestAnonymousInterClass {
    // 在這個方法中構造一個匿名內部類
    private void show() {
        Out anonyInter = new Out() {
            // 獲取匿名內部類的例項
            void show() {
                System.out.println("呼叫匿名類中的 show() 方法");
            }
        };
        anonyInter.show();
    }
    public static void main(String[] args) {
        TestAnonymousInterClass test = new TestAnonymousInterClass();
        test.show();
    }
}

程式的輸出結果如下:

呼叫匿名類中的 show() 方法

從輸出結果可以看出,匿名內部類有自己的實現。

提示:匿名內部類實現一個介面的方式與實現一個類的方式相同

匿名類有如下特點:

1)匿名類和區域性內部類一樣,可以訪問外部類的所有成員。如果匿名類位於一個方法中,則匿名類只能訪問方法中 final 型別的區域性變數和引數。

2)匿名類中允許使用非靜態程式碼塊進行成員初始化操作。

3)匿名類的非靜態程式碼塊會在父類的構造方法之後被執行。

Java8新特性:Effectively final

Java 中區域性內部類和匿名內部類訪問的區域性變數必須由 final 修飾,以保證內部類和外部類的資料一致性。但從 Java 8 開始,我們可以不加 final 修飾符,由系統預設新增,當然這在 Java 8 以前的版本是不允許的。Java 將這個功能稱為 Effectively final 功能。

因為系統會預設新增 final 修飾符,所以在匿名內部類中直接使用非 final 變數,而 final 修飾的區域性變數不能在被重新賦值。也就是說從 Java 8 開始,它不要求程式設計師必須將訪問的區域性變數顯式的宣告為 final 的。只要該變數不被重新賦值就可以。

一個非 final 的區域性變數或方法引數,其值在初始化後就從未更改,那麼該變數就是 effectively final。在 Lambda 表示式中,使用區域性變數的時候,也要求該變數必須是 final 的,所以 effectively final 在 Lambda 表示式上下文中非常有用。

Lambda 表示式在程式設計中是經常使用的,而匿名內部類是很少使用的。那麼,我們在 Lambda 程式設計中每一個被使用到的區域性變數都去顯示定義成 final 嗎?顯然這不是一個好方法。所以,Java 8 引入了 effectively final 新概念。

總結一下,規則沒有改變,Lambda 表示式和匿名內部類訪問的區域性變數必須是 final 的,只是不需要程式設計師顯式的宣告變數為 final 的,從而節省時間。

Lambda表示式

Lambda 表示式(Lambda expression)是一個匿名函式,基於數學中的λ演算得名,也可稱為閉包(Closure)。

Lambda 表示式是推動 Java 8 釋出的重要新特性,它允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),

例 先定義一個計算數值的介面,程式碼如下。

// 可計算介面
public interface Calculable {   
    // 計算兩個int數值    
    int calculateInt(int a, int b);
}

Calculable 介面只有一個方法 calculateInt,引數是兩個 int 型別,返回值也是 int 型別。實現方法程式碼如下:

public class Test{

    /**
     * 通過操作符,進行計算
     *
     * @param opr 操作符
     * @return 實現Calculable介面物件
     */
    public static Calculable calculate(char opr) {
        Calculable result;
        if (opr == '+') {
            // 匿名內部類實現Calculable介面
            result = new Calculable() {
                // 實現加法運算
                @Override
                public int calculateInt(int a, int b) {

                    return a + b;
                }
            };
        } else {
            // 匿名內部類實現Calculable介面
            result = new Calculable() {
                // 實現減法運算
                @Override
                public int calculateInt(int a, int b) {
                    return a - b;
                }
            };
        }
        return result;
    }
}

方法 calculate 中 opr 引數是運算子,返回值是實現 Calculable 介面物件。程式碼第 13 行和第 23 行都採用匿名內部類實現 Calculable 介面。程式碼第 16 行實現加法運算。程式碼第 26 行實現減法運算。

public static void main(String[] args) {
    int n1 = 10;
    int n2 = 5;
    // 實現加法計算Calculable物件
    Calculable f1 = calculate('+');
    // 實現減法計算Calculable物件
    Calculable f2 = calculate('-');
    // 呼叫calculateInt方法進行加法計算
    System.out.println(n1 + "+" + n2 + "=" + f1.calculateInt(n1, n2));
    // System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2));
    // 呼叫calculateInt方法進行減法計算
    System.out.println(n1 + "-" + n2 + "=" + f1.calculateInt(n1, n2));
    // System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2));
}

程式碼第 5 行中 f1 是實現加法計算 Calculable 物件,程式碼第 7 行中 f2 是實現減法計算 Calculable 物件。程式碼第 9 行和第 12 行才進行方法呼叫。

Java 中常見的輸出函式:

  1. printf 主要繼承了C語言中 printf 的一些特性,可以進行格式化輸出。
  2. print 就是一般的標準輸出,但是不換行。
  3. println 和 print 基本沒什麼差別,就是最後會換行。

輸出結果如下:

10+5=15
10-5=15

例 1 使用匿名內部類的方法 calculate 程式碼很臃腫,Java 8 採用 Lambda 表示式可以替代匿名內部類。修改之後的通用方法 calculate 程式碼如下:

/**
* 通過操作符,進行計算
* @param opr 操作符
* @return 實現Calculable介面物件
*/
public static Calculable calculate(char opr) {
    Calculable result;
    if (opr == '+') {
        // Lambda表示式實現Calculable介面
        result = (int a, int b) -> {
            return a + b;
        };
    } else {
        // Lambda表示式實現Calculable介面
        result = (int a, int b) -> {
            return a - b;
        };
    }
    return result;
}

程式碼第 10 行和第 15 行用 Lambda 表示式替代匿名內部類,可見程式碼變得簡潔。通過以上示例我們發現,Lambda 表示式是一個匿名函式(方法)程式碼塊,可以作為表示式、方法引數和方法返回值。

Lambda 表示式標準語法形式如下:

(引數列表) -> {
  // Lambda表示式體
}

->被稱為箭頭操作符或 Lambda 操作符,箭頭操作符將 Lambda 表示式拆分成兩部分:

  • 左側:Lambda 表示式的引數列表。
  • 右側:Lambda 表示式中所需執行的功能,用{ }包起來,即 Lambda 體。

Java Lambda 表示式的優缺點

優點:

  1. 程式碼簡潔,開發迅速
  2. 方便函數語言程式設計
  3. 非常容易進行平行計算
  4. Java 引入 Lambda,改善了集合操作(引入 Stream API)

缺點:

  1. 程式碼可讀性變差
  2. 在非平行計算中,很多計算未必有傳統的 for 效能要高
  3. 不容易進行除錯

函式式介面

Lambda 表示式實現的介面不是普通的介面,而是函式式介面。如果一個介面中,有且只有一個抽象的方法(Object 類中的方法不包括在內),那這個介面就可以被看做是函式式介面。這種介面只能有一個方法。如果介面中宣告多個抽象方法,那麼 Lambda 表示式會發生編譯錯誤:

The target type of this expression must be a functional interface

這說明該介面不是函式式介面,為了防止在函式式介面中宣告多個抽象方法,Java 8 提供了一個宣告函式式介面註解 @FunctionalInterface,示例程式碼如下。

// 可計算介面@FunctionalInterfacepublic interface Calculable {    // 計算兩個int數值    int calculateInt(int a, int b);}

在介面之前使用 @FunctionalInterface 註解修飾,那麼試圖增加一個抽象方法時會發生編譯錯誤。但可以新增預設方法和靜態方法。

@FunctionalInterface 註解與 @Override 註解的作用類似。Java 8 中專門為函式式介面引入了一個新的註解 @FunctionalInterface。該註解可用於一個介面的定義上,一旦使用該註解來定義介面,編譯器將會強制檢查該介面是否確實有且僅有一個抽象方法,否則將會報錯。需要注意的是,即使不使用該註解,只要滿足函式式介面的定義,這仍然是一個函式式介面,使用起來都一樣。

提示:Lambda 表示式是一個匿名方法程式碼,Java 中的方法必須宣告在類或介面中,那麼 Lambda 表示式所實現的匿名方法是在函式式介面中宣告的

Lambda表示式的使用

訪問變數

Lambda 表示式可以訪問所在外層作用域定義的變數,包括成員變數和區域性變數。

訪問成員變數

成員變數包括例項成員變數和靜態成員變數。

在 Lambda 表示式中可以訪問這些成員變數,此時的 Lambda 表示式與普通方法一樣,可以讀取成員變數,也可以修改成員變數。

public class LambdaDemo {
    // 例項成員變數
    private int value = 10;
    // 靜態成員變數
    private static int staticValue = 5;

    // 靜態方法,進行加法運算
    public static Calculable add() {
        Calculable result = (int a, int b) -> {
            // 訪問靜態成員變數,不能訪問例項成員變數
            staticValue++;
            int c = a + b + staticValue;
            // this.value;
            return c;
        };
        return result;
    }

    // 例項方法,進行減法運算
    public Calculable sub() {
        Calculable result = (int a, int b) -> {
            // 訪問靜態成員變數和例項成員變數
            staticValue++;
            this.value++;
            int c = a - b - staticValue - this.value;
            return c;
        };
        return result;
    }
}

LambdaDemo 類中宣告一個例項成員變數 value 和一個靜態成員變數 staticValue。此外,還聲明瞭靜態方法 add(見程式碼第 8 行)和例項方法 sub(見程式碼第 20 行)。add 方法是靜態方法,靜態方法中不能訪問例項成員變數,所以程式碼第 13 行的 Lambda 表示式中也不能訪問例項成員變數,也不能訪問例項成員方法。

sub 方法是例項方法,例項方法中能夠訪問靜態成員變數和例項成員變數,所以程式碼第 23 行的 Lambda 表示式中可以訪問這些變數,當然例項方法和靜態方法也可以訪問。當訪問例項成員變數或例項方法時可以使用 this,如果不與區域性變數發生衝突情況下可以省略 this。

訪問區域性變數

對於成員變數的訪問 Lambda 表示式與普通方法沒有區別,但是訪問區域性變數時,變數必須是 final 型別的(不可改變)。

public class LambdaDemo {
    // 例項成員變數
    private int value = 10;
    // 靜態成員變數
    private static int staticValue = 5;

    // 靜態方法,進行加法運算
    public static Calculable add() {
        // 區域性變數
        int localValue = 20;
        Calculable result = (int a, int b) -> {
            // localValue++;
            // 編譯錯誤
            int c = a + b + localValue;
            return c;
        };
        return result;
    }

    // 例項方法,進行減法運算
    public Calculable sub() {
        // final區域性變數
        final int localValue = 20;
        Calculable result = (int a, int b) -> {
            int c = a - b - staticValue - this.value;
            // localValue = c;
            // 編譯錯誤
            return c;
        };
        return result;
    }
}

上述程式碼第 10 行和第 23 行都宣告一個區域性變數 localValue,Lambda 表示式中訪問這個變數,如程式碼第 14 行和第 25 行。不管這個變數是否顯式地使用 final 修飾,它都不能在 Lambda 表示式中修改變數,所以程式碼第 12 行和第 26 行如果去掉註釋會發生編譯錯誤。

注意:Lambda 表示式只能訪問區域性變數而不能修改,否則會發生編譯錯誤,但對靜態變數和成員變數可讀可寫。

方法引用

方法引用可以理解為 Lambda 表示式的快捷寫法,它比 Lambda 表示式更加的簡潔,可讀性更高,有很好的重用性。如果實現比較簡單,複用的地方又不多,推薦使用 Lambda 表示式,否則應該使用方法引用。

Java 8 之後增加了雙冒號::運算子,該運算子用於“方法引用”,注意不是呼叫方法。“方法引用”雖然沒有直接使用 Lambda 表示式,但也與 Lambda 表示式有關,與函式式介面有關。 方法引用的語法格式如下:

ObjectRef::methodName 

其中,ObjectRef 是類名或者例項名,methodName 是相應的方法名。

注意:被引用方法的引數列表和返回值型別,必須與函式式介面方法引數列表和方法返回值型別一致,示例程式碼如下。

public class LambdaDemo {
    // 靜態方法,進行加法運算
    // 引數列表要與函式式介面方法calculateInt(int a, int b)相容
    public static int add(int a, int b) {
        return a + b;
    }

    // 例項方法,進行減法運算
    // 引數列表要與函式式介面方法calculateInt(int a, int b)相容
    public int sub(int a, int b) {
        return a - b;
    }
}

LambdaDemo 類中提供了一個靜態方法 add,一個例項方法 sub。這兩個方法必須與函式式介面方法引數列表一致,方法返回值型別也要保持一致。

public class HelloWorld {
    public static void main(String[] args) {
        int n1 = 10;
        int n2 = 5;
        // 列印加法計算結果
        display(LambdaDemo::add, n1, n2);
        LambdaDemo d = new LambdaDemo();
        // 列印減法計算結果 
        display(d::sub, n1, n2);
    }

    /**
     * 列印計算結果
     *
     * @param calc Lambda表示式
     * @param n1   運算元1
     * @param n2   運算元2
     */
    public static void display(Calculable calc, int n1, int n2) {
        System.out.println(calc.calculateInt(n1, n2));
    }
}

程式碼第 18 行宣告 display 方法,第一個引數 calc 是 Calculable 型別,它可以接受三種物件:Calculable 實現物件、Lambda 表示式和方法引用。程式碼第 6 行中第一個實際引數LambdaDemo::add是靜態方法的方法引用。程式碼第 9 行中第一個實際引數d::sub,是例項方法的方法引用,d 是 LambdaDemo 例項。

提示:程式碼第 6 行的LambdaDemo::add和第 9 行的d::sub是方法引用,此時並沒有呼叫方法,只是將引用傳遞給 display 方法,在 display 方法中才真正呼叫方法。