寫在前邊
- 聊到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基礎知識和面經,提前為以后面試打下基礎!