寫在前邊
- 聊到Java8新特性,我們第一反應想到的肯定是Lambda運算式和函式式介面的出現,要說ta到底有沒有在一定程度上“優化”了代碼的簡潔性呢?抑或是ta在一定程度上給程式員增加了閱讀和debug的難度,讓不少程式員頭疼,這期來接著“聊聊Java”,新特性篇只又愛又恨的Lambda,
Lambda運算式
實質屬于函式式編程的概念,可回傳一個介面的實作
執行緒中的應用
傳統方式
創建一個一次性的類
//一次性的類,用在new Thread中充當Runnable對的實作類
class runnable implements Runnable{
@Override
public void run() {
System.out.println("我在路上");
}
}
public class lambdaTest {
public static void main(String[] args) {
runnable runnable = new runnable();
Thread thread1 = new Thread(runnable);
}
}
(稍微優化)匿名內部類
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我在路上");
}
});
作用
- 避免匿名內部類定義過多
- 使代碼看起來簡潔
- 簡化代碼,只留下核心邏輯
函式式介面
定義:任何介面,如果只包含唯一一個抽象方法,那么它就是一個函式式介面
public interface Runnable{
void run();
}
為了避免后來人給這個介面添加函式后,導致該介面有多個函式,不再是函式式介面,我們可以在介面類的上方宣告@FunctionalInterface
所有的Lambda的型別都是一個介面
- 而Lambda運算式本身,就是這個介面的實作
如果定義成實作類,就會報錯
- 我們在程式編譯的時候并不知道我們最終創建出來的物件具體是哪個子類,直到運行時期才能得知,隨之我們可以讓這個like指向指向各種不同的類上,可以呼叫各種不同的子類方法,大大提高了程式的可擴展性
而這里我們用lambda實際上是等價于匿名內部類(沒有類名),實際創建出來的類是什么,我們不知道,所以我們會定義成介面,利用多型的向上轉型特性
關于多型的更多特性,在我的另一篇博客中 : 傳送門-> 多型
方法參考
Demo
//介面定義
interface parseIntNum{
//定義一個String轉化成Integer的方法
int pass(String s);
}
public static void main(String[] args) {
parseIntNum parseIntNum1;
parseIntNum parseIntNum2;
//原始lambda
parseIntNum1 = (str)-> Integer.parseInt(str);
System.out.println(parseIntNum1.pass("1"));
//方法參考改進版本
parseIntNum2 = Integer::parseInt;
System.out.println(parseIntNum2.pass("1"));
}
所謂方法參考,是指如果某個已經存在的方法,他的簽名和介面里邊定義的函式恰好一致,就可以直接傳入方法參考,
因為parseIntNum介面定義的方法是int pass(String s),和Integer中的靜態方法int parseInt(String s)相比,除了方法名外,方法引數一致,回傳型別相同,這就是我們說的方法簽名一致,可以直接傳入方法參考
方法參考的寫法
- 其實很簡單,只需要使用運算子雙冒號** "::"**
常見的方法參考
常見的參考形式
類名::靜態方法名
呼叫類的靜態方法
- 其實我們上邊使用Integer::parseInt 就等價于呼叫 Integer的靜態方法 parseInt
物件:實體方法
此處小小中二了一點hhhh
class Naruto{
public static void Rasengan(){
System.out.println("螺旋丸");
}
}
//介面定義
interface Ninja{
//定義一個奧義方法
void aoyi();
}
public class methodQuote {
//通過參考Naurto的螺旋丸
Ninja ninja=Naruto::Rasengan;
//再發動奧義
ninjia.aoyi();
}
資料型別:new
public static void main(String[] args) {
//方式一
IntFunction<int []> arr1 = new IntFunction<int[]>() {
@Override
public int[] apply(int num) {
return new int[num];
}
};
arr1.apply(10);
//方式二(方法參考)
IntFunction<int []> arr2 = int[]::new;
arr2.apply(10);
}
- 除此之外還有很多種形式,這里就不過多贅述了
開發常用
常用小技巧:遍歷陣列列印
對比三種方法,可以看出簡潔程度
ArrayList<Integer> arrayList=new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
//匿名內部類
arrayList.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
//lambda優化
arrayList.forEach((integer)-> System.out.println(integer));
//方法參考列印,用方法參考替代了我們的匿名內部類(相當于替代了lambda)
arrayList.forEach(System.out::println);
遍歷Map
Map<Integer, String> map = new HashMap<>();
map.forEach((k,v)->System.out.println(v));
Lambda運算式作用域
訪問區域普通變數
- 只能參考標記了final的外層區域變數
即 : 不能在lambda 內部修改定義在域外的區域變數,否則會編譯錯誤,
- 特殊情況下,區域變數也可以不用宣告為 final,但是必須不可被后面的代碼修改(即隱性的具有 final 的語意)
破壞隱式final(再次修改)
package com.melo.notes.lambdaTest;
public class TestFinal {
interface MeiYan{
Integer change(String str);
}
public static void main(String[] args) {
//定義區域變數
String temp = "222";
//寫成一行可以不用return
MeiYan meiYan = (str -> Integer.valueOf(str+temp));
//再次修改,不符合隱式final定義
temp = "333";
Integer str =meiYan.change("111") ;
System.out.println(str);
}
}
- 不允許宣告一個與區域變數同名的引數或者區域變數,
訪問區域參考變數
import java.util.ArrayList;
public class TestArray {
interface MeiYan{
Integer change();
}
void testArrayList(){
ArrayList<String> list = new ArrayList<>();
list.add("111");
//訪問外部參考區域參考變數
MeiYan meiYan = (() -> Integer.valueOf(list.get(0)));
//修改區域參考變數
list.set(0,"222");
Integer str =meiYan.change();
System.out.println(str);
}
public static void main(String[] args) {
new TestArray().testArrayList();
}
}
- 訪問參考變數的話,是沒有問題的,因為lambda可以感知到外部對該參考變數的改變,不會出現資料不同步問題
具體可以看下邊的"理解"有更詳細的解釋
訪問靜態變數和實體變數
都是可以的,再次修改也不會報錯
代碼
public class TestFinal {
//靜態變數
static String StaticTemp;
//實體變數
String instanceTemp;
interface MeiYan{
Integer change(String str);
}
void testStatic(){
StaticTemp="222";
MeiYan meiYan = (str -> Integer.valueOf(str+StaticTemp));
StaticTemp="333";
Integer str =meiYan.change("111") ;
System.out.println(str);
}
public static void main(String[] args) {
new TestFinal().testStatic();
}
理解
實體變數和區域變數的區別
這里的關鍵問題轉換到: 實體變數和區域變數的區別是什么?
- 實體變數存盤在堆上
堆是在執行緒之間共享的,
- 而區域變數存盤在堆疊上
有可能會有相應的執行緒問題(見下)
執行緒問題
比如A執行緒分配了一個變數temp
- 有可能Lambda是在另一個執行緒B中使用的,使用Lambda的執行緒B,可能會在分配該變數的執行緒A將temp變數識訓之后,還去訪問temp變數,
資料不同步問題
聯想一下我們普通的方法,方法的引數只是存活在方法堆疊這個空間里,而我們lambda的{ }里實際上也相當于一個方法塊,
- 如果我們這里的方法塊訪問了外部的變數,而這個變數只是一個普通資料型別的話,相當于只是訪問到了一份副本,當外部對這個變數進行修改時,lambda內部(只有副本)是無法感知到這個變數的修改的,
因此為了防止出現資料不同步的問題,java8就限制了:lambda訪問區域普通資料型別變數時,需要用final修飾或使用隱式final的方法!
擴展--值傳遞還是參考傳遞?
- 這里只是提到了,對于普通資料型別的區域變數會有限制,而對于參考型別的區域變數呢?這就涉及到了Java的引數傳值究竟是值傳遞還是參考傳遞了,
先占個坑位,以后還會在本專欄中更新一篇關于這方面的博客!!!
疑問解決
一開始有個疑問,看起來方法參考省略了引數,那我們Intger.parseInt是去對誰操作?
- 其實是自己混淆了lambda,lambda定義的時候那個引數,根本不是實際的引數
可以說那個引數,只是為方法體服務的,只是方法體里邊會用到.
而我們都用了方法參考了,前提就是引數和回傳值一樣,方法體也是我們想要實作的內容,這時自然而然都不用我們寫方法體了,那方法體所依賴的引數也自然不用派上用場了
寫在最后
- 最近要開始忙活專案了,對于這些基礎知識的更深入掌握,也許會放在以后準備面試的時候,目前理解不深,如有錯誤之處還望指出!
本專欄還會斷斷續續更新一些Java基礎知識和面經,提前為以后面試打下基礎!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/347265.html
標籤:其他
上一篇:spring-cloud-square原始碼速讀(retrofit + okhttp篇)
下一篇:壞了!面試官問我垃圾回識訓制
