5萬字長文:Stream和Lambda表達式最佳實踐-附PDF下載

目錄

1. Streams簡介

今天要講的Stream指的是java.util.stream包中的諸多類。Stream可以方便的將之前的結合類以轉換為Stream並以流式方式進行處理,大大的簡化了我們的編程,Stream包中,最核心的就是interface Stream

從上面的圖中我們可以看到Stream繼承自BaseStream。Stream中定義了很多非常實用的方法,比如filter,map,flatmap,forEach,reduce,collect等等。接下來我們將會逐一講解。

1.1 創建Stream

Stream的創建有很多方式,java引入Stream之後所有的集合類都添加了一個stream()方法,通過這個方法可以直接得到其對應的Stream。也可以通過Stream.of方法來創建:

//Stream Creation
        String[] arr = new String[]{"a", "b", "c"};
        Stream<String> stream = Arrays.stream(arr);
        stream = Stream.of("a", "b", "c");

1.2 Streams多執行緒

如果我們想使用多執行緒來處理集合類的數據,Stream提供了非常方便的多執行緒方法parallelStream():

//Multi-threading
        List<String> list =new ArrayList();
        list.add("aaa");
        list.add("bbb");
        list.add("abc");
        list.add("ccc");
        list.add("ddd");
        list.parallelStream().forEach(element -> doPrint(element));

1.3 Stream的基本操作

Stream的操作可以分為兩類,一類是中間操作,中間操作返回Stream,因此可以級聯調用。 另一類是終止操作,這類操作會返回Stream定義的類型。

//Operations
        long count = list.stream().distinct().count();

上面的例子中,distinct()返回一個Stream,所以可以級聯操作,最後的count()是一個終止操作,返回最後的值。

Matching

Stream提供了anyMatch(), allMatch(), noneMatch()這三種match方式,我們看下怎麼使用:

//Matching
        boolean isValid = list.stream().anyMatch(element -> element.contains("h"));
        boolean isValidOne = list.stream().allMatch(element -> element.contains("h"));
        boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h"));  

Filtering

filter() 方法允許我們對Stream中的數據進行過濾,從而得到我們需要的:

Stream<String> filterStream = list.stream().filter(element -> element.contains("d"));

上面的例子中我們從list中選出了包含「d」字母的String。

Mapping

map就是對Stream中的值進行再加工,然後將加工過後的值作為新的Stream返回。

//Mapping
        Stream<String> mappingStream = list.stream().map(element -> convertElement(element));

    private static String convertElement(String element) {
        return "element"+"abc";
    }

上的例子中我們把list中的每個值都加上了「abc」然後返回一個新的Stream。

FlatMap

flatMap和Map很類似,但是他們兩個又有不同,看名字我們可以看到flatMap意思是打平之後再做Map。

怎麼理解呢?

假如我們有一個CustBook類:

@Data
public class CustBook {

    List<String> bookName;
}

CustBook定義了一個bookName欄位。

先看一下Map返回的結果:

List<CustBook> users = new ArrayList<>();
        users.add(new CustBook());
Stream<Stream<String>> userStreamMap
                = users.stream().map(user -> user.getBookName().stream());

在上面的程式碼中,map將每一個user都轉換成了stream,所以最後的結果是返回Stream的Stream。

如果我們只想返回String,則可以使用FlatMap:

List<CustBook> users = new ArrayList<>();
        users.add(new CustBook());
        Stream<String> userStream
                = users.stream().map(user -> user.getBookName().stream());

簡單點講FlatMap就是將層級關係鋪平重來。

Reduction

使用reduce() 方法可以方便的對集合的數據進行運算,reduce()接收兩個參數,第一個是開始值,後面是一個函數表示累計。

//Reduction
        List<Integer> integers = Arrays.asList(1, 1, 1);
        Integer reduced = integers.stream().reduce(100, (a, b) -> a + b);

上面的例子我們定義了3個1的list,然後調用reduce(100, (a, b) -> a + b)方法,最後的結果是103.

Collecting

collect()方法可以方便的將Stream再次轉換為集合類,方便處理和展示:

List<String> resultList
                = list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());

2. functional interface的分類和使用

java 8引入了lambda表達式,lambda表達式實際上表示的就是一個匿名的function。

在java 8之前,如果需要使用到匿名function需要new一個類的實現,但是有了lambda表達式之後,一切都變的非常簡介。

我們看一個之前講執行緒池的時候的一個例子:

//ExecutorService using class
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new Runnable() {
            @Override
            public void run() {
            log.info("new runnable");
            }
        });

executorService.submit需要接收一個Runnable類,上面的例子中我們new了一個Runnable類,並實現了它的run()方法。

上面的例子如果用lambda表達式來重寫,則如下所示:

//ExecutorService using lambda
        executorService.submit(()->log.info("new runnable"));

看起是不是很簡單,使用lambda表達式就可以省略匿名類的構造,並且可讀性更強。

那麼是不是所有的匿名類都可以用lambda表達式來重構呢?也不是。

我們看下Runnable類有什麼特點:

@FunctionalInterface
public interface Runnable 

Runnable類上面有一個@FunctionalInterface註解。這個註解就是我們今天要講到的Functional Interface。

2.1 Functional Interface

Functional Interface是指帶有 @FunctionalInterface 註解的interface。它的特點是其中只有一個子類必須要實現的abstract方法。如果abstract方法前面帶有default關鍵字,則不做計算。

其實這個也很好理解,因為Functional Interface改寫成為lambda表達式之後,並沒有指定實現的哪個方法,如果有多個方法需要實現的話,就會有問題。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

Functional Interface一般都在java.util.function包中。

根據要實現的方法參數和返回值的不同,Functional Interface可以分為很多種,下面我們分別來介紹。

2.2 Function:一個參數一個返回值

Function介面定義了一個方法,接收一個參數,返回一個參數。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

一般我們在對集合類進行處理的時候,會用到Function。

Map<String, Integer> nameMap = new HashMap<>();
        Integer value = nameMap.computeIfAbsent("name", s -> s.length());

上面的例子中我們調用了map的computeIfAbsent方法,傳入一個Function。

上面的例子還可以改寫成更短的:

Integer value1 = nameMap.computeIfAbsent("name", String::length);

Function沒有指明參數和返回值的類型,如果需要傳入特定的參數,則可以使用IntFunction, LongFunction, DoubleFunction:

@FunctionalInterface
public interface IntFunction<R> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}

如果需要返回特定的參數,則可以使用ToIntFunction, ToLongFunction, ToDoubleFunction:

@FunctionalInterface
public interface ToDoubleFunction<T> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    double applyAsDouble(T value);
}

如果要同時指定參數和返回值,則可以使用DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction:

