天天看點

[轉]Java 8新特性探究(lambda)函數式接口 Lambda文法方法引用總結什麼是預設方法,為什麼要有預設方法簡單的例子 java 8抽象類與接口對比 多重繼承的沖突說明總結 1.關于JSR335 2.外部VS内部疊代 3.Stream API 4.總結

原文位址:http://my.oschina.net/benhaile/blog/175012

目錄[-]

  • 函數式接口
  • Lambda文法
  • 方法引用
  • 總結

函數式接口

函數式接口(functional interface 也叫功能性接口,其實是同一個東西)。簡單來說,函數式接口是隻包含一個方法的接口。比如Java标準庫中的java.lang.Runnable和 java.util.Comparator都是典型的函數式接口。java 8提供 @FunctionalInterface作為注解,這個注解是非必須的,隻要接口符合函數式接口的标準(即隻包含一個方法的接口),虛拟機會自動判斷, 但 最好在接口上使用注解@FunctionalInterface進行聲明,以免團隊的其他人員錯誤地往接口中添加新的方法。

Java中的lambda無法單獨出現,它需要一個函數式接口來盛放,lambda表達式方法體其實就是函數接口的實作,下面講到文法會講到

Lambda文法

包含三個部分

  1. 一個括号内用逗号分隔的形式參數,參數是函數式接口裡面方法的參數
  2. 一個箭頭符号:->
  3. 方法體,可以是表達式和代碼塊,方法體函數式接口裡面方法的實作,如果是代碼塊,則必須用{}來包裹起來,且需要一個return 傳回值,但有個例外,若函數式接口裡面方法傳回值是void,則無需{}

總體看起來像這樣

?

1

(parameters) -> expression 或者 (parameters) -> { statements; }

看一個完整的例子,友善了解

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

public

class

TestLambda {

public

static

void

runThreadUseLambda() {

//Runnable是一個函數接口,隻包含了有個無參數的,傳回void的run方法;

//是以lambda表達式左邊沒有參數,右邊也沒有return,隻是單純的列印一句話

new

Thread(() ->System.out.println(

"lambda實作的線程"

)).start(); 

}

public

static

void

runThreadUseInnerClass() {

//這種方式就不多講了,以前舊版本比較常見的做法

new

Thread(

new

Runnable() {

@Override

public

void

run() {

System.out.println(

"内部類實作的線程"

);

}

}).start();

}

public

static

void

main(String[] args) {

TestLambda.runThreadUseLambda();

TestLambda.runThreadUseInnerClass();

}

}

可以看出,使用lambda表達式設計的代碼會更加簡潔,而且還可讀。

方法引用

其實是lambda表達式的一個簡化寫法,所引用的方法其實是lambda表達式的方法體實作,文法也很簡單,左邊是容器(可以是類名,執行個體名),中間是"::",右邊是相應的方法名。如下所示:

?

1

ObjectReference::methodName

一般方法的引用格式是

  1. 如果是靜态方法,則是ClassName::methodName。如 Object ::equals
  2. 如果是執行個體方法,則是Instance::methodName。如Object obj=new Object();obj::equals;
  3. 構造函數.則是ClassName::new

再來看一個完整的例子,友善了解

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

import

java.awt.FlowLayout;

import

java.awt.event.ActionEvent;

import

javax.swing.JButton;

import

javax.swing.JFrame;

public

class

TestMethodReference {

public

static

void

main(String[] args) {

JFrame frame = 

new

JFrame();

frame.setLayout(

new

FlowLayout());

frame.setVisible(

true

);

JButton button1 = 

new

JButton(

"點我!"

);

JButton button2 = 

new

JButton(

"也點我!"

);

frame.getContentPane().add(button1);

frame.getContentPane().add(button2);

//這裡addActionListener方法的參數是ActionListener,是一個函數式接口

//使用lambda表達式方式

button1.addActionListener(e -> { System.out.println(

"這裡是Lambda實作方式"

); });

//使用方法引用方式

button2.addActionListener(TestMethodReference::doSomething);

}

public

static

void

doSomething(ActionEvent e) {

System.out.println(

"這裡是方法引用實作方式"

);

}

}

可以看出,doSomething方法就是lambda表達式的實作,這樣的好處就是,如果你覺得lambda的方法體會很長,影響代碼可讀性,方法引用就是個解決辦法

