Stream API
本文繼續介紹Java 8的另一個新特性——Stream API。Stream API是對Java中集合操作的增強,可以利用它進行各種過濾、排序、分組、聚合等操作。Stream API配合Lambda表達式可以加大的提高代碼可讀性和編碼效率,Stream API也支持並行操作,我們不用再花費很多精力來編寫容易出錯的多線程代碼了,Stream API已經替我們做好了,並且充分利用多核CPU的優勢。藉助Stream API和Lambda,開發人員可以很容易的編寫出高性能的併發處理程序。
簡介
Stream API是Java 8中加入的一套新的API,主要用於處理集合操作,不過它的處理方式與傳統的方式不同,稱爲“數據流處理”。流(Stream)類似於關係數據庫的查詢操作,是一種聲明式操作。比如要從數據庫中獲取所有年齡大於20歲的用戶的名稱,並按照用戶的創建時間進行排序,用一條SQL語句就可以搞定,不過使用Java程序實現就會顯得有些繁瑣,這時候可以使用流:
List<String> userNames =
users.stream()
.filter(user -> user.getAge() > 20)
.sorted(comparing(User::getCreationDate))
.map(User::getUserName)
.collect(toList());
在這個大數據的時代,數據變得越來越多樣化,很多時候我們會面對海量數據,並對其做一些複雜的操作(比如統計,分組),依照傳統的遍歷方式(for-each)
,每次只能處理集合中的一個元素,並且是按順序處理,這種方法是極其低效的。你也許會想到並行處理,但是編寫多線程代碼並非易事,很容易出錯並且維護困難。不過在Java 8之後,你可以使用Stream API來解決這一問題。
可以看到Stream API
裏的參數大多都是函數式接口的各種形態。
還不知道函數式接口的同學快來看看這兩篇
Stream API將迭代操作封裝到了內部,它會自動的選擇最優的迭代方式,並且使用並行方式處理時,將集合分成多段,每一段分別使用不同的線程處理,最後將處理結果合併輸出。
需要注意的是,流只能遍歷一次,遍歷結束後,這個流就被關閉掉了。如果要重新遍歷,可以從數據源(集合)中重新獲取一個流。如果你對一個流遍歷兩次,就會拋出java.lang.IllegalStateException異常
:
List<String> list = Arrays.asList("A", "B", "C", "D");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 這裏會拋出java.lang.IllegalStateException異常,因爲流已經被關閉
流通常由三部分構成:
- 數據源:數據源一般用於流的獲取,比如本文開頭那個過濾用戶的例子中users.stream()方法。
- 中間處理:中間處理包括對流中元素的一系列處理,如:過濾(filter()),映射(map()),排序(sorted())。
- 終端處理:終端處理會生成結果,結果可以是任何不是流值,如List;也可以不返回結果,如stream.forEach(System.out::println)就是將結果打印到控制檯中,並沒有返回。
中間操作也稱爲惰性操作。
終端操作也稱爲急切操作。
惰性操作不處理元素,直到在流上調用熱切操作。
流上的中間操作產生另一流。
Streams鏈接操作以創建流管道。
創建流
創建流的方式有很多,具體可以劃分爲以下幾種:
由值創建流
使用靜態方法Stream.of()
創建流,該方法接收一個變長參數:
Stream<Stream> stream = Stream.of("A", "B", "C", "D");
也可以使用靜態方法Stream.empty()
創建一個空的流:
Stream<Stream> stream = Stream.empty();
由數組創建流
使用靜態方法Arrays.stream()
從數組創建一個流,該方法接收一個數組參數:
String[] strs = {"A", "B", "C", "D"};
Stream<Stream> stream = Arrays.stream(strs);
通過文件生成流
使用java.nio.file.Files
類中的很多靜態方法都可以獲取流,比如Files.lines()
方法,該方法接收一個java.nio.file.Path
對象,返回一個由文件行構成的字符串流:
Stream<String> stream = Files.lines(Paths.get("text.txt"), Charset.defaultCharset());
通過函數創建流
java.util.stream.Stream
中有兩個靜態方法用於從函數生成流,他們分別是Stream<T> generate(Supplier<T> s)
和Stream<T> iterate(final T seed, final UnaryOperator<T> f)
:
// iteartor
Stream.iterate(0, n -> n + 2).limit(51).forEach(System.out::println);
// generate
Stream.generate(() -> "Hello Man!").limit(10).forEach(System.out::println);
第一個方法會打印100以內的所有偶數,第二個方法打印10個Hello Man!
。值得注意的是,這兩個方法生成的流都是無限流,沒有固定大小,可以無窮的計算下去,在上面的代碼中我們使用了limit()
來避免打印無窮個值。
一般來說,iterate()
用於生成一系列值,比如生成以當前時間開始之後的10天的日期:
Stream.iterate(LocalDate.now(), date -> date.plusDays(1)).limit(10).forEach(System.out::println);
generate()
方法用於生成一些隨機數,比如生成10個UUID:
Stream.generate(() -> UUID.randomUUID().toString()).limit(10).forEach(System.out::println);
使用流
Stream
接口中包含許多對流操作的方法,這些方法分別爲:
filter()
:對流的元素過濾map()
:將流的元素映射成另一個類型distinct()
:去除流中重複的元素sorted()
:對流的元素排序forEach()
:對流中的每個元素執行某個操作peek()
:與forEach()
方法效果類似,不同的是,該方法會返回一個新的流,而forEach()
無返回limit()
:截取流中前面幾個元素skip()
:跳過流中前面幾個元素toArray()
:將流轉換爲數組reduce()
:對流中的元素歸約操作,將每個元素合起來形成一個新的值collect()
:對流的彙總操作,比如輸出成List
集合anyMatch()
:匹配流中的元素,類似的操作還有allMatch()
和noneMatch()
方法findFirst()
:查找第一個元素,類似的還有findAny()
方法max()
:求最大值min()
:求最小值count()
:求總數
下面逐一介紹這些方法的用法。
簡單栗子:
Stream.of(1, 8, 5, 2, 1, 0, 9, 2, 0, 4, 8)
.filter(n -> n > 2) // 對元素過濾,保留大於2的元素
.distinct() // 去重,類似於SQL語句中的DISTINCT
.skip(1) // 跳過前面1個元素
.limit(2) // 返回開頭2個元素,類似於SQL語句中的SELECT TOP
.sorted() // 對結果排序
.forEach(System.out::println);
過濾
List<Apple> filterList = appleList.stream().filter(a -> a.getName().equals("香蕉")).collect(Collectors.toList());
求和(歸約)
歸約操作就是將流中的元素進行合併,形成一個新的值,常見的歸約操作包括求和,求最大值或最小值。歸約操作一般使用reduce()
方法,與map()
方法搭配使用,可以處理一些很複雜的歸約操作。)
//計算 總金額
// map -> reduce
BigDecimal totalMoney = appleList.stream().map(Apple::getMoney).reduce(BigDecimal.ZERO, BigDecimal::add);
//計算 數量
int sum = appleList.stream().mapToInt(Apple::getNum).sum();
提取Bean某一屬性
// 取Bean某一屬性
stuList.stream()
.map(Student::getId).distinct()
.collect(Collectors.toList());
去重
List<Integer> distinctNumbers = numbers.stream().distinct().collect(Collectors.toList());
// 根據Bean的某種屬性去重
// 首先定義一個過濾器
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> seen = new ConcurrentHashMap<>();
return object -> seen.putIfAbsent(keyExtractor.apply(object), Boolean.TRUE) == null;
}
List<User> distinctUsers = users.stream()
.filter(distinctByKey(User::getName))
.collect(Collectors.toList());
這種去重也可以對多個Key進行,將多個Key拼接成一個Key。
private String getGroupingByKey(Person p){
return p.getAge()+"-"+p.getName();
}
下面的分組也是用了這個思想。
// 或者
List<User> unique = list.stream()
.collect(Collectors
.collectingAndThen(Collectors
.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getName()))), ArrayList::new));
// 最終版
list.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparing(o -> o.getAge() + ";" + o.getName()))), ArrayList::new)).forEach(u -> println(u));
排序
排序需重新賦值,內部操作循環的foreach的話就不用賦值新變量
list = list.stream().sorted(byNumber).collect(Collectors.toList());
多屬性先後順序排序:
自己重寫comparator方法
@Test
public void testSort_with_multipleComparator() throws Exception {
ArrayList<Human> humans = Lists.newArrayList(
new Human("tomy", 22),
new Human("li", 25)
);
Comparator<Human> comparator = (h1, h2) -> {
if (h1.getName().equals(h2.getName())) {
return Integer.compare(h1.getAge(), h2.getAge());
}
return h1.getName().compareTo(h2.getName());
};
humans.sort(comparator.reversed());
Assert.assertThat("tomy", equalTo(humans.get(0).getName()));
}
// 一般排序
// 定義一個比較器,用於排序
Comparator<Rule> byNumber = Comparator.comparingInt(Rule::getNumber);
// 獲取排序後的list,先通過filter篩選,然後在排序
List<Rule> rule = lstRule.stream().filter(s -> s.getCode() == 2).sorted(byNumber).collect(Collectors.toList());
// 聯合排序
Comparator<Rule> byNumber = Comparator.comparingInt(Rule::getNumber);
Comparator<Rule> byCode = Comparator.comparingInt(Rule::getCode);
Comparator<Rule> byNumberAndCode = byNumber.thenComparing(byCode);
// byNumberAndCode是一個聯合排序的比較器
List<Rule> rule = lstRule.stream().filter(s -> s.getCode() == 2).sorted(byNumberAndCode).collect(Collectors.toList());
// 排序時包括null
List<User> nList = list.stream()
.sorted(Comparator.comparing(User::getCode, Comparator.nullsFirst(String::compareTo)))
.collect(Collectors.toList());
List<User> list = minPriceList.stream()
.sorted(Comparator.comparing(l -> l.getCreateDate(), Comparator.nullsLast(Date::compareTo)))
.collect(Collectors.toList());
eg: 多字段排序例子,有個坑是後排序的字段需要先寫
List<Food> list = new ArrayList<>();
list.add(new Food(3, "aa", 2));
list.add(new Food(3, "bb", null));
list.add(new Food(2, "cc", 1));
list.add(new Food(2, "dd", 15));
List<Food> sortedList = list.stream()
.sorted(Comparator.comparing(Food::getPrice, Comparator.nullsLast(Integer::compareTo)).reversed())
.sorted(Comparator.comparing(Food::getId, Comparator.nullsFirst(Integer::compareTo)))
.collect(Collectors.toList());
分組
此方法類似Mysql的group by,但是可能比sql的複雜語句來得簡單些。
輔助POJO
static class Person {
private String name;
private int age;
private long salary;
Person(String name, int age, long salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, salary=%d}", name, age, salary);
}
}
// 一般分組後的結構是Map,key是分組的屬性,value是組成員
Map<Integer, List<Person>> peopleByAge = people.stream().collect(Collectors.groupingBy(Person::getAge));
Map<Integer, List<Person>> peopleByAge = people.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.toList()));
Map<Integer, List<Person>> peopleByAge = people.stream().collect(Collectors.groupingBy(p -> p.age, Collectors.mapping((Person p) -> p, toList())));
上面三種方法返回均相同,分組後的Map裏的value也可以根據Collectors.groupingBy方法的第二個參數設置不同,Collectors裏還有更多的方法,如求和、最值、均值、拼接。
方式一:
Map<String, Map<Integer, List<Person>>> map = people.stream()
.collect(Collectors.groupingBy(Person::getName,
Collectors.groupingBy(Person::getAge));
map.get("Fred").get(18);
方式二:
定義一個表示分組的類:
//靜態內部類
class Person {
public static class NameAge {
public NameAge(String name, int age) {
...
}
// 注意 重寫方法 must implement equals and hash function
}
public NameAge getNameAge() {
return new NameAge(name, age);
}
}
Map<NameAge, List<Person>> map = people.collect(Collectors.groupingBy(Person::getNameAge));
map.get(new NameAge("Fred", 18));
不定義分組類也可以使用如apache commons pair如果您使用這些庫之一。
Map<Pair<String, Integer>, List<Person>> map =
people.collect(Collectors.groupingBy(p -> Pair.of(p.getName(), p.getAge())));
map.get(Pair.of("Fred", 18));
最終方式:
將多個字段拼接成一個新字段,在使用Java8的groupBy進行分組。上面的去重裏我也是沿用了這個思想。
這個方法雖然看起來簡單笨拙,但是卻是最有效的解決了我的問題的,多個字段如果大於2個字段,上面的Pair就不是很好用,並且分組出來的結構也複雜。
Map<String, List<Person>> peopleBySomeKey = people
.collect(Collectors.groupingBy(p -> getGroupingByKey(p), Collectors.mapping((Person p) -> p, toList())));
//write getGroupingByKey() function
private String getGroupingByKey(Person p){
return p.getAge()+"-"+p.getName();
}
//分組求和,key是分組屬性名
Map<String, Long> tt = orgHoldingDatas.stream()
.collect(Collectors.groupingBy(OrgHoldingData::getOrgTypeCode, Collectors.summingLong(OrgHoldingData::getHeldNum)));
list.stream()
.sorted(Comparator.comparing(User::getAge))
.collect(Collectors.groupingBy(User::getId))
.forEach((k, v) -> {
Optional<User> csm = v.stream().reduce((v1, v2) -> {
v1.setName(v1.getNameS() + "、" + v2.getName());
list.remove(v2);
return v1;
});
});
// 或者
list.stream().collect(Collectors.groupingBy(User::getName))
.forEach((k, v) -> {
Optional<User> sum = v.stream().reduce((v1, v2) -> { //合併
v1.setNum(v1.getNum() + v2.getNum());
v1.setPct(v1.getPct() + v2.getPct());
return v1;
});
if (sum.isPresent()) {
items.add(sum.get());
}
});
Map<String, List<Fruit>> map = list.stream()
.collect(Collectors.groupingBy((Function<Fruit, String>) fruit -> {
String key;
if (fruit.getType().equals("1")) {
key = "蘋果";
} else if (fruit.getType().equals("2")) {
key = "香蕉";
} else {
key = "其他";
}
return key;
}, Collectors.toList()));
分區
分區的話就結果就只有兩個部分part這種。要麼是這一個區要麼是另一個。
Map<Boolean, List<Student>> map = students.stream()
.collect(Collectors.partitioningBy(student -> student.getScore() > 90));
巧用flatMap
// 應該使用flatMap . flatMap()的作用在於打平
List<String> reList = list.stream().map(item -> item.split(",")).flatMap(Arrays::stream).distinct()
.collect(Collectors.toList());
歸約
歸約操作就是將流中的元素進行合併,形成一個新的值,常見的歸約操作包括求和,求最大值或最小值。歸約操作一般使用reduce()
方法,與map()
方法搭配使用,可以處理一些很複雜的歸約操作。有點兒類似大數據裏的Map-Reduce。
流統計
- DoubleSummaryStatistics
- LongSummaryStatistics
- IntSummaryStatistics
並行流
並行流使用集合的parallelStream()
方法可以獲取一個並行流。Java內部會將流的內容分割成若干個子部分,然後將它們交給多個線程並行處理,這樣就將工作的負擔交給多核CPU的其他內核處理。
在並行流中,我們關注最多的大概就是性能問題。在觀察發現,在選擇合適的數據結構和處理後,並行流確實可以優於平時的for循環。
parallelStream()
本質上基於java7的Fork-Join框架實現,其默認的線程數爲宿主機的內核數。
啓動並行流式處理雖然簡單,只需要將stream()
替換成parallelStream()
即可,但既然是並行,就會涉及到多線程安全問題,所以在啓用之前要先確認並行是否值得(並行的效率不一定高於順序執行),另外就是要保證線程安全。此兩項無法保證,那麼並行毫無意義,畢竟結果比速度更加重要。
更深入的parallelStream
詳見:深入淺出parallelStream
坑
- 在
Stream.of
創建的流中,對於流的使用只能操作一次,操作後會有個標誌位被置爲true,如果再次對此流
進行操作,會報錯。但是對於集合的流形式,如list.stream()不會有問題,可多次操作。 parallelStream
的多線程併發寫list會發生線程安全問題,list數據少了,導致數組越界。建議使用線程安全的集合類。
記一次java8 parallelStream使用不當引發的血案
Java8的ParallelStream踩坑記錄-雲棲社區-阿里雲
性能測試
Java Stream API性能測試 - CarpenterLee - 博客園
參考
Java8 新特性之流式數據處理 - 深藍至尊 - 博客園
Java 8新特性(二):Stream API_Java_一書生VOID-CSDN博客