5萬字長文:Stream和Lambda表達式最佳實踐-附PDF下載
- 1. Streams簡介
- 2. functional interface的分類和使用
- 3. Lambda表達式最佳實踐
- 4. stream表達式中實現if/else邏輯
- 5. 在map中使用stream
- 6. Stream中的操作類型和peek的使用
- 7. lambda表達式中的異常處理
- 8. stream中throw Exception
- 9. stream中Collectors的用法
- 9.1 Collectors.toList()
- 9.2 Collectors.toSet()
- 9.3 Collectors.toCollection()
- 9.4 Collectors.toMap()
- 9.5 Collectors.collectingAndThen()
- 9.6 Collectors.joining()
- 9.7 Collectors.counting()
- 9.8 Collectors.summarizingDouble/Long/Int()
- 9.9 Collectors.averagingDouble/Long/Int()
- 9.10 Collectors.summingDouble/Long/Int()
- 9.11 Collectors.maxBy()/minBy()
- 9.12 Collectors.groupingBy()
- 9.13 Collectors.partitioningBy()
- 10. 創建一個自定義的collector
- 11. stream reduce詳解和誤區
- 12. stream中的Spliterator
- 13. break stream的foreach
- 14. predicate chain的使用
- 15. 中構建無限的stream
- 16. 自定義parallelStream的thread pool
- 17. 總結
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
//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的類型推斷大家應該都知道,如果是
我們看下例子:
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
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!