總結

以上就是lambda表達式文法的全部内容了,相信大家對lambda表達式都 有一定的了解了,但隻是代碼簡潔了這個好處的話,并不能打動很多觀衆,java 8也不會這麼令人期待,其實java 8引入lambda迫切需求是因為lambda 表達式能簡化集合上資料的多線程或者多核的處理,提供更快的集合處理速度 ,這個後續會講到,關于JEP126的這一特性,将分3部分,之是以分開,是因為這一特性可寫的東西太多了,這部分讓讀者熟悉lambda表達式以及方法 引用的文法和概念,第二部分則是虛拟擴充方法(default method)的内容,最後一部分則是大資料集合的處理,解開lambda表達式的最強作用的神秘面紗。敬請期待。。。。

  • 什麼是預設方法,為什麼要有預設方法
  • 簡單的例子
  • java 8抽象類與接口對比
  • 多重繼承的沖突說明
  • 總結

上篇講了 lambda表達式的文法,但隻是 JEP126 特性的一部分,另一部分就是預設方法(也稱為虛拟擴充方法或防護方法)

什麼是預設方法,為什麼要有預設方法

簡單說,就是接口可以有實作方法,而且不需要實作類去實作其方法。隻需在方法名前面加個default關鍵字即可。

為什麼要有這個特性?首先,之前的接口是個雙刃劍,好處是面向抽象而不是面向具體程式設計,缺陷是,當需要修改接口時候,需要修改全部實作該接口的類,目前的 java 8之前的集合架構沒有foreach方法,通常能想到的解決辦法是在JDK裡給相關的接口添加新的方法及實作。然而,對于已經釋出的版本,是沒法在給接口 添加新方法的同時不影響已有的實作。是以引進的預設方法。他們的目的是為了解決接口的修改與現有的實作不相容的問題。

簡單的例子

一個接口A,Clazz類實作了接口A。

?

1 2 3 4 5 6 7 8 9 10 11 12

public

interface

A {

default

void

foo(){

System.out.println(

"Calling A.foo()"

);

}

}

public

class

Clazz 

implements

A {

public

static

void

main(String[] args){

Clazz clazz = 

new

Clazz();

clazz.foo();

//調用A.foo()

}

}

代碼是可以編譯的,即使Clazz類并沒有實作foo()方法。在接口A中提供了foo()方法的預設實作。

java 8抽象類與接口對比

這一個功能特性出來後,很多同學都反應了,java 8的接口都有實作方法了,跟抽象類還有什麼差別?其實還是有的,請看下表對比。。

相同點 不同點

1.都是抽象類型;

2.都可以有實作方法(以前接口不行);

3.都可以不需要實作類或者繼承者去實作所有方法,(以前不行,現在接口中預設方法不需要實作者實作)

1.抽象類不可以多重繼承,接口可以(無論是多重類型繼承還是多重行為繼承);

2.抽象類和接口所反映出的設計理念不同。其實抽象類表示的是"is-a"關系,接口表示的是"like-a"關系;

3.接口中定義的變量預設是public static final 型,且必須給其初值,是以實作類中不能改變其值;抽象類中的變量預設是 friendly 型,其值可以在子類中重新定義,也可以重新指派。 

多重繼承的沖突說明

由于同一個方法可以從不同接口引入,自然而然的會有沖突的現象,預設方法判斷沖突的規則如下:

1.一個聲明在類裡面的方法優先于任何預設方法(classes always win)

2.否則,則會優先選取最具體的實作,比如下面的例子 B重寫了A的hello方法。

[轉]Java 8新特性探究(lambda)函數式接口 Lambda文法方法引用總結什麼是預設方法,為什麼要有預設方法簡單的例子 java 8抽象類與接口對比 多重繼承的沖突說明總結 1.關于JSR335 2.外部VS内部疊代 3.Stream API 4.總結

輸出結果是:Hello World from B

如果想調用A的預設函數,則用到新文法X.super.m(...),下面修改C類,實作A接口,重寫一個hello方法,如下所示:

?

1 2 3 4 5 6 7 8 9 10 11

public

class

implements

A{

@Override

public

void

hello(){

A.

super

.hello();

}

public

static

void

main(String[] args){

new

C().hello();

}

}

輸出結果是:Hello World from A

總結