@FunctionalInterface
public interface LongToIntFunction {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    int applyAsInt(long value);
}

2.3 BiFunction:接收兩個參數,一個返回值

如果需要接受兩個參數,一個返回值的話,可以使用BiFunction:BiFunction, ToDoubleBiFunction, ToIntBiFunction, ToLongBiFunction等。

@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

我們看一個BiFunction的例子:

//BiFunction
        Map<String, Integer> salaries = new HashMap<>();
        salaries.put("alice", 100);
        salaries.put("jack", 200);
        salaries.put("mark", 300);

        salaries.replaceAll((name, oldValue) ->
                name.equals("alice") ? oldValue : oldValue + 200);

2.4 Supplier:無參的Function

如果什麼參數都不需要,則可以使用Supplier:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

2.5 Consumer:接收一個參數,不返回值

Consumer接收一個參數,但是不返回任何值,我們看下Consumer的定義:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

看一個Consumer的具體應用:

//Consumer
        nameMap.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

2.6 Predicate:接收一個參數,返回boolean

Predicate接收一個參數,返回boolean值:

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

如果用在集合類的過濾上面那是極好的:

//Predicate
        List<String> names = Arrays.asList("A", "B", "C", "D", "E");
        List<String> namesWithA = names.stream()
                .filter(name -> name.startsWith("A"))
                .collect(Collectors.toList());

2.7 Operator:接收和返回同樣的類型

Operator接收和返回同樣的類型,有很多種Operator:UnaryOperator BinaryOperator ,DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, LongBinaryOperator等。

@FunctionalInterface
public interface IntUnaryOperator {

    /**
     * Applies this operator to the given operand.
     *
     * @param operand the operand
     * @return the operator result
     */
    int applyAsInt(int operand);

我們看一個BinaryOperator的例子:

 //Operator
        List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
        int sum = values.stream()
                .reduce(0, (i1, i2) -> i1 + i2);

3. Lambda表達式最佳實踐

Lambda表達式java 8引入的函數式編程框架。之前的文章中我們也講過Lambda表達式的基本用法。

本文將會在之前的文章基礎上更加詳細的講解Lambda表達式在實際應用中的最佳實踐經驗。

3.1 優先使用標準Functional介面

之前的文章我們講到了,java在java.util.function包中定義了很多Function介面。基本上涵蓋了我們能夠想到的各種類型。

假如我們自定義了下面的Functional interface:

@FunctionalInterface
public interface Usage {
    String method(String string);
}

然後我們需要在一個test方法中傳入該interface:

public String test(String string, Usage usage) {
    return usage.method(string);
}

上面我們定義的函數介面需要實現method方法,接收一個String,返回一個String。這樣我們完全可以使用Function來代替:

public String test(String string, Function<String, String> fn) {
    return fn.apply(string);
}

使用標準介面的好處就是,不要重複造輪子。

3.2 使用@FunctionalInterface註解

雖然@FunctionalInterface不是必須的,不使用@FunctionalInterface也可以定義一個Functional Interface。

但是使用@FunctionalInterface可以在違背Functional Interface定義的時候報警。

如果是在維護一個大型項目中,加上@FunctionalInterface註解可以清楚的讓其他人了解這個類的作用。

從而使程式碼更加規範和更加可用。

所以我們需要這樣定義:

@FunctionalInterface
public interface Usage {
    String method(String string);
}

而不是:

public interface Usage {
    String method(String string);
}

3.3 在Functional Interfaces中不要濫用Default Methods

Functional Interface是指只有一個未實現的抽象方法的介面。

如果該Interface中有多個方法,則可以使用default關鍵字為其提供一個默認的實現。

但是我們知道Interface是可以多繼承的,一個class可以實現多個Interface。 如果多個Interface中定義了相同的default方法,則會報錯。

通常來說default關鍵字一般用在升級項目中,避免程式碼報錯。

3.4 使用Lambda 表達式來實例化Functional Interface

還是上面的例子:

@FunctionalInterface
public interface Usage {
    String method(String string);
}

要實例化Usage,我們可以使用new關鍵詞:

Usage usage = new Usage() {
    @Override
    public String method(String string) {
        return string;
    }
};

但是最好的辦法就是用lambda表達式:

Usage usage = parameter -> parameter;

3.5 不要重寫Functional Interface作為參數的方法

怎麼理解呢? 我們看下面兩個方法:

public class ProcessorImpl implements Processor {
    @Override
    public String process(Callable<String> c) throws Exception {
        // implementation details
    }
 
