JAVA基礎之介面與內部類

山人西來發表於2020-11-17

介面與內部類

本文主要整理了一些作者看JAVA核心技術卷第六章遇到的難點以及其思考, 歡迎小夥伴及時指出錯誤!

1. Lambda表示式

1. 關於懶計算

在JAVA8中, 提供了 Supplier這個介面實現懶計算

原理是這樣的, 主要依據是以下三個原理

  • 在JAVA8的新特性中, 只要一個介面只有一個抽象方法(不包括default和static), 那麼這個介面就會被被認為是一個函式式介面, 可以使用lambda表示式, 而註解 @FunctionalInterface 和我們的 @Override 一樣, 用於提示, 不寫也可以, 但是建議寫

  • lambda表示式在被呼叫時才執行

  • lambda表示式可以做型別推斷(不是太重要的原理)

我們可以觀察Objects.requireNoNull 方法, 在引數為 null 會丟擲一個異常, 異常的內容與我們傳遞的第二個引數有關

這個方法有三個過載, 我們主要關注的是有兩個方法的過載

  • 首先是傳統的過載
public static <T> T requireNonNull(T obj, String message) {
    if (obj == null)
        throw new NullPointerException(message);
    return obj;
}

這裡直接傳入了一個String型別的message, 這樣看雖然沒什麼不妥, 但是設想一下, 如果我們的 null 不是一個經常出現的結果, 同時我們的String是通過呼叫某個方法得到的, 這樣每次執行非空判斷, 都會呼叫我們對message寫的方法, 比如我們傳入一個時間

new LocalDate(1970, 1, 1);

這樣如果有大量的程式呼叫這個判斷, 同時並沒有那麼多null, 會造成效能浪費

  • 基於懶計算的優化
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
    if (obj == null)
        throw new NullPointerException(messageSupplier.get());
    return obj;
}

與上面不同, 這裡的第二個引數是一個 Supplier 介面, 我們從第一點可以得知, 由於該介面只有一個抽象方法, 因此它是一個函式式介面, 我們可以使用Lambda表示式; 又根據我們第二點, lambda表示式只有在被呼叫的時候才會執行, 那麼如果我們沒有那麼多的空判斷, 這個方法就不會執行, 當我們的第二個引數很複雜(比如要向資料庫查詢資料), 這樣就可以節省了大量的效能, 第二個引數的lambda表示式我們可以這樣寫

() -> new LocalDate(1970, 1, 1)

類似的, 與懶計算設計思路相似的優化方法還有懶載入, 即頁面的元素(比如圖片或者視訊等)只有在被呼叫(比如我們往下翻頁的時候)才載入, 這樣大大緩解了伺服器的壓力與網路的壓力, 畢竟不是所有人都會看到底的

2. Predicate介面

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

可以看出, 這個介面同樣只有一個抽象方法, 因此他也是一個函式式介面, 這個函式式介面很有用, 因為它可以返回一個布林值, 在我們傳入一個方法可以做判斷

3. 關於方法引用

  • 方法引用主要有三種情況

    • object :: instanceMethod 等價於向方法傳遞引數的lambda表示式
    • Class :: instanceMethod 等價於第一個引數作為方法的隱式引數(即this, 表示該引數自己, 方法包括定義自己的屬性或者呼叫自己的一些方法, 最後的結果會返回到這個引數上), 其餘的引數會傳遞到方法
    • Class :: staticMethod 等價於所有的引數都傳遞到靜態方法中, 與上面的區別是有沒有隱式引數(即改變了自己的值)
  • 雖然我們可以用lambda表示式來等價方法引用, 但是兩者最重要的區別是 方法引用會立即執行, 而lambda表示式只有在呼叫的時候才會執行

  • 只有當lambda表示式的方法體只呼叫一個方法而不做其他操作時, 我們才可以將lambda表示式重寫為方法引用, 比如下面的就不可以, 因為它除了方法呼叫, 還進行了比較

s -> s.length() == 0

4. 關於構造器引用

  • 構造器引用的基本結構

    • Class :: new
    • 表示 Class 構造器的一個引用, 引用的構造器取決於上下文, 編譯器會自動推導
  • 陣列型別的構造器引用

    • Class[] :: new

    • 等價於

      x -> new Class[x]
      

      即建立了一個指定型別的物件陣列

5. 關於變數的作用域

  • lambda可以捕獲外圍作用域中的變數的值
  • lambda表示式中捕獲的值必須實際上是 事實最終變數, 即初始化後就不再為其賦新值, 這是由於lambda表示式在呼叫後才執行, 如果改變的話會造成不安全
  • lambda表示式的體與巢狀塊有相同的作用域, 我們可以理解為, 在lambda表示式左側傳入的變數和上下文的變數的作用域是一致的
    • 在lambda表示式中, 沒與引數也要寫括號 () -> xxx
    • 在lambda表示式中, 會自動推斷變數型別, 可以不寫, (String first) -> xxx 和 (first) -> xxx是一樣的, 因此如果上文有first這個變數, 這裡就會報變數衝突的錯誤

2. 內部類

1. 區域性內部類

  • 在一個方法中區域性定義的類叫做區域性內部類

  • 宣告區域性類時不能有訪問說明符(即 public private protected), 作用域僅限於宣告這個區域性類的類中 ==> 可以訪問類的全部屬性, 包括私有屬性

  • 優點: 對外部世界完全遮蔽

2. 匿名內部類

  • 如果只想建立區域性內部類的一個物件而不需要給其指定名字, 可以使用匿名內部類

  • new SuperType(construction parameters) 
    {
        inner class methods and data
    }
    
  • SuperType可以是介面 ==> 匿名內部類實現這個介面

  • SuperType可以使一個類 ==> 匿名內部類擴充這個類

  • 如果引數列表的結束小括號後面跟著一個開始大括號, 就是在定義匿名內部類

  • 與lambda表示式最大的區別

    • lambda編譯後不會生成class檔案,那麼也就略過了類的載入、驗證、解析等。相當於是在執行時再進行相應的操作
    • 這裡主要體現在對Spring的影響中, 在spring注入過程中,無法注入含確定型別的入參和出參方法的實現類,所以,才會出現無法確定型別,導致注入失敗,從而springboot啟動失敗的問題。

相關文章