預設方法給予我們修改接口而不破壞原來的實作類的結構提供了便利,目前java 8的集合架構已經大量使用了預設方法來改進了,當我們最終開始使用Java 8的lambdas表達式時,提供給我們一個平滑的過渡體驗。也許将來我們會在API設計中看到更多的預設方法的應用。

  • 1.關于JSR335
  • 2.外部VS内部疊代
  • 3.Stream API
  • 3.1中間與終點方法
  • 3.1.1Filter
  • 3.1.2Map
  • 3.1.3Count
  • 3.1.4Collect
  • 3.2順序流與并行流
  • 3.2.1并行流原理:
  • 3.2.2順序與并行性能測試對比
  • 3.3關于Folk/Join架構
  • 4.總結

我們期待了很久lambda為java帶來閉包的概 念,但是如果我們不在集合中使用它的話,就損失了很大價值。現有接口遷移成為lambda風格的問題已經通過default methods解決了,在這篇文章将深入解析Java集合裡面的批量資料操作(bulk operation),解開lambda最強作用的神秘面紗。

1.關于JSR335

JSR是Java Specification Requests的縮寫,意思是Java 規範請求,Java 8 版本的主要改進是 Lambda 項目(JSR 335),其目的是使 Java 更易于為多核處理器編寫代碼。JSR 335=lambda表達式+接口改進(預設方法)+批量資料操作。加上前面兩篇,我們已是完整的學習了JSR335的相關内容了。

2.外部VS内部疊代

以前Java集合是不能夠表達内部疊代的,而隻提供了一種外部疊代的方式,也就是for或者while循環。

?

1 2 3 4

List persons = asList(

new

Person(

"Joe"

),

new

Person(

"Jim"

),

new

Person(

"John"

));

for

(Person p :  persons) {

p.setLastName(

"Doe"

);

}

上面的例子是我們以前的做法,也就是所謂的外部疊代,循環是固定的順序循環。在現在多核的時代,如果我們想并行循環,不得不修改以上代碼。效率能有多大提升還說定,且會帶來一定的風險(線程安全問題等等)。

要描述内部疊代,我們需要用到Lambda這樣的類庫,下面利用lambda和Collection.forEach重寫上面的循環

?

1

persons.forEach(p->p.setLastName(

"Doe"

));

現在是由jdk 庫來控制循環了,我們不需要關心last name是怎麼被設定到每一個person對象裡面去的,庫可以根據運作環境來決定怎麼做,并行,亂序或者懶加載方式。這就是内部疊代,用戶端将行為p.setLastName當做資料傳入api裡面。

内部疊代其實和集合的批量操作并沒有密切的聯系,借助它我們感受到文法表達上的變化。真正有意思的和批量操作相關的是新的流(stream)API。新的java.util.stream包已經添加進JDK 8了。

3.Stream API

流(Stream)僅僅代表着資料流,并沒有資料結構,是以他周遊完一次之後便再也無法周遊(這點在程式設計時候需要注意,不像Collection,周遊多少次裡面都還有資料),它的來源可以是Collection、array、io等等。

3.1中間與終點方法

流作用是提供了一種操作大資料接口,讓資料操作更容易和更快。它具有過濾、映射以及減少周遊數等方法,這些方法分兩種:中間方法和終端方法,“流”抽象天 生就該是持續的,中間方法永遠傳回的是Stream,是以如果我們要擷取最終結果的話,必須使用終點操作才能收集流産生的最終結果。區分這兩個方法是看他 的傳回值,如果是Stream則是中間方法,否則是終點方法。具體請參照Stream的api。

簡單介紹下幾個中間方法(filter、map)以及終點方法(collect、sum)

3.1.1Filter

在資料流中實作過濾功能是首先我們可以想到的最自然的操作了。Stream接口暴露了一個filter方法,它可以接受表示操作的Predicate實作來使用定義了過濾條件的lambda表達式。

?

1 2

List persons = …

Stream personsOver18 = persons.stream().filter(p -> p.getAge() >

18

);

//過濾18歲以上的人

3.1.2Map

假使我們現在過濾了一些資料,比如轉換對象的時候。Map操作允許我們執行一個Function的實作(Function<T,R>的泛型T,R分别表示執行輸入和執行結果),它接受入參并傳回。首先,讓我們來看看怎樣以匿名内部類的方式來描述它:

