關注公衆號 JavaStorm 學習更多精彩java
Java 8 帶來一大新特性 Lambda 表達式流(Stream),當流與 Lambda 表達式結合使用,代碼將變得至關騷氣與簡潔。數據庫
假若有一個需求,須要對數據庫查詢的發票信息進行處理:編程
發票 Model數組
@Builder
@Data
public class Invoice implements Serializable {
/** * 銷方名稱 */
private String saleName;
/** * 是否做廢 */
private Boolean cancelFlag;
/** * 開票金額 */
private BigDecimal amount;
/** * 發票類型 */
private Integer type;
/** * 明細條數 */
private Integer detailSize;
}
複製代碼
咱們使用傳統的方式實現,在以前咱們初始化測試數據數據結構
public class StreamTest {
private List<Invoice> invoiceList;
@Before
public void initData() {
Invoice invoice = Invoice.builder().amount(BigDecimal.valueOf(100.02)).cancelFlag(false).detailSize(10)
.saleName("廣西製藥").type(1).build();
Invoice invoice2 = Invoice.builder().amount(BigDecimal.valueOf(89032478.9)).cancelFlag(false).detailSize(2)
.saleName("深圳電子科技").type(1).build();
Invoice invoice3 = Invoice.builder().amount(BigDecimal.valueOf(2077777889)).cancelFlag(true).detailSize(6)
.saleName("宇宙心空").type(1).build();
Invoice invoice4 = Invoice.builder().amount(BigDecimal.valueOf(356.8)).cancelFlag(false).detailSize(10)
.saleName("孟達餐廳").type(2).build();
Invoice invoice5 = Invoice.builder().amount(BigDecimal.valueOf(998.88)).cancelFlag(false).detailSize(0)
.saleName("網紅餐廳").type(2).build();
Invoice invoice6 = Invoice.builder().amount(BigDecimal.valueOf(9009884.09)).cancelFlag(false).detailSize(1)
.saleName("機動車").type(3).build();
invoiceList = Stream.of(invoice, invoice2, invoice3, invoice4, invoice5, invoice6).collect(Collectors.toList());
System.out.println("原始數據:" + invoiceList.toString());
}
複製代碼
Java8 以前的實現方式dom
/** * 篩選出金額小於 10000 的發票,根據金額排序,獲取排序後的銷方名稱列表 */
@Test
public void testJava7() {
ArrayList<Invoice> lowInvoiceList = new ArrayList<>();
//篩選出 金額小於 10000 的發票
for (Invoice invoice: invoiceList) {
if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0) {
lowInvoiceList.add(invoice);
}
}
// 對篩選出的發票排序
lowInvoiceList.sort(new Comparator<Invoice>() {
@Override
public int compare(Invoice o1, Invoice o2) {
return o1.getAmount().compareTo(o2.getAmount());
}
});
// 獲取排序後的銷方名字
ArrayList<String> nameList = new ArrayList<>();
for (Invoice invoice : lowInvoiceList) {
nameList.add(invoice.getSaleName());
}
}
複製代碼
Java8 以後的騷氣操做,一鼓作氣。不再用加班寫又臭又長的代碼了ide
@Test
public void testJava8() {
List<String> nameList = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)// 過濾數據
.sorted(Comparator.comparing(Invoice::getAmount))// 對金額升序排序
.map(Invoice::getSaleName)//提取名稱
.collect(Collectors.toList());//轉換成list
}
複製代碼
一套龍服務的感受,一鼓作氣送你上青天。大大減小了代碼量。 函數
如今又來一個需求性能
對查詢出來的發票數據進行分類,返回一個 Map<Integer, List> 的數據。學習
回顧下 Java7 的寫法,有沒有一種我擦,這也太麻煩了。還能不能早點下班回去抱女友。
@Test
public void testGroupByTypeJava7() {
HashMap<Integer, List<Invoice>> groupMap = new HashMap<>();
for (Invoice invoice : invoiceList) {
//存在則追加
if (groupMap.containsKey(invoice.getType())) {
groupMap.get(invoice.getType()).add(invoice);
} else {
// 不存在則初始化添加
ArrayList<Invoice> invoices = new ArrayList<>();
invoices.add(invoice);
groupMap.put(invoice.getType(), invoices);
}
}
System.out.println(groupMap.toString());
}
複製代碼
接着就是咱們利用 stream 的騷操做代碼實現上面的需求
groupingBy 分組
@Test
public void testGroupByTypeJava8() {
Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));
}
複製代碼
就是這麼簡單粗暴,一行代碼直搗黃龍。
Stream(流)是一個來自數據源的元素隊列並支持聚合操做,它不是數據結構並不保存數據,主要目的是在於計算。
元素是特定類型的對象,造成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。 數據源流的來源。能夠是集合,數組,I/O channel, 產生器 generator 等。 聚合操做相似SQL語句同樣的操做,好比filter, map, reduce, find, match, sorted等。 和之前的Collection操做不一樣,Stream操做還有兩個基礎的特徵:
主要有五種方式
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
複製代碼
int[] intArr = new int[]{1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(intArr);
複製代碼
經過Arrays.stream方法生成流,而且該方法生成的流是數值流【即IntStream】而不是Stream<Integer>
。補充一點使用數值流能夠避免計算過程當中拆箱裝箱,提升性能。
Stream API提供了mapToInt、mapToDouble、mapToLong三種方式將對象流【即Stream】轉換成對應的數值流,同時提供了boxed方法將數值流轉換爲對象流
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
複製代碼
經過Stream的of方法生成流,經過Stream的empty方法能夠生成一個空流
Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
複製代碼
經過Files.line方法獲得一個流,而且獲得的每一個流是給定文件中的一行
iterator: iterate方法接受兩個參數,第一個爲初始化值,第二個爲進行的函數操做,由於iterator生成的流爲無限流,經過limit方法對流進行了截斷,只生成5個偶數
Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(5);
複製代碼
generator: 接受一個參數,方法參數類型爲Supplier,由它爲流提供值。generate生成的流也是無限流,所以經過limit對流進行了截斷
Stream<Double> stream = Stream.generate(Math::random).limit(5);
複製代碼
主要分爲兩種類型
一個流能夠後面跟隨零個或多箇中間操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。
這類操做都是惰性化的,僅僅調用到這類方法,並無真正開始流的遍歷,真正的遍歷需等到終端操做時,常見的中間操做有下面即將介紹的filter、map等
一個流有且只能有一個終端操做,當這個操做執行後,流就被關閉了,沒法再被操做,所以一個流只能被遍歷一次,若想在遍歷須要經過源數據在生成流。終端操做的執行,纔會真正開始流的遍歷。以下面即將介紹的 count、collect 等。
filter篩選
Stream<Invoice> invoiceStream = invoiceList.stream().filter(invoice -> invoice.getDetailSize() < 10);
複製代碼
distinct去除重複元素
List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream().distinct();
複製代碼
limit返回指定流個數
Stream<Invoice> invoiceStream = invoiceList.stream().limit(3);
複製代碼
經過limit方法指定返回流的個數,limit的參數值必須>=0,不然將會拋出異常
skip跳過流中的元素
List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream().skip(2);
複製代碼
經過skip方法跳過流中的元素,上述例子跳過前兩個元素,因此打印結果爲2,3,4,5,skip的參數值必須>=0,不然將會拋出異常。
map流映射
所謂流映射就是將接受的元素映射成另一個元素
List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action");
Stream<Integer> stream = stringList.stream().map(String::length);
複製代碼
經過 map 方法能夠完成映射,該例子完成中String -> Integer的映射,以前上面的例子經過 map 方法完成了 Invoice -> String 的映射
flatMap流轉換
將一個流中的每一個值都轉換爲另外一個流
List<String> wordList = Arrays.asList("Hello", "World");
List<String> strList = wordList.stream()
.map(w -> w.split(""))// 將元素根據 空格分隔字符的Stream<String[]>
.flatMap(Arrays::stream)// 將Stream<String[]> 轉換成 Stream<String>
.distinct() //去重
.collect(Collectors.toList());
System.out.println(strList.toString());
複製代碼
map(w -> w.split(" "))的返回值爲Stream<String[]>
,咱們想獲取Stream<String>
,能夠經過flatMap方法完成Stream ->Stream的轉換。因此最後打印的結果是 [H, e, l, o, W, r, d]
元素匹配
if (invoiceList.stream().allMatch(Invoice::getCancelFlag)) {
System.out.println("發票全是做廢");
}
複製代碼
存在做廢發票則打印
if (invoiceList.stream().anyMatch(Invoice::getCancelFlag)) {
System.out.println("存在做廢發票");
}
複製代碼
等同於
for (Invoice invoice : invoiceList) {
if (invoice.getCancelFlag()) {
System.out.println("存在做廢發票");
break;
}
}
複製代碼
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
System.out.println("值都小於3");
}
複製代碼
統計流中元素個數
long count = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.count();
複製代碼
long count = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.collect(Collectors.counting());
複製代碼
最後一種統計元素個數的方法在與collect聯合使用的時候特別有用
查找
Optional<Invoice> first = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.findFirst();
複製代碼
經過 findFirst 找到金額小於 10000 的第一個元素
Optional<Invoice> any = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.findAny();
複製代碼
經過findAny方法查找到其中一個小於 10000 的元素並打印,由於內部進行優化的緣由,當找到第一個知足大於三的元素時就結束,該方法結果和findFirst方法結果同樣。提供findAny方法是爲了更好的利用並行流,findFirst方法在並行上限制更多【本篇文章將不介紹並行流】
reduce將流中的元素組合起來
假設咱們對一個集合中的值進行求和
jdk8 以前
int sum = 0;
for (int i : integerList) {
sum += i;
}
複製代碼
jdk8以後經過reduce進行處理
int sum = integerList.stream().reduce(0, (a, b) -> (a + b));
//還能夠用方法引用寫
int sum = integerList.stream().reduce(0, Integer::sum);
複製代碼
好比統計發票金額求和
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, (a, b) -> (a.add(b)));
複製代碼
繼續使用方法引用簡化
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
複製代碼
reduce 接受兩個參數,一個初始值這裏是0,一個BinaryOperator<T> accumulator
來將兩個元素結合起來產生一個新值,
另外reduce方法還有一個沒有初始化值的重載方法
獲取流中最小最大值
經過min/max獲取最小最大值
Optional<BigDecimal> min = invoiceList.stream().map(Invoice::getAmount).min(BigDecimal::compareTo);
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).max(BigDecimal::compareTo);
複製代碼
也能夠寫成
OptionalInt min1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).min();
OptionalInt max1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).max();
複製代碼
min獲取流中最小值,max獲取流中最大值,方法參數爲Comparator<? super T> comparator
經過minBy/maxBy獲取最小最大值
invoiceList.stream().map(Invoice::getAmount).collect(Collectors.minBy(BigDecimal::compareTo)).get();
複製代碼
經過reduce獲取最小最大值
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal::max);
複製代碼
求和
經過summingInt
Integer sum = invoiceList.stream().collect(Collectors.summingInt(Invoice::getDetailSize));
複製代碼
若是數據類型爲double、long,則經過summingDouble、summingLong方法進行求和
經過reduce
Integer sum = invoiceList.stream().map(Invoice::getDetailSize).reduce(0, Integer::sum);
複製代碼
經過sum,最佳寫法
//推薦寫成
Integer sum = invoiceList.stream().mapToInt(Invoice::getDetailSize).sum();
複製代碼
在上面求和、求最大值、最小值的時候,對於相同操做有不一樣的方法能夠選擇執行。能夠選擇collect、reduce、min/max/sum方法,推薦使用min、max、sum方法。由於它最簡潔易讀,同時經過mapToInt將對象流轉換爲數值流,避免了裝箱和拆箱操做
經過averagingInt求平均值
Double avg = invoiceList.stream().collect(Collectors.averagingInt(Invoice::getDetailSize));
複製代碼
若是數據類型爲double、long,則經過averagingDouble、averagingLong方法進行求平均
對於BigDecimal 則須要先求和再除以總條數
List<BigDecimal> sumList = invoiceList.stream().map(Invoice::getAmount).collect(Collectors.toList());
BigDecimal average = average(sumList, RoundingMode.HALF_UP);
// 求平均值
public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
BigDecimal sum = bigDecimals.stream()
.map(Objects::requireNonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(bigDecimals.size()), roundingMode);
}
複製代碼
經過summarizingInt同時求總和、平均值、最大值、最小值
IntSummaryStatistics statistics = invoiceList.stream().collect(Collectors.summarizingInt(Invoice::getDetailSize));
double average1 = statistics.getAverage();
int max1 = statistics.getMax();
int min1 = statistics.getMin();
long sum = statistics.getSum();
複製代碼
經過foreach進行元素遍歷
invoiceList.forEach(item -> {
System.out.println(item.getAmount());
});
複製代碼
經過joining拼接流中的元素
String result = invoiceList.stream().map(Invoice::getSaleName).collect(Collectors.joining(", "));
複製代碼
經過groupingBy進行分組
Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));
複製代碼
在collect方法中傳入groupingBy進行分組,其中groupingBy的方法參數爲分類函數。還能夠經過嵌套使用groupingBy進行多級分類
Map<String, Map<String, List<RzInvoice>>> = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType, Collectors.groupingBy(invoice -> {
if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) <= 0) {
return "low";
} else if (invoice.getAmount().compareTo(BigDecimal.valueOf(80000)) <= 0) {
return "mi";
} else {
return "high";
}
})));
複製代碼
首先根據 發票類型分組,再根據開票金額大小分組,返回的數據類型是 Map<String, Map<String, List>>
進階經過partitioningBy進行分區
特殊的分組,它分類依據是true和false,因此返回的結果最多能夠分爲兩組
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.partitioningBy(RzInvoice::getCancelFlag));
複製代碼
等同於
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.groupingBy(RzInvoice::getCancelFlag));
複製代碼
這個例子可能並不能看出分區和分類的區別,甚至以爲分區根本沒有必要,換個明顯一點的例子:
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Map<Boolean, List<Integer>> result = integerList.stream().collect(partitioningBy(i -> i < 3));
複製代碼
返回值的鍵仍然是布爾類型,可是它的分類是根據範圍進行分類的,分區比較適合處理根據範圍進行分類
來一個本人在工做中遇到的樣例
// 過濾T-1至T-12 近12月數據,根據省份分組求和開票金額,使用金額進行倒序,產生LinkedHashMap
LinkedHashMap<String, BigDecimal> areaSortByAmountMaps =
invoiceStatisticsList.stream().filter(FilterSaleInvoiceUtil.filterSaleInvoiceWithRange(1, 12, analysisDate)) //根據時間過濾數據
.collect(Collectors.groupingBy(FkSalesInvoiceStatisticsDO::getBuyerAdministrativeAreaCode
, Collectors.reducing(BigDecimal.ZERO, FkSalesInvoiceStatisticsDO::getInvoiceAmount, BigDecimal::add)))// 根據開票地區分組,並同時將每一個分組數據的開票金額求和
.entrySet().stream().sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed()) // 根據金額大小倒序
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); //收集數據生成LinkedHashMap
複製代碼
經過使用Stream API能夠簡化代碼,同時提升了代碼可讀性,趕忙在項目裏用起來。講道理在沒學Stream API以前,誰要是給我在應用裏寫不少Lambda,Stream API,飛起就想給他一腳。
我想,我如今可能愛上他了【嘻嘻】。同時使用的時候注意不要將聲明式和命令式編程混合使用。