介面與內部類
本文主要整理了一些作者看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啟動失敗的問題。