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