OneAPM
摘要:此篇文章主要介紹Java8 Lambda 運算式產生的背景和用法,以及 Lambda 運算式與匿名類的不同等,本文系OneAPM工程師編譯整理,
Java是一流的面向物件語言,除了部分簡單資料型別,Java 中的一切都是物件,即使陣列也是一種物件,每個類創建的實體也是物件,在 Java 中定義的函式或方法不可能完全獨立,也不能將方法作為引數或回傳一個方法給實體,
從 Swing 開始,我們總是通過匿名類給方法傳遞函式功能,以下是舊版的事件監聽代碼:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
在上面的例子里,為了給 Mouse 監聽器添加自定義代碼,我們定義了一個匿名內部類 MouseAdapter 并創建了它的物件,通過這種方式,我們將一些函式功能傳給 addMouseListener 方法,
簡而言之,在 Java 里將普通的方法或函式像引數一樣傳值并不簡單,為此,Java 8 增加了一個語言級的新特性,名為Lambda 運算式,
為什么 Java 需要 Lambda 運算式?
如果忽視注解(Annotations)、泛型(Generics)等特性,自 Java 語言誕生時起,它的變化并不大,Java 一直都致力維護其物件至上的特征,在使用過 JavaScript 之類的函式式語言之后,Java 如何強調其面向物件的本質,以及原始碼層的資料型別如何嚴格變得更加清晰可感,其實,函式對 Java 而言并不重要,在 Java 的世界里,函式無法獨立存在,
在函式式編程語言中,函式是一等公民,它們可以獨立存在,你可以將其賦值給一個變數,或將他們當做引數傳給其他函式,JavaScript 是最典型的函式式編程語言,點擊此處以及此處可以清楚了解 JavaScript 這種函式式語言的好處,函式式語言提供了一種強大的功能——閉包,相比于傳統的編程方法有很多優勢,閉包是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自于創建它的作用域,Java 現在提供的最接近閉包的概念便是 Lambda 運算式,雖然閉包與 Lambda 運算式之間存在顯著差別,但至少 Lambda 運算式是閉包很好的替代者,
在 Steve Yegge 辛辣又幽默的博客文章里,描繪了 Java 世界是如何嚴格地以名詞為中心的,如果你還沒看過,趕緊去讀吧,寫得非常風趣幽默,而且恰如其分地解釋了為什么 Java 要引進 Lambda 運算式,
Lambda 運算式為 Java 添加了缺失的函式式編程特點,使我們能將函式當做一等公民看待,盡管不完全正確,我們很快就會見識到 Lambda 與閉包的不同之處,但是又無限地接近閉包,在支持一類函式的語言中,Lambda 運算式的型別將是函式,但是,在 Java 中,Lambda 運算式是物件,他們必須依附于一類特別的物件型別——函式式介面(functional interface),我們會在后文詳細介紹函式式介面,
Mario Fusco 的這篇思路清晰的文章介紹了為什么 Java 需要 Lambda 運算式,他解釋了為什么現代編程語言必須包含閉包這類特性,
Lambda 運算式簡介
Lambda 運算式是一種匿名函式(對 Java 而言這并不完全正確,但現在姑且這么認為),簡單地說,它是沒有宣告的方法,也即沒有訪問修飾符、回傳值宣告和名字,
你可以將其想做一種速記,在你需要使用某個方法的地方寫上它,當某個方法只使用一次,而且定義很簡短,使用這種速記替代之尤其有效,這樣,你就不必在類中費力寫宣告與方法了,
Java 中的 Lambda 運算式通常使用(argument) -> (body)語法書寫,例如:
(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }
以下是一些 Lambda 運算式的例子:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
Lambda 運算式的結構
讓我們了解一下 Lambda 運算式的結構,
- 一個 Lambda 運算式可以有零個或多個引數
- 引數的型別既可以明確宣告,也可以根據背景關系來推斷,例如:
(int a)與(a)效果相同 - 所有引數需包含在圓括號內,引數之間用逗號相隔,例如:
(a, b)或(int a, int b)或(String a, int b, float c) - 空圓括號代表引數集為空,例如:
() -> 42 - 當只有一個引數,且其型別可推導時,圓括號()可省略,例如:
a -> return a*a - Lambda 運算式的主體可包含零潭訓多條陳述句
- 如果 Lambda 運算式的主體只有一條陳述句,花括號{}可省略,匿名函式的回傳型別與該主體運算式一致
- 如果 Lambda 運算式的主體包含一條以上陳述句,則運算式必須包含在花括號{}中(形成代碼塊),匿名函式的回傳型別與代碼塊的回傳型別一致,若沒有回傳則為空
什么是函式式介面
在 Java 中,Marker(標記)型別的介面是一種沒有方法或屬性宣告的介面,簡單地說,marker 介面是空介面,相似地,函式式介面是只包含一個抽象方法宣告的介面,
java.lang.Runnable就是一種函式式介面,在 Runnable 介面中只宣告了一個方法void run(),相似地,ActionListener 介面也是一種函式式介面,我們使用匿名內部類來實體化函式式介面的物件,有了 Lambda 運算式,這一方式可以得到簡化,
每個 Lambda 運算式都能隱式地賦值給函式式介面,例如,我們可以通過 Lambda 運算式創建 Runnable 介面的參考,
Runnable r = () -> System.out.println("hello world");
當不指明函式式介面時,編譯器會自動解釋這種轉化:
new Thread(
() -> System.out.println("hello world")
).start();
因此,在上面的代碼中,編譯器會自動推斷:根據執行緒類的建構式簽名public Thread(Runnable r) { },將該 Lambda 運算式賦給 Runnable 介面,
以下是一些 Lambda 運算式及其函式式介面:
Consumer<Integer> c = (int x) -> { System.out.println(x) };
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
Predicate<String> p = (String s) -> { s == null };
@FunctionalInterface是 Java 8 新加入的一種介面,用于指明該介面型別宣告是根據 Java 語言規范定義的函式式介面,Java 8 還宣告了一些 Lambda 運算式可以使用的函式式介面,當你注釋的介面不是有效的函式式介面時,可以使用 @FunctionalInterface 解決編譯層面的錯誤,
以下是一種自定義的函式式介面: @FunctionalInterface public interface WorkerInterface {
public void doSomeWork();
}
根據定義,函式式介面只能有一個抽象方法,如果你嘗試添加第二個抽象方法,將拋出編譯時錯誤,例如:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
錯誤:
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error
函式式介面定義好后,我們可以在 API 中使用它,同時利用 Lambda 運算式,例如:
//定義一個函式式介面
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
//invoke doSomeWork using Annonymous class
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker invoked using Anonymous class");
}
});
//invoke doSomeWork using Lambda expression
execute( () -> System.out.println("Worker invoked using Lambda expression") );
}
}
輸出:
Worker invoked using Anonymous class
Worker invoked using Lambda expression
這上面的例子里,我們創建了自定義的函式式介面并與 Lambda 運算式一起使用,execute() 方法現在可以將 Lambda 運算式作為引數,
Lambda 運算式舉例
學習 Lambda 運算式的最好方式是學習例子,
執行緒可以通過以下方法初始化:
//舊方法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
//新方法:
new Thread(
() -> System.out.println("Hello from thread")
).start();
事件處理可以使用 Java 8 的 Lambda 運算式解決,下面的代碼中,我們將使用新舊兩種方式向一個 UI 組件添加 ActionListener:
//Old way:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("The button was clicked using old fashion code!");
}
});
//New way:
button.addActionListener( (e) -> {
System.out.println("The button was clicked. From Lambda expressions !");
});
以下代碼的作用是列印出給定陣列中的所有元素,注意,使用 Lambda 運算式的方法不止一種,在下面的例子中,我們先是用常用的箭頭語法創建 Lambda 運算式,之后,使用 Java 8 全新的雙冒號(::)運算子將一個常規方法轉化為 Lambda 運算式:
//Old way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
System.out.println(n);
}
//New way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
//or we can use :: double colon operator in Java 8
list.forEach(System.out::println);
在下面的例子中,我們使用斷言(Predicate)函式式介面創建一個測驗,并列印所有通過測驗的元素,這樣,你就可以使用 Lambda 運算式規定一些邏輯,并以此為基礎有所作為:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.println("Print all numbers:");
evaluate(list, (n)->true);
System.out.println("Print no numbers:");
evaluate(list, (n)->false);
System.out.println("Print even numbers:");
evaluate(list, (n)-> n%2 == 0 );
System.out.println("Print odd numbers:");
evaluate(list, (n)-> n%2 == 1 );
System.out.println("Print numbers greater than 5:");
evaluate(list, (n)-> n > 5 );
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.println(n + " ");
}
}
}
}
輸出:
Print all numbers: 1 2 3 4 5 6 7
Print no numbers:
Print even numbers: 2 4 6
Print odd numbers: 1 3 5 7
Print numbers greater than 5: 6 7
下面的例子使用 Lambda 運算式列印數值中每個元素的平方,注意我們使用了 .stream() 方法將常規陣列轉化為流,Java 8 增加了一些超棒的流 APIs,java.util.stream.Stream介面包含許多有用的方法,能結合 Lambda 運算式產生神奇的效果,我們將 Lambda 運算式x -> x*x傳給 map() 方法,該方法會作用于流中的所有元素,之后,我們使用 forEach 方法列印資料中的所有元素:
//Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
//New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
下面的例子會計算給定數值中每個元素平方后的總和,請注意,Lambda 運算式只用一條陳述句就能達到此功能,這也是 MapReduce 的一個初級例子,我們使用 map() 給每個元素求平方,再使用 reduce() 將所有元素計入一個數值:
//Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
int x = n * n;
sum = sum + x;
}
System.out.println(sum);
//New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);
Lambda 運算式與匿名類的區別
使用匿名類與 Lambda 運算式的一大區別在于關鍵詞的使用,對于匿名類,關鍵詞this解讀為匿名類,而對于 Lambda 運算式,關鍵詞this解讀為寫就 Lambda 的外部類,
Lambda 運算式與匿名類的另一不同在于兩者的編譯方法,Java 編譯器編譯 Lambda 運算式并將他們轉化為類里面的私有函式,它使用 Java 7 中新加的invokedynamic指令動態系結該方法,關于 Java 如何將 Lambda 運算式編譯為位元組碼,Tal Weiss 寫了一篇很好的文章,
到此為止啦,親們!
Mark Reinhold,甲骨文的首席架構師,將 Lambda 運算式描述為該編程模型最大的提升——比泛型(generics)還強大,事實的確如此,Lambda 運算式賦予了 Java程式員相較于其他函式式編程語言缺失的特性,結合虛擬擴展方法之類的特性,Lambda 運算式能寫出一些極好的代碼,
希望這篇文章能讓您對Java 8的新特性所有了解,
原文地址:http://viralpatel.net/blogs/Lambda-expressions-java-tutorial/
OneAPM for Java能夠深入到所有 Java 應用內部完成應用性能管理和監控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實用戶體驗監控、服務器監控和端到端的應用性能管理,想閱讀更多技術文章,請訪問OneAPM 官方博客,
青山不改,綠水常流,謝謝大家!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/545892.html
標籤:架構設計
上一篇:訂單超時怎么處理?我們用這種方案
下一篇:我們要選擇哪個訊息佇列產品?
