Java 8 - 04 类型检查、类型推断以及限制
文章目錄
- Pre
- 類型檢查
- 同樣的 Lambda,不同的函數式接口
- 菱形運算符
- 特殊的void兼容規則
- 類型推斷
- 使用局部變量
Pre
當我們第一次提到Lambda表達式時,說它可以為函數式接口生成一個實例。然而,Lambda
表達式本身并不包含它在實現哪個函數式接口的信息。為了全面了解Lambda表達式,women 應該知道Lambda的實際類型是什么 .
類型檢查
Lambda的類型是從使用Lambda的上下文推斷出來的。 上下文(比如,接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。
舉個例子
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);類型檢查過程可以分解為如下所示。
? 首先,我們要找出 filter 方法的聲明。
? 第二,要求它是 Predicate<Apple> (目標類型)對象的第二個正式參數。
? 第三, Predicate<Apple> 是一個函數式接口,定義了一個叫作 test 的抽象方法。
? 第四, test 方法描述了一個函數描述符,它可以接受一個 Apple ,并返回一個 boolean 。
? 最后, filter 的任何實際參數都必須匹配這個要求
這段代碼是有效的,因為我們所傳遞的Lambda表達式也同樣接受 Apple 為參數,并返回一個boolean 。請注意,如果Lambda表達式拋出一個異常,那么抽象方法所聲明的 throws 語句也必須與之匹配
同樣的 Lambda,不同的函數式接口
有了目標類型的概念,同一個Lambda表達式就可以與不同的函數式接口聯系起來,只要它們的抽象方法簽名能夠兼容.
我們來看下這兩個函數式接口
這兩個函數式接口 都是 什么也不接受且返回一個泛型 T 的函數, 所以 下面兩個賦值是有效的
Callable<Integer> integerCallable = () -> 18;PrivilegedAction<Integer> privilegedAction = () -> 18;第一個賦值的目標類型是 Callable<Integer>
第二個賦值的目標類型是PrivilegedAction<Integer>
再舉個栗子 : 同一個Lambda可用于多個不同的函數式接口
Comparator<Enginner> enginnerComparator = (e1, e2) -> e1.getJob().compareTo(e2.getJob());ToIntBiFunction<Enginner, Enginner> toIntBiFunction = (e1, e2) -> e1.getJob().compareTo(e2.getJob());BiFunction<Enginner, Enginner, Integer> toIntFunction = (e1, e2) -> e1.getJob().compareTo(e2.getJob());Comparator 、 ToIntBiFunction 、 BiFunction 都是返回一個int類型的的函數
菱形運算符
Java 7中已經引入了菱形運算符( <> ),利用泛型推斷從上下文推斷類型的思想。 一個類實例表達式可以出現在兩個或更多不同的上下文中,并會像下面這樣推斷出適當的類型參數。
List<String> listOfStrings = new ArrayList<>(); List<Integer> listOfIntegers = new ArrayList<>();特殊的void兼容規則
如果一個Lambda的主體是一個語句表達式, 它就和一個返回 void 的函數描述符兼容(當然需要參數列表也兼容)。
舉個例子:
以下兩行都是合法的,盡管 List 的 add 方法返回了一個boolean ,而不是 Consumer 上下文( T -> void )所要求的 void
List<String> stringList = new ArrayList<>();// Predicate返回了一個booleanPredicate<String> predicate = s -> stringList.add(s);// Consumer返回了一個voidConsumer<String> consumer = s -> stringList.add(s);經過了這幾個小demo ,是不是能夠很好地理解在什么時候以及在哪里可以使用Lambda表達式了。Lambda表達式可以從賦值的上下文、方法調用的上下文(參數和返回值),以及類型轉換的上下文中獲得目標類型
來個小測驗
類型檢查——為什么下面的代碼不能編譯呢? Object o = () -> {System.out.println("Tricky example"); };答案: Lambda表達式的上下文是 Object (目標類型)。但 Object 不是一個函數式接口 。 為了解決這個問題,可以把目標類型改成 Runnable ,它的函數描述符是 () -> void :Runnable r = () -> {System.out.println("Tricky example"); };類型推斷
剛才已經討論了如何利用目標類型來檢查一個Lambda是否可以用于某個特定的上下文。其實,
它也可以用來做一些略有不同的事:推斷Lambda參數的類型,我們來看下。
Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標注參數類型.
舉個例子
List<Enginner> goEngineerList = filter(enginnerList,a-> a.getJob().equals("GO"));參數 a 沒有顯式類型 .
再舉個栗子 ,Lambda表達式有多個參數,代碼可讀性的好處就更為明顯
// 沒有類型推斷,因為給o1,o2指定了Enginner 類型Comparator<Enginner> comparator = (Enginner o1, Enginner o2) -> o1.getJob().compareTo(o2.getJob());// 有類型推斷,因為沒有給o1,o2指定了Enginner 類型Comparator<Enginner> comparator2 = ( o1, o2) -> o1.getJob().compareTo(o2.getJob());個人感覺,第二種寫法更簡單 。
當Lambda僅有一個類型需要推斷的參數時,參數名稱兩邊的括號也可以省略。
使用局部變量
上面所介紹的所有Lambda表達式都只用到了其主體里面的參數。但Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。 它們被稱作捕獲Lambda。
舉個例子
int num = 1;Runnable runnable = ()->System.out.println(num);這么做雖然有點啰嗦,我們這里想要討論的是 使用外部的變量有什么限制嗎?
如果你想要對這個變量進行操作,之前的lambda就報錯了。所以說Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量,但是局部變量必須顯式聲明為 final.
換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲實例變量可以被看作捕獲最終局部變量 this 。) 如上圖。
為什么會這樣呢?
- 第一: 實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之后,去訪問該變量。因此,Java在訪問自由局部變量時,實際上是在訪問的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了——因此就有了這個限制
- 第二,這一限制不鼓勵你使用改變外部變量的典型命令式編程模式,這種模式會阻礙很容易做到的并行處理.
總結
以上是生活随笔為你收集整理的Java 8 - 04 类型检查、类型推断以及限制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 8 - 03 Lambda 函
- 下一篇: Java 8 - 05 方法引用