    @Override
    public String process(Supplier<String> s) {
        // implementation details
    }
}

兩個方法的方法名是一樣的,只有傳入的參數不同。但是兩個參數都是Functional Interface,都可以用同樣的lambda表達式來表示。

在調用的時候:

String result = processor.process(() -> "test");

因為區別不了到底調用的哪個方法,則會報錯。

最好的辦法就是將兩個方法的名字修改為不同的。

3.6 Lambda表達式和內部類是不同的

雖然我們之前講到使用lambda表達式可以替換內部類。但是兩者的作用域範圍是不同的。

在內部類中,會創建一個新的作用域範圍,在這個作用域範圍之內,你可以定義新的變數,並且可以用this引用它。

但是在Lambda表達式中,並沒有定義新的作用域範圍,如果在Lambda表達式中使用this,則指向的是外部類。

我們舉個例子:

private String value = "Outer scope value";

public String scopeExperiment() {
    Usage usage = new Usage() {
        String value = "Inner class value";
 
        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String result = usage.method("");
 
    Usage usageLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = usageLambda.method("");
 
    return "Results: result = " + result + 
      ", resultLambda = " + resultLambda;
}

上面的例子將會輸出「Results: result = Inner class value, resultLambda = Outer scope value」

3.7 Lambda Expression儘可能簡潔

通常來說一行程式碼即可。如果你有非常多的邏輯,可以將這些邏輯封裝成一個方法,在lambda表達式中調用該方法即可。

因為lambda表達式說到底還是一個表達式,表達式當然越短越好。

java通過類型推斷來判斷傳入的參數類型,所以我們在lambda表達式的參數中盡量不傳參數類型,像下面這樣:

(a, b) -> a.toLowerCase() + b.toLowerCase();

而不是:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

如果只有一個參數的時候,不需要帶括弧:

a -> a.toLowerCase();

而不是:

(a) -> a.toLowerCase();

返回值不需要帶return:

a -> a.toLowerCase();

而不是:

a -> {return a.toLowerCase()};

3.8 使用方法引用

為了讓lambda表達式更加簡潔,在可以使用方法引用的時候,我們可以使用方法引用:

a -> a.toLowerCase();

可以被替換為:

String::toLowerCase;

3.9 Effectively Final 變數

如果在lambda表達式中引用了non-final變數,則會報錯。

effectively final是什麼意思呢?這個是一個近似final的意思。只要一個變數只被賦值一次,那麼編譯器將會把這個變數看作是effectively final的。

    String localVariable = "Local";
    Usage usage = parameter -> {
         localVariable = parameter;
        return localVariable;
    };

上面的例子中localVariable被賦值了兩次,從而不是一個Effectively Final 變數,會編譯報錯。

為什麼要這樣設置呢?因為lambda表達式通常會用在並行計算中,當有多個執行緒同時訪問變數的時候Effectively Final 變數可以防止不可以預料的修改。

4. stream表達式中實現if/else邏輯

在Stream處理中,我們通常會遇到if/else的判斷情況,對於這樣的問題我們怎麼處理呢?

還記得我們在上一篇文章lambda最佳實踐中提到,lambda表達式應該越簡潔越好,不要在其中寫臃腫的業務邏輯。

接下來我們看一個具體的例子。

4.1 傳統寫法

假如我們有一個1 to 10的list,我們想要分別挑選出奇數和偶數出來,傳統的寫法,我們會這樣使用:

    public void inForEach(){
        List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        ints.stream()
                .forEach(i -> {
                    if (i.intValue() % 2 == 0) {
                        System.out.println("i is even");
                    } else {
                        System.out.println("i is old");
                    }
                });
    }

上面的例子中,我們把if/else的邏輯放到了forEach中,雖然沒有任何問題,但是程式碼顯得非常臃腫。

接下來看看怎麼對其進行改寫。

4.2 使用filter

我們可以把if/else的邏輯改寫為兩個filter:

List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Stream<Integer> evenIntegers = ints.stream()
                .filter(i -> i.intValue() % 2 == 0);
        Stream<Integer> oddIntegers = ints.stream()
                .filter(i -> i.intValue() % 2 != 0);

有了這兩個filter,再在filter過後的stream中使用for each:

        evenIntegers.forEach(i -> System.out.println("i is even"));
        oddIntegers.forEach(i -> System.out.println("i is old"));

怎麼樣,程式碼是不是非常簡潔明了。

5. 在map中使用stream

Map是java中非常常用的一個集合類型,我們通常也需要去遍歷Map去獲取某些值,java 8引入了Stream的概念,那麼我們怎麼在Map中使用Stream呢?

5.1 基本概念

Map有key,value還有表示key,value整體的Entry。

創建一個Map:

Map<String, String> someMap = new HashMap<>();

獲取Map的entrySet:

Set<Map.Entry<String, String>> entries = someMap.entrySet();

獲取map的key:

Set<String> keySet = someMap.keySet();

獲取map的value:

Collection<String> values = someMap.values();

上面我們可以看到有這樣幾個集合:Map,Set,Collection。

除了Map沒有stream,其他兩個都有stream方法:

Stream<Map.Entry<String, String>> entriesStream = entries.stream();
        Stream<String> valuesStream = values.stream();
        Stream<String> keysStream = keySet.stream();

我們可以通過其他幾個stream來遍歷map。

5.2 使用Stream獲取map的key

我們先給map添加幾個值:

someMap.put("jack","20");
someMap.put("bill","35");

上面我們添加了name和age欄位。

如果我們想查找age=20的key,則可以這樣做:

Optional<String> optionalName = someMap.entrySet().stream()
                .filter(e -> "20".equals(e.getValue()))
                .map(Map.Entry::getKey)
                .findFirst();

        log.info(optionalName.get());

因為返回的是Optional,如果值不存在的情況下,我們也可以處理:

optionalName = someMap.entrySet().stream()
                .filter(e -> "Non ages".equals(e.getValue()))
                .map(Map.Entry::getKey).findFirst();

        log.info("{}",optionalName.isPresent());

上面的例子我們通過調用isPresent來判斷age是否存在。

如果有多個值,我們可以這樣寫:

someMap.put("alice","20");
        List<String> listnames = someMap.entrySet().stream()
                .filter(e -> e.getValue().equals("20"))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

        log.info("{}",listnames);

上面我們調用了collect(Collectors.toList())將值轉成了List。

5.3 使用stream獲取map的value

上面我們獲取的map的key,同樣的我們也可以獲取map的value:

List<String> listAges = someMap.entrySet().stream()
                .filter(e -> e.getKey().equals("alice"))
                .map(Map.Entry::getValue)
                .collect(Collectors.toList());

        log.info("{}",listAges);

上面我們匹配了key值是alice的value。

6. Stream中的操作類型和peek的使用

java 8 stream作為流式操作有兩種操作類型,中間操作和終止操作。這兩種有什麼區別呢?

我們看一個peek的例子:

Stream<String> stream = Stream.of("one", "two", "three","four");
        stream.peek(System.out::println);

上面的例子中,我們的本意是列印出Stream的值,但實際上沒有任何輸出。

為什麼呢?

6.1 中間操作和終止操作

一個java 8的stream是由三部分組成的。數據源,零個或一個或多個中間操作,一個或零個終止操作。

中間操作是對數據的加工,注意,中間操作是lazy操作,並不會立馬啟動,需要等待終止操作才會執行。

終止操作是stream的啟動操作,只有加上終止操作,stream才會真正的開始執行。

所以,問題解決了,peek是一個中間操作,所以上面的例子沒有任何輸出。

6.2 peek

我們看下peek的文檔說明:peek主要被用在debug用途。

我們看下debug用途的使用:

Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList());

上面的例子輸出:

Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

上面的例子我們輸出了stream的中間值,方便我們的調試。

為什麼只作為debug使用呢?我們再看一個例子:

Stream.of("one", "two", "three","four").peek(u -> u.toUpperCase())
                .forEach(System.out::println);

上面的例子我們使用peek將element轉換成為upper case。然後輸出:

one
two
three
four

可以看到stream中的元素並沒有被轉換成大寫格式。

再看一個map的對比:

Stream.of("one", "two", "three","four").map(u -> u.toUpperCase())
                .forEach(System.out::println);

輸出:

ONE
TWO
THREE
FOUR

可以看到map是真正的對元素進行了轉換。

當然peek也有例外,假如我們Stream裡面是一個對象會怎麼樣?

