我的博客:zhenganwen.topjava
函數式編程給個人直觀感覺:數據庫
假設你如今是一個農場主,你採摘了一筐蘋果以下:編程
List<Apple> apples = Arrays.asList(
new Apple("red", 100),
new Apple("red", 300),
new Apple("red", 500),
new Apple("green", 200),
new Apple("green", 400),
new Apple("green", 600),
new Apple("yellow", 300),
new Apple("yellow", 400),
new Apple("yellow", 500)
);
複製代碼
Apple
數組
@Data
@AllArgsConstructor
public class Apple {
private String color;
private int weight;
}
複製代碼
如今須要你編寫一個方法,挑選出籮筐中顏色爲綠色的蘋果,因而你垂手可得地寫了以下代碼:緩存
@Test
public void pickGreenApples() {
List<Apple> list = new ArrayList<>();
for (Apple apple : apples) {
if (Objects.equals(apple.getColor(), "green")) {
list.add(apple);
}
}
System.out.println(list);
}
[Apple(color=green, weight=200), Apple(color=green, weight=400), Apple(color=green, weight=600)]
複製代碼
若是須要你挑選出紅色的呢?你發現能夠將按照顏色挑選蘋果抽取出來以供複用:bash
public void pickByColor(List<Apple> apples, String color) {
for (Apple apple : apples) {
if (Objects.equals(apple.getColor(), color)) {
System.out.println(apple);
}
}
}
@Test
public void testPickByColor() {
pickByColor(apples, "red");
}
Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
複製代碼
好了,如今我須要你挑選出重量在400g以上,顏色不是綠色的蘋果呢?你會發現,根據不一樣的顏色和重量挑選標準可以組合成若干的挑選策略,那是否是針對每一個策略咱們都須要編寫一個方法呢?這固然不可能,咱們也沒法事先預知顧客須要什麼樣的蘋果。併發
如今咱們來理一下業務需求,其實也就是咱們給顧客一個蘋果,由他來判斷是否符合他的食用標準,這能夠採用策略模式來實現:app
將判斷某個蘋果是否符合標準這一行爲抽象爲一個接口:框架
public interface AppleJudgementStrategy {
/** * 你給我一個apple,我判斷他是否符合挑選出來的標準 * @param apple the apple in container * @return true if apple need be picked */
boolean judge(Apple apple);
}
複製代碼
挑選蘋果的方法根據策略挑選蘋果(面向接口編程,靈活性更大)dom
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
for (Apple apple : apples) {
if (strategy.judge(apple)) {
// apple 符合既定的挑選策略
System.out.println(apple);
}
}
}
複製代碼
業務方法根據實際的業務需求建立具體的挑選策略傳給挑選方法
@Test
public void testPickByStrategy() {
// 挑選400g以上且顏色不是綠色的
pickByStrategy(apples, new AppleJudgementStrategy() {
@Override
public boolean judge(Apple apple) {
return apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green");
}
});
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼
那麼以上的代碼重構是無需基於Java 8的,但其中有一個弊端:策略模式的實現要麼須要建立一個新類,要麼使用匿名類的方式。在此過程當中,new/implements AppleJudgementStrategy
和public boolean judge
的大量重複是很冗餘的,Java 8引入的Lambda
表達式就很好的改善了這一點,如上述代碼可簡化以下:
@Test
public void testPickByStrategyWithLambda() {
pickByStrategy(apples, apple -> apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green"));
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼
本節經過挑選蘋果的例子初步體驗了一下Lambda
表達式的魅力,但其的做用不只於此,接下來讓咱們打開函數式編程的大門。
正如上一節說表現的,面對同一業務(挑選蘋果)的需求的頻繁變動,業務方法僅能接受基本類型、引用類型(對象類型)的參數已經不能知足咱們的需求了,咱們指望可以經過參數接受某一特定的行爲(如判斷某個蘋果是否應該被挑選出來)以使方法可以「以不變應萬變」。
上例中,雖然咱們能經過策略模式達到此目的,但若是究其本質,策略接口其實就是對一個函數的封裝,而咱們業務方法參數接收該對象也僅僅是爲了調用該對象實現的接口方法。不管咱們是在調用業務方法以前建立一個該接口的實現類而後在調用業務方法傳參時new
該實現類,仍是在調用業務方法傳參時直接new
一個匿名類,這些建立類的操做都只是爲了遵照「在Java中,類是第一公民的事實」(即先有類,後有方法,方法必須封裝在類中)。所以,建立類和聲明方法的過程顯得有些多餘,其實業務方法只是想要一個具體的策略而已,如return apple.getWeight() >= 400
(挑選出重量大於400g的蘋果)。
所以你會看到Java 8引入Lambda
表達式以後,業務調用方能夠變動以下:
// 業務方法
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
for (Apple apple : apples) {
if (strategy.judge(apple)) {
// apple 符合既定的挑選策略
System.out.println(apple);
}
}
}
// 調用業務
@Test
public void testPickByStrategy() {
// 挑選400g以上的
pickByStrategy( apples, (apple) -> { return apple.getWeight() >= 400 } );
}
複製代碼
咱們使用一個Lambda
表達式(apple) -> { return apple.getWeight() >= 400 }
代替了AppleJudgementStrategy
實例的建立
Lambda
表達式能夠理解爲函數表達式,在上例中指的就是(apple) -> { return apple.getWeight() >= 400 }
,表示對接口AppleJudgementStrategy
函數boolean judge(Apple apple);
的一個實現。其中(apple)
表示函數接受一個Apple
類型的對象;{ return apple.getWeight() >= 400 }
則是函數體,表示傳入Apple
對象的重量大於400時返回true
。
Lambda
表達式和接口是密切相關的,並且能使用Lambda
表達式代替其實例的接口必須是隻聲明瞭一個抽象方法的接口
default
或static
方法@FunctionalInterface
以表示該接口是函數式接口,其實例能夠經過Lambda
表達式建立,而且該註解能夠約束該接口知足上述三點規則注意:Java 8 對接口進行了從新定義,爲了得到更好的兼容性,接口中的方法能夠有方法體,此時該方法需被標記爲
default
,即該方法有一個默認的實現,子類能夠選擇性的重寫。不像抽象方法,必須被具體子類重寫。此外接口中還能夠定義帶有方法體的靜態方法,能夠經過
接口名.方法名
的形式訪問,這一點與類靜態方法無異。上述兩點打破了Java 8 以前對接口的定義:接口中的方法必須都是抽象方法(
public abstract
)。
也就是說在AppleJudgementStrategy
添加若干default
或static
方法,都是不影響使用Lambda
表達式來代替其實例的(在IDE中,@FunctionalInterface
註解不會報紅表示這是一個合法的函數式接口):
@FunctionalInterface
public interface AppleJudgementStrategy {
/** * 你給我一個apple,我判斷他是否符合挑選出來的標準 * @param apple the apple in container * @return true if apple need be picked */
boolean judge(Apple apple);
default void fun1() {
System.out.println("這是帶有默認實現的方法");
}
static void fun2() {
System.out.println("這是定義在接口中的靜態方法");
}
}
複製代碼
有了函數式接口以後咱們就能夠在須要該接口實例的地方使用lambda
表達式了。
下面咱們經過實戰來鞏固lambda
表達式的運用。
編寫函數式接口:
@FunctionalInterface
public interface AccumulatorFunction {
/** * 該函數聚合兩個整數經過運算生成一個結果 * @param a 整數a * @param b 整數b * @return 運算結果 */
int accumulate(int a, int b);
}
複製代碼
編寫業務方法,參數接受函數式接口實例,讓該參數具有行爲能力
/** * 經過既定的計算規則對輸入值a和b得出輸出結果 * @param a * @param b * @param accumulatorFunction * @return */
public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
return accumulatorFunction.accumulate(a,b);
}
複製代碼
編寫業務調用方,經過lambda
表達式闡述行爲
@Test
public void testAccumulatorFunction() {
int res1 = compute(1, 2, (a, b) -> { //闡述的行爲是求和
return a + b;
});
System.out.println("1加2的結果是:" + res1);
int res2 = compute(1, 2, (a, b) -> { //闡述的行爲是求乘積
return a * b;
});
System.out.println("1乘2的結果是:" + res2);
}
1加2的結果是:3
1乘2的結果是:2
複製代碼
經過上一節咱們知道lambda
表達式是和函數式接口密切相關的,在須要傳入函數式接口實例時咱們能夠編寫lambda
表達式,編寫時咱們要特別關注該接口抽象方法定義的參數列表以及返回值。
假設函數式接口聲明以下(其中R,T1,T2,T3
爲基本類型或引用類型):
@FunctionalInterface
public interface MyFunction{
R fun(T1 t1, T2 t2, T3 t3);
}
複製代碼
那麼你的lambda
表達式就應該編寫以下:
(t1, t2, t3) -> { // 參數名自定義,爲a,b,c也能夠,可是要知道在方法體中訪問時a的類型是T1,b的類型是T2
// do you service with t1,t2,t3
return instance_of_R;
}
複製代碼
總結以下,嚴格意義上的lambda
表達式需包含:
return
語句做爲結尾可是爲了書寫簡潔,上述幾點有時不是必須的。
t1 -> {
// do your service with t1
return instance_of_R;
}
複製代碼
其餘狀況下(包括無參時)都必須有小括號
當函數體只有一條語句時,如return t1+t2+t3
(假設t1,t2,t3,R
都是int
型),那麼方法體能夠省略大括號和return
:
(t1,t2,t3) -> t1+t2+t3
複製代碼
只要函數體包含了語句(必須以分號結尾),那麼函數體就須要加上大括號
在Java 8 中,爲咱們新增了一個java.util.function
包,其中定義的就所有都是函數式接口,其中最爲主要的接口以下:
Consumer
,接受一個參數,沒有返回值。表明了消費型函數,函數調用消費你傳入的參數,但不給你返回任何信息Supplier
,不接收參數,返回一個對象。表明了生產型函數,經過函數調用可以獲取特定的對象Predicate
,接收一個對象,返回一個布爾值。表明了斷言型函數,對接收的對象斷言是否符合某種標註。(咱們上面定義的AppleJudgementFunction
就屬於這種函數。Function
,接收一個參數,返回函數處理結果。表明了輸入-輸出型函數。該包下的其餘全部接口都是基於以上接口在參數接受個數(如BiConsumer
消費兩個參數)、和參數接收類型(如IntConsumer
僅用於消費int
型參數)上作了一個具體化。
對每一個蘋果進行「消費」操做:
public void consumerApples(List<Apple> apples, Consumer<Apple> consumer) {
for (Apple apple : apples) {
consumer.accept(apple);
}
}
複製代碼
如「消費」操做就是將傳入的蘋果打印一下:
@Test
public void testConsumer() {
consumerApples(apples, apple -> System.out.println(apple));
}
Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
Apple(color=green, weight=200)
Apple(color=green, weight=400)
Apple(color=green, weight=600)
Apple(color=yellow, weight=300)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼
若是你使用的IDE是IDEA,那麼它會提示你System.out.println(apple)
能夠Lambda can be replaced with method inference
(即該lambda
表達式可使用方法推導),因而咱們使用快捷鍵alt + Enter
尋求代碼優化提示,表明被替換成了apple -> System.out::println
。
這裏引伸出了lambda
的一個新用法:方法推導。當咱們在編寫lambda
時,若是方法體只是一個表達式,而且該表達式調用的方法行爲與此處對應的函數接口的行爲一致時,可使用方法推導(類名::方法名
或對象名::方法名
)。
由於這裏咱們須要一個Consumer
,而println
的定義與Consumer.accept
的函數行爲是一致的(接受一個對象,無返回值):
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
複製代碼
所以IDEA提示咱們此處可使用方法推導apple -> System.out::println
代替方法調用apple -> System.out.println(apple)
。如此,前者看起來更具可讀性:使用println
行爲消費這個apple
。
Supplier
像是一個工廠方法,你能夠經過它來獲取對象實例。
如,經過Supplier
你給我一個蘋果,我來打印它的信息
public void printApple(Supplier<Apple> appleSupplier) {
Apple apple = appleSupplier.get();
System.out.println(apple);
}
@Test
public void testSupplier() {
printApple(() -> {
return new Apple("red", 666);
});
}
Apple(color=red, weight=666)
複製代碼
若是Apple
提供無參構造方法,那麼這裏可使用構造函數的方法推導(無參構造函數不接收參數,但返回一個對象,和Supplier.get
的函數類型一致):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
private String color;
private int weight;
}
@Test
public void testSupplier() {
// printApple(() -> {
// return new Apple("red", 666);
// });
printApple(Apple::new);
}
Apple(color=null, weight=0)
複製代碼
Predicate
則是用來斷言對象,即按照某種策略斷定入參對象是否符合標準的。
爲此咱們能夠將先前寫的挑選蘋果的業務方法中的AppleJudgementStrategy
換成Predicate
,做用是同樣的,之後用到策略模式的地方直接使用Predicate<T>
就好了
// public void pickByStrategy(List<Apple> apples, AppleJudgementStrategy strategy) {
// for (Apple apple : apples) {
// if (strategy.judge(apple)) {
// // apple 符合既定的挑選策略
// System.out.println(apple);
// }
// }
// }
public void pickByStrategy(List<Apple> apples, Predicate<Apple> applePredicate) {
for (Apple apple : apples) {
if (applePredicate.test(apple)) {
// apple 符合既定的挑選策略
System.out.println(apple);
}
}
}
@Test
public void testPickByStrategyWithLambda() {
pickByStrategy(apples, apple ->
apple.getWeight() >= 400 && !Objects.equals(apple.getColor(),"green"));
}
Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼
Function
就不用說了,表明了最普通的函數描述:有輸入,有輸出。
咱們此前編寫的AccumulatorFunction
就屬於一個BiFunction
,讓咱們來使用BiFucntion
對其進行改造:
// public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
// return accumulatorFunction.accumulate(a,b);
// }
public int compute(int a, int b, BiFunction<Integer,Integer,Integer> biFunction) {
return biFunction.apply(a,b);
}
@Test
public void testAccumulatorFunction() {
int res1 = compute(1, 2, (a, b) -> {
return a + b;
});
System.out.println("1加2的結果是:" + res1);
int res2 = compute(1, 2, (a, b) -> {
return a * b;
});
System.out.println("1乘2的結果是:" + res2);
}
1加2的結果是:3
1乘2的結果是:2
複製代碼
如今有一個菜品類定義以下:
public class Dish {
public enum Type {MEAT, FISH, OTHER}
private final String name; //菜品名稱
private final boolean vegetarian; //是不是素食
private final int calories; //提供的卡路里
private final Type type; //菜品類型
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name;
}
}
複製代碼
給你一份包含若干菜品的菜單:
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH));
複製代碼
如今要你以卡路里升序的方法打印卡路里在400如下的菜品名稱清單,在Java 8 以前,你可能須要這樣作:
@Test
public void beforeJava8() {
// 1. filter which calories is lower than 400 and collect them
List<Dish> filterMenu = new ArrayList<>();
for (Dish dish : menu) {
if (dish.getCalories() < 400) {
filterMenu.add(dish);
}
}
// 2. sort by calories ascending
Collections.sort(filterMenu, new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories() - o2.getCalories();
}
});
// 3. map Dish to Dish.getName and collect them
List<String> nameList = new ArrayList<>();
for (Dish dish : filterMenu) {
nameList.add(dish.getName());
}
// print name list
System.out.println(nameList);
}
[season fruit, prawns, rice]
複製代碼
在Java 8以後,經過Stream
只需簡潔明瞭的一行代碼就能搞定:
@Test
public void userJava8() {
List<String> nameList = menu.stream()
// 1. filter which calories is lower than 400
.filter(dish -> dish.getCalories() < 400)
// 2. sort by calories ascending
.sorted(Comparator.comparing(Dish::getCalories))
// 3. map Dish to Dish.getName
.map(Dish::getName)
// 4. collect
.collect(Collectors.toList());
System.out.println(nameList);
}
[season fruit, prawns, rice]
複製代碼
Stream
的強大才剛剛開始……
《Java 8 In Action》給出的解釋以下:
定義
Sequence of elements
——Stream
是一個元素序列,跟集合同樣,管理着若干類型相同的對象元素Source
——Stream
沒法憑空產生,它從一個數據提供源而來,如集合、數組、I/O資源。值得一提的是,Stream
中元素組織的順序將聽從這些元素在數據提供源中組織的順序,而不會打亂這些元素的既有順序。Data processing operations
——Stream
提供相似數據庫訪問同樣的操做,而且只需傳入lambda
表達式便可按照既定的行爲操縱數據,如filter(過濾)
、map(映射)
、reduce(聚合)
、find(查找)
、match(是否有數據匹配)
、sort(排序)
等。Stream
操做還能夠被指定爲串行執行或並行執行以充分利用多CPU核心。特性
Pipelining
——大多數Stream
操做返回的是當前Stream
對象自己,所以能夠鏈式調用,造成一個數據處理的管道,可是也有一些terminal
操做會終結該管道,如調用Stream
的collect
方法後表示該數據處理流的終結,返回最終的數據集合Internal iteration
——Stream
將元素序列的迭代都隱藏了,咱們只需提供數據處理流中的這一個階段到下一個階段的處理行爲(經過調用Stream
方法傳入lambda
表達式)。上一節菜品的例子中數據處理流可描述以下(其中的limit
表示取前n
個元素):
Stream
內部集成了Java 7 提供的ForkJoin
框架,當咱們經過調用它的parallel
開啓並行執行開關時,Stream
會將數據序列的處理經過ForkJoinPool
進行並行化執行(不只僅是開啓多個線程,底層還會根據你CPU核心數量將子任務分配到不一樣的核心執行):
@Test
public void userJava8() {
List<String> nameList = menu.stream().parallel()
.filter(dish -> {
System.out.println(Thread.currentThread().getName());
return dish.getCalories() < 400;
})
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
System.out.println(nameList);
}
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
main
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-1
[season fruit, prawns, rice]
複製代碼
不然在當前線程串行化執行:
@Test
public void userJava8() {
List<String> nameList = menu.stream()
.filter(dish -> {
System.out.println(Thread.currentThread().getName());
return dish.getCalories() < 400;
})
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
System.out.println(nameList);
}
main
main
main
main
main
main
main
main
main
[season fruit, prawns, rice]
複製代碼
上文曾提到,Stream
是沒法憑空而來的,須要一個數據提供源,而咱們最多見的就是數組和集合了。
做爲一個接口,咱們是沒法經過new Stream
獲取其實例的,獲取其實例的經常使用方法有如下幾種
經過Collection
接口中的stream()
方法,從一個集合實例中建立一個Stream
,對同一個集合對象調用屢次stream()
會產生不一樣的Stream
實例。
開篇挑選蘋果的例子中,apples.stream
就是調用了此方法
經過Arrays.stream()
,由一個數組(引用類型或基本類型組)建立一個Stream
@Test
public void testCreateStream() {
Arrays.stream(new Object[]{"hello",21,99.9,true}).forEach(System.out::println);
}
hello
21
99.9
true
複製代碼
Stream.of(T t)
能夠建立一個僅包含一個元素的Stream
Stream.of(T... t)
能夠從一個變長參列(其實是一個數組)建立一個Stream
使用IntStream/LongStream/DoubleStream
中的方法
IntStream
中的range/rangeClosed (int, int)
能夠建立一個包含指定開區間/閉區間中全部元素的Stream
,LongStream
也同樣,但DoubleStream
由於區間中的浮點數有無數個所以沒有此方法
IntStream.rangeClosed(0, 4).forEach(i -> {
new Thread(() -> {
System.out.println("I am a thread, my name is " + Thread.currentThread().getName());
}, "t-" + i).start();
});
I am a thread, my name is t-0
I am a thread, my name is t-1
I am a thread, my name is t-2
I am a thread, my name is t-3
I am a thread, my name is t-4
複製代碼
generate(Supplier s)
,三者都有此方法,經過一個生產策略來建立一個無窮大的Stream
,若是在此Stream
上進行操做那麼每處理完一個元素都會經過調用s.get
獲取下一個要處理的元素。
final int a = 0;
IntStream.generate(() -> a).forEach(System.out::println);
複製代碼
你會發現上述程序會一直打印0
。
經過
generate
建立的stream
,若是在之上進行operation
,那麼該processing
會一直進行下去只有遇到異常時纔會中止
IntStream.generate(i::getAndIncrement).forEach(e -> {
System.out.println(e);
if (e == 5) {
throw new RuntimeException();
}
});
0
1
2
3
4
5
java.lang.RuntimeException
複製代碼
Stream
的數據操縱方法分爲terminal
和non-terminal
,non-terminal
操做以後能夠繼續鏈式調用其餘operation
,造成一個流式管道,而terminal
操做則會終止數據流的移動。Stream
中方法返回Stream
(實際返回的是當前Stream
實例自己)的都是non-terminal option
調用Stream
的filter
並傳入一個Predicate
能夠對元素序列進行過濾,丟棄不符合條件(是否符合條件根據你傳入的Predicate
進行判斷)的元素
// print the even number between 1 and 10
IntStream.rangeClosed(1, 10).filter(i -> i % 2 == 0).forEach(System.out::println);
2
4
6
8
10
複製代碼
@Test
public void testDistinct() {
Stream.of("a", "b", "a", "d", "g", "b").distinct().forEach(System.out::println);
}
a
b
d
g
複製代碼
distinct
會根據equals
方法對「相等」的對象進行去重。
至關於SQL
的limit <offset,rows>
語句中的offset
public void testSkip() {
IntStream.rangeClosed(1,10).skip(6).forEach(System.out::println);
}
7
8
9
10
複製代碼
至關於SQL
的limit <offset,rows>
語句中的rows
// 丟棄前6個以後Stream只剩下7~10了,再截取前2個,Stream就只有7和8了
IntStream.rangeClosed(1, 10).skip(6).limit(2).forEach(System.out::println);
7
8
複製代碼
map
方法接收一個Function
(接收一個對象,返回一個對象),返回什麼對象、需不須要藉助傳入的對象信息就是映射的邏輯,根據你的所需你能夠將Stream
中的全部元素統一換一個類型。
// 從一份菜單(菜品集合)中提取菜品名稱清單
List<String> nameList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
System.out.println(menu);
[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
複製代碼
flatMap
能夠將你的元素進一步細粒度化,如從某個文件讀取文件內容後根據換行符分割建立一個以「行」爲元素的Stream
,若是你想進一步將「行」按照「空格」分割獲得以「字詞」爲元素的Stream
,那麼你就可使用flatMap
。
你須要傳入一個Function<T,Stream>
:傳入一個元素,返回一個包含由該元素分割造成若干細粒度元素的Stream
Stream.of("hello", "world").
flatMap(str -> Stream.of(str.split(""))).
forEach(System.out::println);
h
e
l
l
o
w
o
r
l
d
複製代碼
sort
須要你傳入一個定義了排序規則的Comparator
,它至關於一個BiPredicate
// 將菜品按照卡路里升序排序
menu.stream().
sorted((dish1,dish2)->dish1.getCalories()-dish2.getCalories())
.map(Dish::getCalories)
.forEach(System.out::println);
120
300
350
400
450
530
550
700
800
複製代碼
你會發現IDEA提示你(dish1,dish2)->dish1.getCalories()-dish2.getCalories()
能夠簡化爲Comparator.comparingInt(Dish::getCalories)
,使用comparing
你能夠直接傳入排序字段如getColaries
,它會自動幫咱們封裝成一個升序的BiPredicate
,若是你須要降序則再鏈式調用reversed
便可:
menu.stream().
sorted(Comparator.comparingInt(Dish::getCalories).reversed())
.map(Dish::getCalories)
.forEach(System.out::println);
800
700
550
530
450
400
350
300
120
複製代碼
這是一個terminal option
,當Stream
調用match
以後會返回一個布爾值,match
會根據你傳入的Predicate
返回true or false
,表示當前Stream
中的全部元素是否存在至少一個匹配(anyMatch
)、是否所有匹配(allMatch
)、是否全都不匹配(noneMatch
)。
這幾僅以anyMatch
的使用示例:
// 菜單中是否有卡路里小於100的菜品
boolean res = menu.stream().anyMatch(dish -> dish.getCalories() < 100);
System.out.println(res);
false
複製代碼
這也是一個terminal operation
。一般用於在對Stream
進行一系列處理以後剩下的元素中取出一個進行消費。
findAny
,一般用於並行處理Stream
時,獲取最早走完整個數據處理流程的元素。
好比對於一個id
,可能會開幾個線程並行地調接口、查數據庫、查緩存、查ES的方式獲取商品數據,但只要有其中一個方式成功返回數據那麼就直接消費這個數據,其餘方式不予等待。
static Random random = new Random();
public Object queryById(Integer id){
// 隨機睡眠0~6秒,模仿從數據庫或者調接口獲取數據的過程
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(6000));
String data = UUID.randomUUID().toString();
System.out.println("get data -> " + data + "[id=" + id + "]");
return data;
} catch (InterruptedException e) {
e.printStackTrace();
return e;
}
}
@Test
public void testFindAny() throws InterruptedException {
Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
// 對於每一個id並行獲取對應的數據
.parallel()
.map(id -> {
Object res = queryById(id);
if ( res instanceof Throwable) {
throw new RuntimeException();
}
return res;
})
// 有一個拿到了就直接用,後面到的無論了
.findAny();
dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
Thread.currentThread().join();
}
get data -> 6722c684-61a6-4065-9472-a58a08bbc9d0[id=1],spend time : 442 ms
get data -> 1975f02b-4e54-48f5-9dd0-51bf2789218e[id=2],spend time : 1820 ms
get data -> fd4bacb1-a34d-450d-8ebd-5f390167f5f8[id=4],spend time : 4585 ms
get data -> b2336c45-c1f9-4cd3-b076-83433fdaf543[id=3],spend time : 4772 ms
get data -> fcc25929-7765-467a-bf36-b85afab5efe6[id=5],spend time : 5575 ms
consume data : 6722c684-61a6-4065-9472-a58a08bbc9d0
複製代碼
上面的輸出中consume data
始終都是spend time
最短的數據
findFirst
則必須等到Stream
中元素組織順序(初始時是數據提供源的元素順序,若是你調用了sorted
那麼該順序就會變)的第一個元素處理完前面的流程而後消費它:
Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
// 對於每一個id並行獲取對應的數據
.parallel()
.map(id -> {
Object res = queryById(id);
if ( res instanceof Throwable) {
throw new RuntimeException();
}
return res;
})
// 先拿到的先用,後面到的無論了
.findFirst();
dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
Thread.currentThread().join();
get data -> d6ac2dd6-66b6-461c-91c4-fa2f13326210[id=4],spend time : 1271 ms
get data -> 560f5c0e-c2ac-4030-becc-1ebe51ebedcb[id=5],spend time : 2343 ms
get data -> 11925a9f-03e8-4136-8411-81994445167e[id=1],spend time : 2825 ms
get data -> 0ecfa02e-3903-4d73-a18b-eb9ac833a899[id=2],spend time : 3270 ms
get data -> 930d7091-cfa6-4561-a400-8d2e268aaa83[id=3],spend time : 5166 ms
consume data : 11925a9f-03e8-4136-8411-81994445167e
複製代碼
consume data
始終都是調用findFirst
時在Stream
排在第一位的元素。
reduce
是對當前Stream
中的元素進行求和、求乘積、求最大/小值等須要遍歷全部元素得出一個結果的聚合操做。它有以下重載方法:
Optional<T> reduce(BinaryOperator<T> accumulator);
經過accumulator
對Stream
中的元素作聚合操做,返回一個包裝了操做結果的Optional
,經過該Optional
能夠拿到該結果以及判斷該結果是否存在(若是Stream
沒有元素,那麼天然也就沒有聚合結果了)
accumulator
是一個BiFunction
,至關於你在遍歷時訪問相鄰的兩個元素得出一個結果,這樣Stream
就能依據此邏輯遍歷全部元素獲得最終結果
Optional<Dish> dishOptional = menu.stream()
.filter(dish -> dish.getCalories() > 600)
.reduce((d1, d2) -> d1.getCalories() < d2.getCalories() ? d1 : d2);
if (dishOptional.isPresent()) {
Dish dish = dishOptional.get();
System.out.println("大於600卡路里的菜品中,卡路里最小的是:" + dish);
} else {
System.out.println("沒有大於600卡路里的菜品");
}
大於600卡路里的菜品中,卡路里最小的是:beef
OptionalInt reduce = IntStream.rangeClosed(1, 10)
.reduce((i, j) -> i + j); // -> method inference: Integer::sum
reduce.ifPresent(System.out::println);
55
複製代碼
方法邏輯的僞代碼以下:
if(stream is emtpy){
return optional(null)
}else{
result = new Element()
foreach element in stream
result = accumulator.apply(element, result);
return optional(result)
}
複製代碼
T reduce(T identity, BinaryOperator<T> accumulator);
此方法增長了一個identity
,它的做用是若是調用reduce
時當前Stream
中沒有元素了,也應該返回一個identity
做爲默認的初始結果。不然,調用空的Optional
的get
會報錯:
OptionalInt reduce = IntStream.rangeClosed(1, 10)
.filter(i -> i > 10)
.reduce(Integer::sum);
System.out.println(reduce.isPresent());
System.out.println(reduce.getAsInt());
false
java.util.NoSuchElementException: No value present
複製代碼
若是加了identity
,reduce
不管如何都將返回一個明確的結果(若是有元素就返回聚合後的結果,不然返回identity
),而不是一個結果未知的Optional
int res = IntStream.rangeClosed(1, 10)
.filter(i -> i > 10)
.reduce(0, Integer::sum);
System.out.println(res);
0
複製代碼
值得注意的是,identity
的值不該該隨便給出,給出的規則應該符合:若是Stream
中有元素,那麼對於任意元素element
,給定的identity
應該知足accumulator.apply(element, result)
,不然會出現以下使人困惑的現象:
int res = IntStream.rangeClosed(1, 10).reduce(1, (i, j) -> i * j);
System.out.println(res); //3628800
int res = IntStream.rangeClosed(1, 10).reduce(0, (i, j) -> i * j);
System.out.println(res); //0
複製代碼
上面給定的identity
爲1
、accumulator
邏輯爲累乘時,知足對於任意元素e
都有e * 1 == e
,所以不會影響聚合邏輯;而將identity
換成0
,不管Stream
中有什麼元素,聚合結果都爲0
。這是由reduce(identity,accumulator)
的方法邏輯致使的,其僞代碼以下:
if(stream is emtpy){
return optional(identity)
}else{
result = identity
foreach element in stream
result = accumulator.apply(element, result);
return optional(result)
}
複製代碼
能夠發現,首先將聚合結果result
置爲identity
,而後將每一個元素累乘到result
中(result = element * result
),因爲任何數乘零都得零,所以聚合結果始終返回0
了。
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
此方法又多了一個combiner
,這個combiner
是爲了支持並行的,若是是在串行狀態下調用那麼傳入的combiner
不會起到任何做用:
String reduce = Stream.of("h", "e", "l", "l", "o").reduce("", (a, b) -> a + b);
System.out.println(reduce);
hello
複製代碼
但若是是串行狀態下調用,那麼combiner
會根據ForkJoin
機制和Stream
使用的Splierator
在子任務回溯合併時對合並的兩個子結果作一些處理:
String result = Stream.of("h", "e", "l", "l", "o")
.parallel()
.reduce("", (a, b) -> a + b, (a, b) -> a + b + "=");
System.out.println(result); //he=llo===
複製代碼
咱們能夠在combiner
中查看當前合併的兩個子結果:
String result = Stream.of("h", "e", "l", "l", "o")
.parallel()
.reduce("", (a, b) -> a + b, (a, b) -> {
System.out.println("combine " + a + " and " + b + ", then append '='");
return a + b + "=";
});
System.out.println("the final result is :" + result);
combine l and o, then append '='
combine l and lo=, then append '='
combine h and e, then append '='
combine he= and llo==, then append '='
the final result is :he=llo===
複製代碼
collect
須要傳入一個Collector
,Collectors
爲咱們封裝了不少Collector
的默認實現。
例如
collect(Collectors.toList())
,能夠將元素收集到一個List
中並返回,相似的有toSet
collect(Collectors.joining())
能將Stream
中的字符串元素拼接起來並返回collect(Collectors.groupBy())
可以實現分組SQL
的外貌,你能夠很容易理解它下面經過一個業務員Trader
和交易Transaction
的例子來鞏固Stream
的運用。
兩個類定義以下:
public class Trader{
private final String name;
private final String city;
public Trader(String n, String c){
this.name = n;
this.city = c;
}
public String getName(){
return this.name;
}
public String getCity(){
return this.city;
}
public String toString(){
return "Trader:"+this.name + " in " + this.city;
}
}
public class Transaction{
private final Trader trader;
private final int year;
private final int value;
public Transaction(Trader trader, int year, int value){
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader(){
return this.trader;
}
public int getYear(){
return this.year;
}
public int getValue(){
return this.value;
}
@Override
public String toString(){
return "{" + this.trader + ", " +
"year: "+this.year+", " +
"value:" + this.value +"}";
}
}
複製代碼
需求以下:
/**
* 1. Find all transactions in the year 2011 and sort them by value (small to high).
* 2. What are all the unique cities where the traders work?
* 3. Find all traders from Cambridge and sort them by name.
* 4. Return a string of all traders’ names sorted alphabetically.
* 5. Are any traders based in Milan?
* 6. Print all transactions’ values from the traders living in Cambridge.
* 7. What’s the highest value of all the transactions?
* 8. Find the transaction with the smallest value.
*/
複製代碼
代碼示例:
//1. Find all transactions in the year 2011 and sort them by value (small to high).
List<Transaction> transactions1 = transactions.stream()
.filter(transaction -> transaction.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList());
System.out.println(transactions1);
//2. What are all the unique cities where the traders work?
String value = transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.distinct()
.reduce("", (c1, c2) -> c1 + " " + c2);
System.out.println(value);
//3. Find all traders from Cambridge and sort them by name.
transactions.stream()
.filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
.map(Transaction::getTrader)
.sorted(Comparator.comparing(Trader::getName))
.forEach(System.out::println);
//4. Return a string of all traders’ names sorted alphabetically.
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.forEach(System.out::println);
//5. Are any traders based in Milan?
boolean res = transactions.stream()
.anyMatch(transaction -> "Milan".equals(transaction.getTrader().getCity()));
System.out.println(res);
//6. Print all transactions’ values from the traders living in Cambridge.
transactions.stream()
.filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
.map(Transaction::getValue)
.forEach(System.out::println);
//7. What’s the highest value of all the transactions?
Optional<Integer> integerOptional = transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max);
System.out.println(integerOptional.get());
//8. Find the transaction with the smallest value.
integerOptional = transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::min);
System.out.println(integerOptional.get());
複製代碼
其實上節介紹reduce
的使用時,就有Optional
的身影,若是你沒有給出identity
,那麼reduce
會給你返回一個Optional
,如此Optional
的身份(拿到這個類的實例,你會立馬條件反射:要在訪問對象以前判斷一下Optional
包裝的對象是否爲null
)會提醒你進行非空判斷,不至於你拿着reduce
返回的null
去使用從而致使空指針異常。
這種將非空判斷交給API的機制,可以讓咱們沒必要每次拿到對象的時候都要爲其是否爲空而提心吊膽,又或十分敏感地每拿到一個對象都進行一下if (obj != null)
。
有了Optional
之後,避免空指針的兩個點轉變以下:
null
的對象,如空字符串""
,空數組等Optional
,如有結果則返回Optional.ofNullable(obj)
,若想返回null
則用Optional.empty()
Optional
,在使用以前它會提醒咱們使用isPresent
或ifPresent
規避空指針說白了,以前須要咱們人爲的記住非空判斷,但引入
Optional
後,非空判斷流程交給API了,卸去了咱們對非空判斷的關注點,規範了流程開發。
相關源碼以下:
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
public boolean isPresent() {
return value != null;
}
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
複製代碼
你會發現其實他的源碼很簡單,就是將咱們要使用的對象包了一層
經過of(非空對象)
、ofNullable(可爲空對象)
來建立Optional
實例
經過isPresent
能夠判斷內部對象是否爲null
經過get
能夠獲取內部對象,若是內部對象爲null
則會拋異常,所以一般在調用get
前要使用isPresent
判斷一下
if(optional.isPresent()){
obj = optional.get();
}
複製代碼
orElse
,若是不爲null
則返回,不然
orElse(T t)
,返回你傳入的對象t
orElseGet(Supplier<T> s)
,調用s.get
獲取一個對象orElseGet(Supplier<Throwable> e)
經過ifPresent(consumer)
,能夠整合判斷和對象訪問,若是對象不爲null
,那就用傳入的consumer
消費它
appleOptional.ifPresent(System.out.println(apple.getName()))
複製代碼
此外,Optional
中還提供了filter
、map
兩個從Stream
中借鑑的方法,做用相似,可自行查看源碼。
其實我還想說
CompletableFutrue
、LocalDate/LocalTime/LocalDateTime
...