?

1 2 3 4 5 6 7 8 9

Stream adult= persons

.stream()

.filter(p -> p.getAge() >

18

)

.map(

new

Function() {

@Override

public

Adult apply(Person person) {

return

new

Adult(person);

//将大于18歲的人轉為成年人

}

});

現在,把上述例子轉換成使用lambda表達式的寫法:

?

1 2 3

Stream map = persons.stream()

.filter(p -> p.getAge() >

18

)

.map(person ->

new

Adult(person));

3.1.3Count

count方法是一個流的終點方法,可使流的結果最終統計,傳回int,比如我們計算一下滿足18歲的總人數

?

1 2 3 4

int

countOfAdult=persons.stream()

.filter(p -> p.getAge() >

18

)

.map(person ->

new

Adult(person))

.count();

3.1.4Collect

collect方法也是一個流的終點方法,可收集最終的結果

?

1 2 3 4

List adultList= persons.stream()

.filter(p -> p.getAge() >

18

)

.map(person ->

new

Adult(person))

.collect(Collectors.toList());

或者,如果我們想使用特定的實作類來收集結果:

?

1 2 3 4 5

List adultList = persons

.stream()

.filter(p -> p.getAge() >

18

)

.map(person ->

new

Adult(person))

.collect(Collectors.toCollection(ArrayList::

new

));

篇幅有限,其他的中間方法和終點方法就不一一介紹了,看了上面幾個例子,大家明白這兩種方法的差別即可,後面可根據需求來決定使用。

3.2順序流與并行流

每個Stream都有兩種模式:順序執行和并行執行。

順序流:

?

1

List <Person> people = list.getStream.collect(Collectors.toList());

并行流:

?

1

List <Person> people = list.getStream.parallel().collect(Collectors.toList());

顧名思義,當使用順序方式去周遊時,每個item讀完後再讀下一個item。而使用并行去周遊時,數組會被分成多個段,其中每一個都在不同的線程中處理,然後将結果一起輸出。

3.2.1并行流原理:

?

1 2 3 4 5 6

List originalList = someData;

split1 = originalList(

, mid);

//将資料分小部分

split2 = originalList(mid,end);

new

Runnable(split1.process());

//小部分執行操作

new

Runnable(split2.process());

List revisedList = split1 + split2;

//将結果合并

大家對hadoop有稍微了解就知道,裡面的 MapReduce  本身就是用于并行處理大資料集的軟體架構,其 處理大資料的核心思想就是大而化小,配置設定到不同機器去運作map,最終通過reduce将所有機器的結果結合起來得到一個最終結果,與MapReduce 不同,Stream則是利用多核技術可将大資料通過多核并行處理,而MapReduce則可以分布式的。

3.2.2順序與并行性能測試對比

如果是多核機器,理論上并行流則會比順序流快上一倍,下面是測試代碼

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

long

t0 = System.nanoTime();

//初始化一個範圍100萬整數流,求能被2整除的數字,toArray()是終點方法

int

a[]=IntStream.range(

, 1_000_000).filter(p -> p %

2

==

).toArray();

long

t1 = System.nanoTime();

//和上面功能一樣,這裡是用并行流來計算

int

b[]=IntStream.range(

, 1_000_000).parallel().filter(p -> p %

2

==

).toArray();

long

t2 = System.nanoTime();

//我本機的結果是serial: 0.06s, parallel 0.02s,證明并行流确實比順序流快

System.out.printf(

"serial: %.2fs, parallel %.2fs%n"

, (t1 - t0) * 1e-

9

, (t2 - t1) * 1e-

9

);

3.3關于Folk/Join架構

應用硬體的并行性在java 7就有了,那就是 java.util.concurrent 包的新增功能之一是一個 fork-join 風格的并行分解架構,同樣也很強大高效,有興趣的同學去研究,這裡不詳談了,相比Stream.parallel()這種方式,我更傾向于後者。

4.總結

如果沒有lambda,Stream用起來相當别扭,他會産生大量的匿名内部類,比如上面的3.1.2map例子,如果沒有default method,集合架構更改勢必會引起大量的改動,是以lambda+default method使得jdk庫更加強大,以及靈活,Stream以及集合架構的改進便是最好的證明。

轉載于:https://www.cnblogs.com/tianlanliao/p/4294858.html