    @Data
    @AllArgsConstructor
    static class User{
        private String name;
    }
        List<User> userList=Stream.of(new User("a"),new User("b"),new User("c")).peek(u->u.setName("kkk")).collect(Collectors.toList());
        log.info("{}",userList);

輸出結果:

10:25:59.784 [main] INFO com.flydean.PeekUsage - [PeekUsage.User(name=kkk), PeekUsage.User(name=kkk), PeekUsage.User(name=kkk)]

我們看到如果是對象的話,實際的結果會被改變。

為什麼peek和map有這樣的區別呢?

我們看下peek和map的定義:

Stream<T> peek(Consumer<? super T> action)
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

peek接收一個Consumer,而map接收一個Function。

Consumer是沒有返回值的,它只是對Stream中的元素進行某些操作,但是操作之後的數據並不返回到Stream中,所以Stream中的元素還是原來的元素。

而Function是有返回值的,這意味著對於Stream的元素的所有操作都會作為新的結果返回到Stream中。

這就是為什麼peek String不會發生變化而peek Object會發送變化的原因。

7. lambda表達式中的異常處理

java 8中引入了lambda表達式,lambda表達式可以讓我們的程式碼更加簡介,業務邏輯更加清晰,但是在lambda表達式中使用的Functional Interface並沒有很好的處理異常,因為JDK提供的這些Functional Interface通常都是沒有拋出異常的,這意味著需要我們自己手動來處理異常。

因為異常分為Unchecked Exception和checked Exception,我們分別來討論。

7.1 處理Unchecked Exception

Unchecked exception也叫做RuntimeException,出現RuntimeException通常是因為我們的程式碼有問題。RuntimeException是不需要被捕獲的。也就是說如果有RuntimeException,沒有捕獲也可以通過編譯。

我們看一個例子:

List<Integer> integers = Arrays.asList(1,2,3,4,5);
        integers.forEach(i -> System.out.println(1 / i));

這個例子是可以編譯成功的,但是上面有一個問題,如果list中有一個0的話,就會拋出ArithmeticException。

雖然這個是一個Unchecked Exception,但是我們還是想處理一下:

        integers.forEach(i -> {
            try {
                System.out.println(1 / i);
            } catch (ArithmeticException e) {
                System.err.println(
                        "Arithmetic Exception occured : " + e.getMessage());
            }
        });

上面的例子我們使用了try,catch來處理異常,簡單但是破壞了lambda表達式的最佳實踐。程式碼變得臃腫。

我們將try,catch移到一個wrapper方法中:

    static Consumer<Integer> lambdaWrapper(Consumer<Integer> consumer) {
        return i -> {
            try {
                consumer.accept(i);
            } catch (ArithmeticException e) {
                System.err.println(
                        "Arithmetic Exception occured : " + e.getMessage());
            }
        };
    }

則原來的調用變成這樣:

integers.forEach(lambdaWrapper(i -> System.out.println(1 / i)));

但是上面的wrapper固定了捕獲ArithmeticException,我們再將其改編成一個更通用的類:

    static <T, E extends Exception> Consumer<T>
    consumerWrapperWithExceptionClass(Consumer<T> consumer, Class<E> clazz) {

        return i -> {
            try {
                consumer.accept(i);
            } catch (Exception ex) {
                try {
                    E exCast = clazz.cast(ex);
                    System.err.println(
                            "Exception occured : " + exCast.getMessage());
                } catch (ClassCastException ccEx) {
                    throw ex;
                }
            }
        };
    }

上面的類傳入一個class,並將其cast到異常,如果能cast,則處理,否則拋出異常。

這樣處理之後,我們這樣調用:

integers.forEach(
                consumerWrapperWithExceptionClass(
                        i -> System.out.println(1 / i),
                        ArithmeticException.class));

7.2 處理checked Exception

checked Exception是必須要處理的異常,我們還是看個例子:

    static void throwIOException(Integer integer) throws IOException {
    }
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
        integers.forEach(i -> throwIOException(i));

上面我們定義了一個方法拋出IOException,這是一個checked Exception,需要被處理,所以在下面的forEach中,程式會編譯失敗,因為沒有處理相應的異常。

最簡單的辦法就是try,catch住,如下所示:

        integers.forEach(i -> {
            try {
                throwIOException(i);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });

當然,這樣的做法的壞處我們在上面已經講過了,同樣的,我們可以定義一個新的wrapper方法:

    static <T> Consumer<T> consumerWrapper(
            ThrowingConsumer<T, Exception> throwingConsumer) {

        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        };
    }

我們這樣調用:

integers.forEach(consumerWrapper(i -> throwIOException(i)));

我們也可以封裝一下異常:

static <T, E extends Exception> Consumer<T> consumerWrapperWithExceptionClass(
            ThrowingConsumer<T, E> throwingConsumer, Class<E> exceptionClass) {

        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                try {
                    E exCast = exceptionClass.cast(ex);
                    System.err.println(
                            "Exception occured : " + exCast.getMessage());
                } catch (ClassCastException ccEx) {
                    throw new RuntimeException(ex);
                }
            }
        };
    }

然後這樣調用:

integers.forEach(consumerWrapperWithExceptionClass(
                i -> throwIOException(i), IOException.class));

8. stream中throw Exception

之前的文章我們講到,在stream中處理異常,需要將checked exception轉換為unchecked exception來處理。

我們是這樣做的:

    static <T> Consumer<T> consumerWrapper(
            ThrowingConsumer<T, Exception> throwingConsumer) {

        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        };
    }

將異常捕獲,然後封裝成為RuntimeException。

封裝成RuntimeException感覺總是有那麼一點點問題,那麼有沒有什麼更好的辦法?

8.1 throw小訣竅

java的類型推斷大家應該都知道,如果是 這樣的形式,那麼T將會被認為是RuntimeException!

我們看下例子:

public class RethrowException {

    public static <T extends Exception, R> R throwException(Exception t) throws T {
        throw (T) t; // just throw it, convert checked exception to unchecked exception
    }

}

上面的類中,我們定義了一個throwException方法,接收一個Exception參數,將其轉換為T,這裡的T就是unchecked exception。

接下來看下具體的使用:

@Slf4j
public class RethrowUsage {

    public static void main(String[] args) {
        try {
            throwIOException();
        } catch (IOException e) {
           log.error(e.getMessage(),e);
            RethrowException.throwException(e);
        }
    }

    static void throwIOException() throws IOException{
        throw new IOException("io exception");
    }
}

上面的例子中,我們將一個IOException轉換成了一個unchecked exception。

9. stream中Collectors的用法

在java stream中,我們通常需要將處理後的stream轉換成集合類,這個時候就需要用到stream.collect方法。collect方法需要傳入一個Collector類型,要實現Collector還是很麻煩的,需要實現好幾個介面。

於是java提供了更簡單的Collectors工具類來方便我們構建Collector。

下面我們將會具體講解Collectors的用法。

假如我們有這樣兩個list:

List<String> list = Arrays.asList("jack", "bob", "alice", "mark");
List<String> duplicateList = Arrays.asList("jack", "jack", "alice", "mark");

上面一個是無重複的list,一個是帶重複數據的list。接下來的例子我們會用上面的兩個list來講解Collectors的用法。

9.1 Collectors.toList()

