JDK1.8 Lambda表示式與Stream
一、概述
jdk1.8對Lambda 表示式的支援,了Stream以實現對集合更方便地進行函數語言程式設計。本文主要介紹jLambda表示式和Stream的一些常用使用方式,並通過一些程式碼小例子向大家展示怎麼使用。
二、函式式介面
什麼時函式式介面?
函式式介面(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的介面。
怎麼寫函式介面?
Java 1.8中專門為函式式介面引入了一個新的註解: @FunctionalInterface 。該註解可用於一個介面的定義上,一旦使用該註解來定義介面,編譯器將會強制檢查該介面是否確實有且僅有一個抽象方法,否則將會報錯。
JDK 1.8之前已有的函式式介面:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK1.8後新加的函式式介面
介面 |
說明 |
BiConsumer<T,U> | 代表了一個接受兩個輸入引數的操作,並且不返回任何結果 |
BiFunction<T,U,R> | 代表了一個接受兩個輸入引數的方法,並且返回一個結果 |
BinaryOperator<T> | 代表了一個作用於於兩個同類型操作符的操作,並且返回了操作符同類型的結果 |
BiPredicate<T,U> | 代表了一個兩個引數的boolean值方法 |
BooleanSupplier | 代表了boolean值結果的提供方 |
Consumer<T> | 代表了接受一個輸入引數並且無返回的操作 |
Function<T,R> | 接受一個輸入引數,返回一個結果 |
Predicate<T> | 接受一個輸入引數,返回一個布林值結果。 |
Supplier<T> | 無引數,返回一個結果。 |
這些介面有很多,這裡就不再一 一的說明,這些介面主要分為四大類supplier生產資料函式式介面、Consumer消費資料函式式介面、Predicate判斷函式式介面、Function型別轉換函式式介面。
三、Lambda表示式
其實Lambda表示式的本質只是一個"語法糖",由編譯器推斷並幫你轉換包裝為常規的程式碼,因此你可以使用更少的程式碼來實現同樣的功能。
Lambda 表示式語法
Lambda表示式又三部分組成,(引數列表-》->(表示式或者程式碼塊)
如下例子:
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
Lambda表示式等價一個匿名內部類,只不過這裡只能是一個實現了函式式介面的一個匿名內部類
public class RunnableTest {
@Test
public void RunnableLambdaTest(){
// 使用匿名內部類形式執行
Runnable r1 = new Runnable() {
public void run() {
System.out.println("this is r1");
}
};
// 執行內部方法, 並且沒有傳遞任何引數
Runnable r2 = () -> System.out.println("this is r2");
// 執行
r1.run();
r2.run();
}
}
三、Stream
什麼是流?
Stream 不是集合元素,它不是資料結構並不儲存資料,它是有關演算法和計算的,它更像一個高階版本的 Iterator。原始版本的 Iterator,使用者只能顯式地一個一個遍歷元素並對其執行某些操作;高階版本的 Stream,使用者只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字串”、“獲取每個字串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的資料轉換。
Stream 就如同一個迭代器(Iterator),單向,不可往復,資料只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。
而和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、序列化操作。顧名思義,當使用序列方式去遍歷時,每個 item 讀完後再讀下一個 item。而使用並行去遍歷時,資料會被分成多個段,其中每一個都在不同的執行緒中處理,然後將結果一起輸出。
接下來,當把一個數據結構包裝成 Stream 後,就要開始對裡面的元素進行各類操作了。常見的操作可以歸類如下。
中間操作:
- map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
終端操作:
- forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
下面我用一個個例子來對這些操作進行介紹、在介紹這些操作前我們先宣告一個簡單的User類,所有的Stream操作都是基於這個User集合進行介紹。
package com.sun.liems.model;
/***
* 人員類主要用於lambda表示式和Stream練習使用
* @author swh
*
*/
public class User {
// 賬號ID
private String userId;
// 使用者姓名
private String userName;
// 性別
private String sex;
// 年齡
private int age;
/***
* 構造器
* @param userId 使用者id
* @param userName 姓名
* @param sex 性別
* @param age 年齡
*/
public User(String userId,String userName,String sex,int age) {
this.userId = userId;
this.userName =userName;
this.sex = sex;
this.age = age;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "賬號:"+this.userId+" 姓名:"+this.userName+" 性別"+this.sex+" 年齡"+this.age;
}
}
1、map
可以將一種型別的值轉換成另外一種型別,map 操作就可以使用該函式,將一個流中的值轉換成一個新的流。其實就是就是實現一個Function介面。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
把一個人員轉換成一個字串
/***
* 把一個人員轉換成一個字串
* @return
*/
public static List<String> map(){
return userList.stream()
.map(s->{
return s.toString();
})
.collect(Collectors.toList());
}
2、flatMap
flatmap 和map功能差不多,區別在於map的輸出對應一個元素,必然是一個元素(null也是要返回),flatmap是0或者多個元素(為null的時候其實就是0個元素)。 flatmap的意義在於,一般的java方法都是返回一個結果,但是對於結果數量不確定的時候,用map這種java方法的方式,是不太靈活的,所以引入了flatmap。 flatMap可以把多個流程返回為一個流。
使用場景:如果map返回的一個數組或者是集合,可以用flatmap這些返回轉換為同一個流。進行扁平化處理。
public static void flatMap(){
String[] words = new String[]{"Hello","World"};
List<String> a = Arrays.stream(words)
.map(word -> word.split(""))
.flatMap(s->Arrays.stream(s))
.collect(Collectors.toList());
a.forEach(System.out::print);
}
3、filter
filter操作用於資料過濾。
去除人員陣列中的所有女性
public static List<User> filter(){
return userList.stream()
.filter(s->s.getSex().equals("男"))
.collect(Collectors.toList());
}
4、distinct
distinct操作用於資料去重。distinct使用hashCode和equals方法來獲取不同的元素。因此,我們的類必須實現hashCode和equals方法。
public static List<User> distinct(){
return userList.stream()
.distinct()
.collect(Collectors.toList());
}
@Override
public boolean equals(Object o) {
User u = (User) o;
return this.getUserId().equals(u.getUserId());
}
@Override
public int hashCode() {
return this.getUserId().hashCode();
}
5、sorted
sorted操作用於以自然序排序
把人員按年齡排序
public static List<User> sorted(){
return userList.stream()
.sorted(Comparator.comparing(s->s.getAge()))
.collect(Collectors.toList());
}
6、peek
peek接收一個沒有返回值的λ表示式,可以做一些輸出,外部處理等。map接收一個有返回值的λ表示式,之後Stream的泛型型別將轉換為map引數λ表示式返回的型別。和forEach是有所區別的。peek是一箇中間操作而forEach中間操作
public static List<User> peek(){
return userList.stream()
.filter(s->s.getSex().equals("男"))
.peek(s->System.out.println(s))
.collect(Collectors.toList());
}
7、limit
limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素;
/***
* 取前4個使用者
* @return
*/
public static List<User> limit(){
return userList.stream()
.limit(4)
.collect(Collectors.toList());
}
8、skip
skip: 返回一個丟棄原Stream的前N個元素後剩下元素組成的新Stream,如果原Stream中包含的元素個數小於N,那麼返回空Stream;
/***
* 取出了前4個後面的使用者
* @return
*/
public static List<User> skip(){
return userList.stream()
.skip(4)
.collect(Collectors.toList());
}
9、forEach
作用是對容器中的每個元素執行action
指定的動作,也就是對元素進行遍歷。
把所有人員打印出來
10、reduce
reduce 操作可以實現從一組值中生成一個值。在上述例子中用到的 count、min 和 max 方法,因為常用而被納入標準庫中。事實上,這些方法都是 reduce 操作。
/***
* 求所有人員年齡的合
*/
public static void reduce() {
int ageSum = userList.stream()
.map(u->u.getAge())
.reduce(0,(sum,u)-> sum+u);
System.out.println(ageSum);
}
11、max及min
求最大值和最小值,此時max及min接受的是Comparator<? super T> comparator
/***
* 求年齡最大的人員
*/
public static void max() {
User user = userList.stream()
.max(Comparator.comparing(u->u.getAge()))
.get();
System.out.println(user);
}
12、collect
collect也就是收集器,是Stream一種通用的、從流生成複雜值的結構。只要將它傳給collect方法,也就是所謂的轉換方法,其就會生成想要的資料結構。這裡不得不提下,Collectors這個工具庫,在該庫中封裝了相應的轉換方法。當然,Collectors工具庫僅僅封裝了常用的一些情景,如果有特殊需求,那就要自定義了。
方法 | 用途 |
toList | 把結果轉換為一個List |
toSet | 把結果轉換為一個Set |
groupingBy | 分類用,例如按某個屬性分類轉換為一個map |
toMap |
/**
* 把人員按性別分組
*
*/
public static void collect() {
Map<Object, List<User>> map = userList.stream()
.collect(Collectors.groupingBy(u->u.getSex()));
System.out.println(map.size());
}
/***
* 得到人員賬號和年齡的對應關係
*/
public static void toMap() {
Map<String, String> map = userList.stream()
.collect(Collectors.toMap(u->u.getUserId(), u->u.getUserName()));
map.entrySet()
.forEach(e->{
System.out.println(e.getKey()+":"+e.getValue());
});
}
使用stream的toMap()函式時,當key重複,系統會報錯相同的key不能形成一個map,那麼需要解決這個問題,一:相同key的情況下,丟棄重複的只保留一個。
13、匹配方法
anyMatch:匹配上任何一個則返回 Boolean
allMatch:匹配所有的元素則返回 Boolean
noneMatch跟allMatch相反,判斷條件裡的元素,所有的都不是,返回true
Optional<T>是一個可以包含或不可以包含非空值的容器物件,
/***
* 匹配
*/
public static void match() {
// 是否包含張三
boolean flagA = userList.stream()
.anyMatch(u->u.getUserName().equals("張三"));
System.out.println( flagA);
// 是否所以人都是18歲
boolean flagB = userList.stream()
.allMatch(u->u.getAge()==18);
System.out.println( flagB);
// 是否所有人都是成年人
boolean flagC = userList.stream()
.noneMatch(u->u.getAge()<18);
System.out.println(flagC);
}
四、總結
steam提供的一些方法,簡化了我們對集合的操作,讓我們只關注程式碼邏輯,不用再去關注一些程式碼模板。能夠讓我們程式碼邏輯清晰。在程式碼執行效率其實是一樣的。但是卻可以提升我們的開發效率。
如果想下載上面例子的原始碼請到點選下面連結