一、概述
Stream流操作是Java為了簡化資料操作推出的新特性。雖然被稱為Stream流,但是它和IO流是完全不同的概念。Stream流更多的是為了增強集合操作,在資料量較大時,我們可以通過Stream流直接在記憶體中操作資料。通過使用流,我們可以說明想要完成什麼任務,而不是說明如何去實作它。
例如,我們想要統計某個整數集合中所有偶數的個數,傳統方式會使用疊代的方式:
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6);
int count = 0;
for (Integer integer : integerList) {
if (integer % 2 == 0){
count++;
}
}
而使用流式操作,隻需指明要做的事情而無需關心實作。
long count1 = integerList.stream().filter(t -> t % 2 == 0).count();
其中
integerList.stream()
用來建立流,
filter()
是中間操作,
count()
用來做終止流的操作,這裡是統計作用。
流和集合是不同的概念,雖然看起來可能有些類似,但是它們有很多不同的地方:
- 流并不存儲元素。這些元素可能存儲在底層的集合中,或者按需生成。
- 流的操作不會修改其資料源。例如filter方法不會從新的流中移除元素,而是會生成一個新的流,其中包含被過濾掉的元素。
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6); //流操作 long count1 = integerList.stream().filter(t -> t % 2 == 0).count(); //結果依然是[1,2,3,4,5,6] System.out.println(Arrays.toString(integerList.toArray())); //結果為3 System.out.println(count1);
- 流的操作是盡可能惰性執行的。這意味着直至需要其結果時,操作才會執行。例如,如果我們隻想找前5個偶數,那麼filter方法就會在比對到第五個偶數後停止過濾。同時,我們還可以建立無限流。
二、流的建立方式
1.使用Collection接口的stream方法
Collection<Integer> collection = Arrays.asList(1,2,3,4);
Stream<Integer> stream = collection.stream();
2. 使用Stream的靜态方法of
of方法具有可變長參數,是以我們可以建構具有任意資料引元的流:
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<String> stringStream = Stream.of("1","2","3");
3. 使用Arrays中的stream方法
Stream<String> stream1 = Arrays.stream(new String[]{"1", "2", "3"}, 0, 3);
上述建立流的方式表示,從一個數組中截取從0到3的元素來建立流。
Stream<String> stream1 = Arrays.stream(new String[]{"1", "2", "3"});
4. 建立不包含任何元素的流
Stream<Object> empty = Stream.empty();
5. 建立無限序列
Stream流有兩種建立無限序列的方式,一種是通過
generate
方法,另一種是通過
iterate
方法。
generate
方法接受一個實作了
Supplier
接口的對象,
Supplier
接口時供給型函數式接口,不需要傳入參數,主要是通過反複調用
Supplier
中的方法來實作
Stream.generate(() -> "1").forEach(System.out::println);
//産生一個無限随機數序列
Stream.generate(Math::random).forEach(System.out::println);
三、 操作流
1. filter
filter轉換會産生一個流,它的元素與某種條件相比對。例如我們可以從一個整數集合中找出所有的偶數。
Stream<Integer> stream = integerList.stream().filter(t -> t%2 ==0);
filter的參數是一個實作了
Predicate
Predicate
接口是判定型接口,可以對傳遞的參數進行判斷,傳回值為Boolean。
2. map
map可以對參數進行映射,比如說我們希望将字元串集合中所有的小寫字母映射(轉換為大寫字母),就可以使用map
List<String> stringList = Arrays.asList("a","b","c");
stringList.stream().map(String::toUpperCase).forEach(System.out::println);
它的底層使用了
Function<T,R>
函數式接口來實作。
3. flatMap
應用場景分析
public void fun10(){
List<String> stringList = Arrays.asList("abc","bcd","cde");
Stream<Stream<String>> streamStream = stringList.stream().map(this::letters);
Stream<String> stringStream = stringList.stream().flatMap(this::letters);
}
private Stream<String> letters(String s){
List<String> result = new ArrayList<>();
for (int i = 0; i < s.length() ; i++) {
result.add(s.substring(i,i+1));
}
return result.stream();
}
如上所示,如果映射結果是Stream流,那麼我們可以直接使用flatMap方法,這樣它底層會幫助我們去轉換Stream,進而更容易去使用。
4. limit
調用
stream.limit(n)
會傳回一個新的流,它在n個元素之後結束(如果原來的流更短,那麼就會在流結束時結束)
stringList.stream().flatMap(this::letters).limit(3);
我們可以使用這個方法對無限流進行裁剪。
Stream.generate(() -> "1").limit(10).forEach(System.out::println);
5. skip
skip
方法會丢棄前n個元素
6. concat
concat
方法可以将兩個流連接配接在一起
7. distinct
用來去重,調用
distinct
方法可以将流中的重複元素剔除
8. sorted
用來對流中的元素進行排序,它有兩種實作方式,一種是對實作
Comparable
接口的元素直接進行排序,另一種是通過
Compparator
接口自定義排序規則。
stringList.stream().sorted().forEach(System.out::println);
//自定義排序規則
stringList.stream().sorted(String::compareTo).forEach(System.out::println);
四、終止流
流在開啟之後必須關閉,但是
Stream
流的關閉方式不是通過
close
方法而是通過終止操作來完成的,一般的終止操作如下
1. count
用來統計流中元素的個數
2. max、min
用來擷取流中元素的最大值或最小值
3. findFirst
傳回非空集合中的第一個元素。
4. findAny
查找任意比對,這個方法在使用并行流進行處理時比較合适。
5. anyMatch
這個方法接受一個判定型函數式接口,不需要搭配filter使用。
上述操作傳回值基本都是類型,
Optional<T>
類型提供了規避空指針異常的有效方式,使用得當的情況下更安全。
Optional<T>
五、Optional類型
Optional<T>
對象是一種包裝器對象,要麼包裝了類型T的對象,要麼沒有包裝任何對象。對于第一種情況,我們稱為值存在。
Optional<T>
類型被當作一種更安全的方式,用來替代類型
T
的引用,這種引用要麼引用某個對象,要麼為null。但是,它隻有在正确使用的情況下才更安全。
1. 使用Optional值
有效的使用
Optional
的關鍵是要使用這樣的方法:它在值不存在的情況下會産生一個可替代物,而如果值存在則使用該值。
首先,我們看第一條政策。通常,在沒有任何比對時,我們會希望使用某種預設值,可能是空字元串或者設定的其它值:
//構造一個空的集合
List<String> stringList = Arrays.asList();
//擷取流并進行排序操作,最後查找第一個元素
Optional<String> first = stringList.stream().sorted().findFirst();
//如果值不存在,則預設為"hello"
String s = first.orElse("hello");
//因為沒有找到結果,是以輸出hello,如果結果不為空,則輸出結果的值
System.out.println(s);
你和可以調用代碼來設定預設值
first.orElseGet(() -> Locale.getDefault().getDisplayName());
或者可以在沒有任何值的時候抛出異常
String s1 = first.orElseThrow(RuntimeException::new);
另一條使用可選值的政策是隻有在其存在的情況下才消費該值。
ifPresent
方法會接受一個函數。如果該可選值存在,那麼它會被傳遞到該函數。否則不會發生任何事。
first.ifPresent(stringList::add );
六、收集結果
針對将流中的元素收集到另一個目标中,有一個便捷的方法
collect
可以使用,它會接受一個
Collector
接口的執行個體。
Collector
類提供了大量用于生成公共收集器的工廠方法。為了将流收集到清單或者集中,可以直接調用
1. 收集到集合中
stringList.stream().sorted().collect(Collectors.toList());
或者
stringList.stream().sorted().collect(Collectors.toSet());
如果想要控制集合的類型,例如需要将其收集到
TreeSet
集合中,那麼可以進行如下操作:
stringList.stream().collect(Collectors.toCollection(TreeSet::new));
2. 通過連接配接操作收集結果
//不指定分隔符
stringList.stream().collect(Collectors.joining());
//指定分隔符
stringList.stream().collect(Collectors.joining(","));
3. 将結果約簡為總和、平均值、最大最小值
IntSummaryStatistics collect = stringList.stream().collect(Collectors.summarizingInt(String::length));
collect.getAverage();
collect.getCount();
collect.getMax();
collect.getMin();
collect.getSum();
4. 收集到映射表中
假設我們有一個
Stream<Person>
,并且希望将其元素收集到一個映射表中,這樣後續就可以通過它們的ID來查找人員了。
Collectors.toMap
方法有兩個函數引元,它們用來産生映射表的鍵和值。
list.stream().collect(Collectors.toMap(Person::getId,Function.identity()));
Function.identity()
方法擷取到的是目前值。
如果有多個元素具有相同的鍵,那麼就會存在沖突,收集器将會抛出一個異常。可以使用三個參數的方法來規避這一問題。
toMap
第三個參數,映射過程中覆寫值的規則,第一個值表示的是舊值,第二個表示的是新值list.stream().collect(Collectors.toMap(Person::getId,Function.identity(),(oldValue,newValue)-> newValue));
注:如果希望使用并行的方式處理映射,可以使用
toConcurrentMap
方法
5. 分組
假如說,我們有這樣的一個需求:我們有一個
List<Person>
集合,我們希望将它按照年齡分組,然後轉換為
Map<Integer,List<Person>>
的形式。在上一節中,我們提到可以使用
toMap
方法來将
List
轉換為
Map
,但是它并不适用于這種複雜場景。
5.1 groupingBy
Map<String, List<Person>> collect = list.stream().collect(Collectors.groupingBy(Person::getId));
5.2 partitioningBy
該函數傳回
true
的元素和其它的元素。在這種情況下,使用
partitioningBy
比使用
groupingBy
更高效。
5.3 注意
和
toConcurrentMap
方法類似,我們可以使用
groupingByConcurrent
方法以并行流的方式處理映射。
5.4 下遊收集器
我們使用
groupingBy
方法可以實作分組,但是預設會生成
Map<T,List<U>>
格式的資料。有時候,我們可能并不需要這種形式的資料,我們需要擷取分組後每組的數量,或者我們希望生成的集合是
Set<U>
而不是
List<U>
。對于這些需求,我們可以使用下遊收集器來完成。
Map<String, Set<Person>> collect1 = list.stream().collect(Collectors.groupingBy(Person::getId, Collectors.toSet()));
Map<String, Long> collect2 = list.stream().collect(Collectors.groupingBy(Person::getId, Collectors.counting()));
從上圖可以看出,我們可以為
collect
方法傳遞兩個參數,第二個參數就可以作為下遊收集器,可以簡單的了解下遊收集器可以按照我們的想法傳回指定的資料格式。
七、基本類型流
Java8的流相關庫中具有專門的類型
IntStream
、
LongStrem
DoubleStream
,用來直接存儲基本類型值,而無需使用包裝器。如果想要存儲
short
boolean
char
byte
,可以使用
IntStream
,而對于
float
DoubleStream
。
我們可以使用
IntStream.of()
Arrays.stream(values,form,to)
來建立基本類型流。同時我們可以通過
mapToInt
來将對象流轉換為基本流,或者通過
boxed()
方法将基本流轉換為對象流。
基本流的使用方式和對象流基本上是一樣的。
八、并行流
Collection.parallelStream()
來獲得一個并行流。或者可以使用
parallel()
将任意流轉換為并行流:
Stream.of(Array).parallel()
九、實際案例應用
1. 計算集合中元素的和
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
//Integer reduce = list.parallelStream().reduce(0, Integer::sum);
Optional<Integer> reduce1 = list.parallelStream().reduce(Integer::sum);
reduce1.ifPresent(System.out::println);
reduce1.orElseThrow(Throwable::new);
//System.out.println(reduce);
2. 周遊某個檔案夾下所有檔案的名字
//使用Lambda表達式
List<String> files = Stream.of(Objects.requireNonNull(new File("C:\\").
listFiles())).
filter(File::isDirectory).
map(File::getName).
collect(Collectors.toList());
//使用傳統的匿名内部類格式
List<Object> collect = Stream.of(Objects.requireNonNull(new File("C:\\").
listFiles())).filter(new Predicate<File>() {
@Override
public boolean test(File file) {
return file.isDirectory();
}
}).map(new Function<File, Object>() {
@Override
public Object apply(File file) {
return file.getName();
}
}).collect(Collectors.toList());
files.forEach(System.out::println);