List<String> listResult = list.stream().collect(Collectors.toList());
        log.info("{}",listResult);

將stream轉換為list。這裡轉換的list是ArrayList,如果想要轉換成特定的list,需要使用toCollection方法。

9.2 Collectors.toSet()

Set<String> setResult = list.stream().collect(Collectors.toSet());
        log.info("{}",setResult);

toSet將Stream轉換成為set。這裡轉換的是HashSet。如果需要特別指定set,那麼需要使用toCollection方法。

因為set中是沒有重複的元素,如果我們使用duplicateList來轉換的話,會發現最終結果中只有一個jack。

Set<String> duplicateSetResult = duplicateList.stream().collect(Collectors.toSet());
        log.info("{}",duplicateSetResult);

9.3 Collectors.toCollection()

上面的toMap,toSet轉換出來的都是特定的類型,如果我們需要自定義,則可以使用toCollection()

List<String> custListResult = list.stream().collect(Collectors.toCollection(LinkedList::new));
        log.info("{}",custListResult);

上面的例子,我們轉換成了LinkedList。

9.4 Collectors.toMap()

toMap接收兩個參數,第一個參數是keyMapper,第二個參數是valueMapper:

Map<String, Integer> mapResult = list.stream()
                .collect(Collectors.toMap(Function.identity(), String::length));
        log.info("{}",mapResult);

如果stream中有重複的值,則轉換會報IllegalStateException異常:

Map<String, Integer> duplicateMapResult = duplicateList.stream()
                .collect(Collectors.toMap(Function.identity(), String::length));

怎麼解決這個問題呢?我們可以這樣:

Map<String, Integer> duplicateMapResult2 = duplicateList.stream()
                .collect(Collectors.toMap(Function.identity(), String::length, (item, identicalItem) -> item));
        log.info("{}",duplicateMapResult2);

在toMap中添加第三個參數mergeFunction,來解決衝突的問題。

9.5 Collectors.collectingAndThen()

collectingAndThen允許我們對生成的集合再做一次操作。

List<String> collectAndThenResult = list.stream()
                .collect(Collectors.collectingAndThen(Collectors.toList(), l -> {return new ArrayList<>(l);}));
        log.info("{}",collectAndThenResult);

9.6 Collectors.joining()

Joining用來連接stream中的元素:

String joinResult = list.stream().collect(Collectors.joining());
        log.info("{}",joinResult);
        String joinResult1 = list.stream().collect(Collectors.joining(" "));
        log.info("{}",joinResult1);
        String joinResult2 = list.stream().collect(Collectors.joining(" ", "prefix","suffix"));
        log.info("{}",joinResult2);

可以不帶參數,也可以帶一個參數,也可以帶三個參數,根據我們的需要進行選擇。

9.7 Collectors.counting()

counting主要用來統計stream中元素的個數:

Long countResult = list.stream().collect(Collectors.counting());
        log.info("{}",countResult);

9.8 Collectors.summarizingDouble/Long/Int()

SummarizingDouble/Long/Int為stream中的元素生成了統計資訊,返回的結果是一個統計類:

IntSummaryStatistics intResult = list.stream()
                .collect(Collectors.summarizingInt(String::length));
        log.info("{}",intResult);

輸出結果:

22:22:35.238 [main] INFO com.flydean.CollectorUsage - IntSummaryStatistics{count=4, sum=16, min=3, average=4.000000, max=5}

9.9 Collectors.averagingDouble/Long/Int()

averagingDouble/Long/Int()對stream中的元素做平均:

Double averageResult = list.stream().collect(Collectors.averagingInt(String::length));
        log.info("{}",averageResult);

9.10 Collectors.summingDouble/Long/Int()

summingDouble/Long/Int()對stream中的元素做sum操作:

Double summingResult = list.stream().collect(Collectors.summingDouble(String::length));
        log.info("{}",summingResult);

9.11 Collectors.maxBy()/minBy()

maxBy()/minBy()根據提供的Comparator,返回stream中的最大或者最小值:

Optional<String> maxByResult = list.stream().collect(Collectors.maxBy(Comparator.naturalOrder()));
        log.info("{}",maxByResult);

9.12 Collectors.groupingBy()

GroupingBy根據某些屬性進行分組,並返回一個Map:

Map<Integer, Set<String>> groupByResult = list.stream()
                .collect(Collectors.groupingBy(String::length, Collectors.toSet()));
        log.info("{}",groupByResult);

9.13 Collectors.partitioningBy()

PartitioningBy是一個特別的groupingBy,PartitioningBy返回一個Map,這個Map是以boolean值為key,從而將stream分成兩部分,一部分是匹配PartitioningBy條件的,一部分是不滿足條件的:

 Map<Boolean, List<String>> partitionResult = list.stream()
                .collect(Collectors.partitioningBy(s -> s.length() > 3));
        log.info("{}",partitionResult);

看下運行結果:

22:39:37.082 [main] INFO com.flydean.CollectorUsage - {false=[bob], true=[jack, alice, mark]}

結果被分成了兩部分。

10. 創建一個自定義的collector

在之前的java collectors文章裡面,我們講到了stream的collect方法可以調用Collectors裡面的toList()或者toMap()方法,將結果轉換為特定的集合類。

今天我們介紹一下怎麼自定義一個Collector。

10.1 Collector介紹

我們先看一下Collector的定義:

Collector介面需要實現supplier(),accumulator(),combiner(),finisher(),characteristics()這5個介面。

同時Collector也提供了兩個靜態of方法來方便我們創建一個Collector實例。

我們可以看到兩個方法的參數跟Collector介面需要實現的介面是一一對應的。

下面分別解釋一下這幾個參數:

  • supplier

Supplier是一個函數,用來創建一個新的可變的集合。換句話說Supplier用來創建一個初始的集合。

  • accumulator

accumulator定義了累加器,用來將原始元素添加到集合中。

  • combiner

combiner用來將兩個集合合併成一個。

  • finisher

finisher將集合轉換為最終的集合類型。

  • characteristics

characteristics表示該集合的特徵。這個不是必須的參數。

Collector定義了三個參數類型,T是輸入元素的類型,A是reduction operation的累加類型也就是Supplier的初始類型,R是最終的返回類型。 我們畫個圖來看一下這些類型之間的轉換關係:

有了這幾個參數,我們接下來看看怎麼使用這些參數來構造一個自定義Collector。

10.2 自定義Collector

我們利用Collector的of方法來創建一個不變的Set:

    public static <T> Collector<T, Set<T>, Set<T>> toImmutableSet() {
        return Collector.of(HashSet::new, Set::add,
                (left, right) -> {
                    left.addAll(right);
                    return left;
                }, Collections::unmodifiableSet);
    }

