1. 程式人生 > >JDK1.8 Lambda表示式與Stream

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提供的一些方法,簡化了我們對集合的操作,讓我們只關注程式碼邏輯,不用再去關注一些程式碼模板。能夠讓我們程式碼邏輯清晰。在程式碼執行效率其實是一樣的。但是卻可以提升我們的開發效率。

   如果想下載上面例子的原始碼請到點選下面連結

   https://github.com/sunwnehongl/LambadAndStream