上面的例子中,我們HashSet::new作為supplier,Set::add作為accumulator,自定義了一個方法作為combiner,最後使用Collections::unmodifiableSet將集合轉換成不可變集合。

上面我們固定使用HashSet::new作為初始集合的生成方法,實際上,上面的方法可以更加通用:

    public static <T, A extends Set<T>> Collector<T, A, Set<T>> toImmutableSet(
            Supplier<A> supplier) {

        return Collector.of(
                supplier,
                Set::add, (left, right) -> {
                    left.addAll(right);
                    return left;
                }, Collections::unmodifiableSet);
    }

上面的方法,我們將supplier提出來作為一個參數,由外部來定義。

看下上面兩個方法的測試:

    @Test
    public void toImmutableSetUsage(){
        Set<String> stringSet1=Stream.of("a","b","c","d")
                .collect(ImmutableSetCollector.toImmutableSet());
        log.info("{}",stringSet1);

        Set<String> stringSet2=Stream.of("a","b","c","d")
                .collect(ImmutableSetCollector.toImmutableSet(LinkedHashSet::new));
        log.info("{}",stringSet2);
    }

輸出:

INFO com.flydean.ImmutableSetCollector - [a, b, c, d]
INFO com.flydean.ImmutableSetCollector - [a, b, c, d]

11. stream reduce詳解和誤區

Stream API提供了一些預定義的reduce操作,比如count(), max(), min(), sum()等。如果我們需要自己寫reduce的邏輯,則可以使用reduce方法。

本文將會詳細分析一下reduce方法的使用,並給出具體的例子。

11.1 reduce詳解

Stream類中有三種reduce,分別接受1個參數,2個參數,和3個參數,首先來看一個參數的情況:

Optional<T> reduce(BinaryOperator<T> accumulator);

該方法接受一個BinaryOperator參數,BinaryOperator是一個@FunctionalInterface,需要實現方法:

R apply(T t, U u);

accumulator告訴reduce方法怎麼去累計stream中的數據。

舉個例子:

List<Integer> intList = Arrays.asList(1,2,3);
        Optional<Integer> result1=intList.stream().reduce(Integer::sum);
        log.info("{}",result1);

上面的例子輸出結果:

com.flydean.ReduceUsage - Optional[6]

一個參數的例子很簡單。這裡不再多說。

接下來我們再看一下兩個參數的例子:

T reduce(T identity, BinaryOperator<T> accumulator);

這個方法接收兩個參數:identity和accumulator。多出了一個參數identity。

也許在有些文章裡面有人告訴你identity是reduce的初始化值,可以隨便指定,如下所示:

Integer result2=intList.stream().reduce(100, Integer::sum);
        log.info("{}",result2);

上面的例子,我們計算的值是106。

如果我們將stream改成parallelStream:

Integer result3=intList.parallelStream().reduce(100, Integer::sum);
        log.info("{}",result3);

得出的結果就是306。

為什麼是306呢?因為在並行計算的時候,每個執行緒的初始累加值都是100,最後3個執行緒加出來的結果就是306。

並行計算和非並行計算的結果居然不一樣,這肯定不是JDK的問題,我們再看一下JDK中對identity的說明:

identity必須是accumulator函數的一個identity,也就是說必須滿足:對於所有的t,都必須滿足 accumulator.apply(identity, t) == t

所以這裡我們傳入100是不對的,因為sum(100+1)!= 1。

這裡sum方法的identity只能是0。

如果我們用0作為identity,則stream和parallelStream計算出的結果是一樣的。這就是identity的真正意圖。

下面再看一下三個參數的方法:

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

和前面的方法不同的是,多了一個combiner,這個combiner用來合併多執行緒計算的結果。

同樣的,identity需要滿足combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

大家可能注意到了為什麼accumulator的類型是BiFunction而combiner的類型是BinaryOperator?

public interface BinaryOperator<T> extends BiFunction<T,T,T>

BinaryOperator是BiFunction的子介面。BiFunction中定義了要實現的apply方法。

其實reduce底層方法的實現只用到了apply方法,並沒有用到介面中其他的方法,所以我猜測這裡的不同只是為了簡單的區分。

雖然reduce是一個很常用的方法,但是大家一定要遵循identity的規範,並不是所有的identity都是合適的。

12. stream中的Spliterator

Spliterator是在java 8引入的一個介面,它通常和stream一起使用,用來遍歷和分割序列。

只要用到stream的地方都需要Spliterator,比如List,Collection,IO channel等等。

我們先看一下Collection中stream方法的定義:

default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }

我們可以看到,不管是並行stream還是非並行stream,都是通過StreamSupport來構造的,並且都需要傳入一個spliterator的參數。

好了,我們知道了spliterator是做什麼的之後,看一下它的具體結構:

spliterator有四個必須實現的方法,我們接下來進行詳細的講解。

12.1 tryAdvance

tryAdvance就是對stream中的元素進行處理的方法,如果元素存在,則對他進行處理,並返回true,否則返回false。

如果我們不想處理stream後續的元素,則在tryAdvance中返回false即可,利用這個特徵,我們可以中斷stream的處理。這個例子我將會在後面的文章中講到。

12.2 trySplit

trySplit嘗試對現有的stream進行分拆,一般用在parallelStream的情況,因為在並發stream下,我們需要用多執行緒去處理stream的不同元素,trySplit就是對stream中元素進行分拆處理的方法。

理想情況下trySplit應該將stream拆分成數目相同的兩部分才能最大提升性能。

12.3 estimateSize

estimateSize表示Spliterator中待處理的元素,在trySplit之前和之後一般是不同的,後面我們會在具體的例子中說明。

12.4 characteristics

characteristics表示這個Spliterator的特徵,Spliterator有8大特徵:

public static final int ORDERED    = 0x00000010;//表示元素是有序的(每一次遍歷結果相同)
public static final int DISTINCT   = 0x00000001;//表示元素不重複
public static final int SORTED     = 0x00000004;//表示元素是按一定規律進行排列(有指定比較器)
public static final int SIZED      = 0x00000040;//
表示大小是固定的
public static final int NONNULL    = 0x00000100;//表示沒有null元素
public static final int IMMUTABLE  = 0x00000400;//表示元素不可變
public static final int CONCURRENT = 0x00001000;//表示迭代器可以多執行緒操作
public static final int SUBSIZED   = 0x00004000;//表示子Spliterators都具有SIZED特性

一個Spliterator可以有多個特徵,多個特徵進行or運算,最後得到最終的characteristics。

12.5 舉個例子

上面我們討論了Spliterator一些關鍵方法,現在我們舉一個具體的例子:

@AllArgsConstructor
@Data
public class CustBook {
    private String name;

}

先定義一個CustBook類,裡面放一個name變數。

定義一個方法,來生成一個CustBook的list:

    public static List<CustBook> generateElements() {
        return Stream.generate(() -> new CustBook("cust book"))
                .limit(1000)
                .collect(Collectors.toList());
    }

我們定義一個call方法,在call方法中調用了tryAdvance方法,傳入了我們自定義的處理方法。這裡我們修改book的name,並附加額外的資訊。

    public String call(Spliterator<CustBook> spliterator) {
        int current = 0;
        while (spliterator.tryAdvance(a -> a.setName("test name"
                .concat("- add new name")))) {
            current++;
        }

        return Thread.currentThread().getName() + ":" + current;
    }

最後,寫一下測試方法:

    @Test
    public void useTrySplit(){
        Spliterator<CustBook> split1 = SpliteratorUsage.generateElements().spliterator();
        Spliterator<CustBook> split2 = split1.trySplit();

        log.info("before tryAdvance: {}",split1.estimateSize());
        log.info("Characteristics {}",split1.characteristics());
        log.info(call(split1));
        log.info(call(split2));
        log.info("after tryAdvance {}",split1.estimateSize());
    }

運行的結果如下:

23:10:08.852 [main] INFO com.flydean.SpliteratorUsage - before tryAdvance: 500
23:10:08.857 [main] INFO com.flydean.SpliteratorUsage - Characteristics 16464
23:10:08.858 [main] INFO com.flydean.SpliteratorUsage - main:500
23:10:08.858 [main] INFO com.flydean.SpliteratorUsage - main:500
23:10:08.858 [main] INFO com.flydean.SpliteratorUsage - after tryAdvance 0

List總共有1000條數據,調用一次trySplit之後,將List分成了兩部分,每部分500條數據。

注意,在tryAdvance調用之後,estimateSize變為0,表示所有的元素都已經被處理完畢。

再看一下這個Characteristics=16464,轉換為16進位:Ox4050 = ORDERED or SIZED or SUBSIZED 這三個的或運算。

這也是ArrayList的基本特徵。

13. break stream的foreach

我們通常需要在java stream中遍歷處理裡面的數據,其中foreach是最最常用的方法。

但是有時候我們並不想處理完所有的數據,或者有時候Stream可能非常的長,或者根本就是無限的。

一種方法是先filter出我們需要處理的數據,然後再foreach遍歷。

那麼我們如何直接break這個stream呢?今天本文重點講解一下這個問題。

13.1 使用Spliterator

上篇文章我們在講Spliterator的時候提到了,在tryAdvance方法中,如果返回false,則Spliterator將會停止處理後續的元素。

通過這個思路,我們可以創建自定義Spliterator。

假如我們有這樣一個stream:

Stream<Integer> ints = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

我們想定義一個操作,當x > 5的時候就停止。

我們定義一個通用的Spliterator:

public class CustomSpliterator<T> extends Spliterators.AbstractSpliterator<T>  {

    private Spliterator<T> splitr;
    private Predicate<T> predicate;
    private volatile boolean isMatched = true;

    public CustomSpliterator(Spliterator<T> splitr, Predicate<T> predicate) {
        super(splitr.estimateSize(), 0);
        this.splitr = splitr;
        this.predicate = predicate;
    }

    @Override
    public synchronized boolean tryAdvance(Consumer<? super T> consumer) {
        boolean hadNext = splitr.tryAdvance(elem -> {
            if (predicate.test(elem) && isMatched) {
                consumer.accept(elem);
            } else {
                isMatched = false;
            }
        });
        return hadNext && isMatched;
    }
}

在上面的類中,predicate是我們將要傳入的判斷條件,我們重寫了tryAdvance,通過將predicate.test(elem)加入判斷條件,從而當條件不滿足的時候返回false.

看下怎麼使用:

@Slf4j
public class CustomSpliteratorUsage {

    public static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<T> predicate) {
        CustomSpliterator<T> customSpliterator = new CustomSpliterator<>(stream.spliterator(), predicate);
        return StreamSupport.stream(customSpliterator, false);
    }

    public static void main(String[] args) {
        Stream<Integer> ints = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> result =
          takeWhile(ints, x -> x < 5 )
                        .collect(Collectors.toList());
        log.info(result.toString());
    }
}

我們定義了一個takeWhile方法,接收Stream和predicate條件。

只有當predicate條件滿足的時候才會繼續,我們看下輸出的結果:

[main] INFO com.flydean.CustomSpliteratorUsage - [1, 2, 3, 4]

13.2 自定義forEach方法

除了使用Spliterator,我們還可以自定義forEach方法來使用自己的遍歷邏輯:

public class CustomForEach {

    public static class Breaker {
        private volatile boolean shouldBreak = false;

        public void stop() {
            shouldBreak = true;
        }

        boolean get() {
            return shouldBreak;
        }
    }

    public static <T> void forEach(Stream<T> stream, BiConsumer<T, Breaker> consumer) {
        Spliterator<T> spliterator = stream.spliterator();
        boolean hadNext = true;
        Breaker breaker = new Breaker();

        while (hadNext && !breaker.get()) {
            hadNext = spliterator.tryAdvance(elem -> {
                consumer.accept(elem, breaker);
            });
        }
    }
}

上面的例子中,我們在forEach中引入了一個外部變數,通過判斷這個外部變數來決定是否進入spliterator.tryAdvance方法。

看下怎麼使用:

@Slf4j
public class CustomForEachUsage {

    public static void main(String[] args) {
        Stream<Integer> ints = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> result = new ArrayList<>();
        CustomForEach.forEach(ints, (elem, breaker) -> {
            if (elem >= 5 ) {
                breaker.stop();
            } else {
                result.add(elem);
            }
        });
        log.info(result.toString());
    }
}

上面我們用新的forEach方法,並通過判斷條件來重置判斷flag,從而達到break stream的目的。

14. predicate chain的使用

Predicate是一個FunctionalInterface,代表的方法需要輸入一個參數,返回boolean類型。通常用在stream的filter中,表示是否滿足過濾條件。

    boolean test(T t);

14.1 基本使用

我們先看下在stream的filter中怎麼使用Predicate:

    @Test
    public void basicUsage(){
        List<String> stringList=Stream.of("a","b","c","d").filter(s -> s.startsWith("a")).collect(Collectors.toList());
        log.info("{}",stringList);
    }

上面的例子很基礎了,這裡就不多講了。

14.2 使用多個Filter

如果我們有多個Predicate條件,則可以使用多個filter來進行過濾:

    public void multipleFilters(){
        List<String> stringList=Stream.of("a","ab","aac","ad").filter(s -> s.startsWith("a"))
                .filter(s -> s.length()>1)
                .collect(Collectors.toList());
        log.info("{}",stringList);
    }

上面的例子中,我們又添加了一個filter,在filter又添加了一個Predicate。

14.3 使用複合Predicate

Predicate的定義是輸入一個參數,返回boolean值,那麼如果有多個測試條件,我們可以將其合併成一個test方法:

    @Test
    public void complexPredicate(){
        List<String> stringList=Stream.of("a","ab","aac","ad")
                .filter(s -> s.startsWith("a") &&  s.length()>1)
                .collect(Collectors.toList());
        log.info("{}",stringList);
    }

上面的例子中,我們把s.startsWith(“a”) && s.length()>1 作為test的實現。

14.4 組合Predicate

Predicate雖然是一個interface,但是它有幾個默認的方法可以用來實現Predicate之間的組合操作。

比如:Predicate.and(), Predicate.or(), 和 Predicate.negate()。

下面看下他們的例子:

@Test
    public void combiningPredicate(){
        Predicate<String> predicate1 = s -> s.startsWith("a");
        Predicate<String> predicate2 =  s -> s.length() > 1;
        List<String> stringList1 = Stream.of("a","ab","aac","ad")
                .filter(predicate1.and(predicate2))
                .collect(Collectors.toList());
        log.info("{}",stringList1);

        List<String> stringList2 = Stream.of("a","ab","aac","ad")
                .filter(predicate1.or(predicate2))
                .collect(Collectors.toList());
        log.info("{}",stringList2);

        List<String> stringList3 = Stream.of("a","ab","aac","ad")
                .filter(predicate1.or(predicate2.negate()))
                .collect(Collectors.toList());
        log.info("{}",stringList3);

    }

實際上,我們並不需要顯示的assign一個predicate,只要是滿足
predicate介面的lambda表達式都可以看做是一個predicate。同樣可以調用and,or和negate操作:

List<String> stringList4 = Stream.of("a","ab","aac","ad")
                .filter(((Predicate<String>)a -> a.startsWith("a"))
                        .and(a -> a.length() > 1))
                .collect(Collectors.toList());
        log.info("{}",stringList4);

14.5 Predicate的集合操作

如果我們有一個Predicate集合,我們可以使用reduce方法來對其進行合併運算:

@Test
    public void combiningPredicateCollection(){
        List<Predicate<String>> allPredicates = new ArrayList<>();
        allPredicates.add(a -> a.startsWith("a"));
        allPredicates.add(a -> a.length() > 1);

        List<String> stringList = Stream.of("a","ab","aac","ad")
                .filter(allPredicates.stream().reduce(x->true, Predicate::and))
                .collect(Collectors.toList());
        log.info("{}",stringList);
    }

上面的例子中,我們調用reduce方法,對集合中的Predicate進行了and操作。

15. 中構建無限的stream

在java中,我們可以將特定的集合轉換成為stream,那麼在有些情況下,比如測試環境中,我們需要構造一定數量元素的stream,需要怎麼處理呢?

這裡我們可以構建一個無限的stream,然後調用limit方法來限定返回的數目。

15.1 基本使用

先看一個使用Stream.iterate來創建無限Stream的例子:

    @Test
    public void infiniteStream(){
        Stream<Integer> infiniteStream = Stream.iterate(0, i -> i + 1);
        List<Integer> collect = infiniteStream
                .limit(10)
                .collect(Collectors.toList());
        log.info("{}",collect);
    }

上面的例子中,我們通過調用Stream.iterate方法,創建了一個0,1,2,3,4….的無限stream。

然後調用limit(10)來獲取其中的前10個。最後調用collect方法將其轉換成為一個集合。

看下輸出結果:

INFO com.flydean.InfiniteStreamUsage - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

15.2 自定義類型

如果我們想輸出自定義類型的集合,該怎麼處理呢?

首先,我們定義一個自定義類型:

@Data
@AllArgsConstructor
public class IntegerWrapper {
    private Integer integer;
}

然後利用Stream.generate的生成器來創建這個自定義類型:

    public static IntegerWrapper generateCustType(){
        return new IntegerWrapper(new Random().nextInt(100));
    }

    @Test
    public void infiniteCustType(){
        Supplier<IntegerWrapper> randomCustTypeSupplier = InfiniteStreamUsage::generateCustType;
        Stream<IntegerWrapper> infiniteStreamOfCustType = Stream.generate(randomCustTypeSupplier);

        List<IntegerWrapper> collect = infiniteStreamOfCustType
                .skip(10)
                .limit(10)
                .collect(Collectors.toList());
        log.info("{}",collect);
    }

看下輸出結果:

INFO com.flydean.InfiniteStreamUsage - [IntegerWrapper(integer=46), IntegerWrapper(integer=42), IntegerWrapper(integer=67), IntegerWrapper(integer=11), IntegerWrapper(integer=14), IntegerWrapper(integer=80), IntegerWrapper(integer=15), IntegerWrapper(integer=19), IntegerWrapper(integer=72), IntegerWrapper(integer=41)]

16. 自定義parallelStream的thread pool

之前我們講到parallelStream的底層使用到了ForkJoinPool來提交任務的,默認情況下ForkJoinPool為每一個處理器創建一個執行緒,parallelStream如果沒有特別指明的情況下,都會使用這個共享執行緒池來提交任務。

那麼在特定的情況下,我們想使用自定義的ForkJoinPool該怎麼處理呢?

16.1 通常操作

假如我們想做一個從1到1000的加法,我們可以用並行stream這樣做:

List<Integer> integerList= IntStream.range(1,1000).boxed().collect(Collectors.toList());
        ForkJoinPool customThreadPool = new ForkJoinPool(4);

        Integer total= integerList.parallelStream().reduce(0, Integer::sum);
        log.info("{}",total);

輸出結果:

INFO com.flydean.CustThreadPool - 499500

16.2 使用自定義ForkJoinPool

上面的例子使用的共享的thread pool。 我們看下怎麼使用自定義的thread pool來提交並行stream:

List<Integer> integerList= IntStream.range(1,1000).boxed().collect(Collectors.toList());

ForkJoinPool customThreadPool = new ForkJoinPool(4);
        Integer actualTotal = customThreadPool.submit(
                () -> integerList.parallelStream().reduce(0, Integer::sum)).get();
        log.info("{}",actualTotal);

上面的例子中,我們定義了一個4個執行緒的ForkJoinPool,並使用它來提交了這個parallelStream。

輸出結果:

INFO com.flydean.CustThreadPool - 499500

如果不想使用公共的執行緒池,則可以使用自定義的ForkJoinPool來提交。

17. 總結

本文統一介紹了Stream和lambda表達式的使用,涵蓋了Stream和lambda表達式的各個小的細節,希望大家能夠喜歡。

本文的程式碼//github.com/ddean2009/learn-java-streams/

本文的PDFjava-stream-lambda-all-in-one.pdf

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!