# 酷炫的 Stream API 最佳指南

關注公衆號 JavaStorm 學習更多精彩java

Java 8 帶來一大新特性 Lambda 表達式流(Stream),當流與 Lambda 表達式結合使用,代碼將變得至關騷氣與簡潔。數據庫

超級大招,釋放代碼

假若有一個需求,須要對數據庫查詢的發票信息進行處理:編程

  1. 取出金額小於 10000 的發票。
  2. 對篩選出來的數據排序。
  3. 獲取排序後的發票銷方名稱。

發票 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?

Stream(流)是一個來自數據源的元素隊列並支持聚合操做,它不是數據結構並不保存數據,主要目的是在於計算。

元素是特定類型的對象,造成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。 數據源流的來源。能夠是集合,數組,I/O channel, 產生器 generator 等。 聚合操做相似SQL語句同樣的操做,好比filter, map, reduce, find, match, sorted等。 和之前的Collection操做不一樣,Stream操做還有兩個基礎的特徵:

  • Pipelining:中間操做都會返回流對象自己。這樣多個操做能夠串聯成一個管道,如同流式風格(fluent style)。 這樣作能夠對操做進行優化,好比延遲執行(laziness)和短路( short-circuiting)。
  • 內部迭代:之前對集合遍歷都是經過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代,這叫作外部迭代。 Stream提供了內部迭代的方式,經過訪問者模式(Visitor)實現。

如何生成流

主要有五種方式

1. 經過集合生成

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
複製代碼

2.經過數組生成

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方法將數值流轉換爲對象流

3. 經過值生成

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
複製代碼

經過Stream的of方法生成流,經過Stream的empty方法能夠生成一個空流

4. 經過文件生成

Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
複製代碼

經過Files.line方法獲得一個流,而且獲得的每一個流是給定文件中的一行

5. 經過函數生成,iterate和generate兩個靜態方法從函數中生成流

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);
複製代碼

流的操做類型

主要分爲兩種類型

1. 中間操做

一個流能夠後面跟隨零個或多箇中間操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。

這類操做都是惰性化的,僅僅調用到這類方法,並無真正開始流的遍歷,真正的遍歷需等到終端操做時,常見的中間操做有下面即將介紹的filter、map等

2. 終端操做

一個流有且只能有一個終端操做,當這個操做執行後,流就被關閉了,沒法再被操做,所以一個流只能被遍歷一次,若想在遍歷須要經過源數據在生成流。終端操做的執行,纔會真正開始流的遍歷。以下面即將介紹的 count、collect 等。

中間操做 API

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]

元素匹配

  1. allMatch匹配全部
if (invoiceList.stream().allMatch(Invoice::getCancelFlag)) {
  System.out.println("發票全是做廢");
}
複製代碼
  1. anyMatch匹配其中一個

存在做廢發票則打印

if (invoiceList.stream().anyMatch(Invoice::getCancelFlag)) {
  System.out.println("存在做廢發票");
}
複製代碼

等同於

for (Invoice invoice : invoiceList) {
  if (invoice.getCancelFlag()) {
    System.out.println("存在做廢發票");
    break;
  }
}
複製代碼
  1. noneMatch所有不匹配
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
    System.out.println("值都小於3");
}
複製代碼

終端操做

統計流中元素個數

  1. 使用 count
long count = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .count();
複製代碼
  1. 使用 counting
long count = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .collect(Collectors.counting());
複製代碼

最後一種統計元素個數的方法在與collect聯合使用的時候特別有用

查找

  1. findFirst查找第一個
Optional<Invoice> first = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .findFirst();
複製代碼

經過 findFirst 找到金額小於 10000 的第一個元素

  1. findAny隨機查找一個
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,飛起就想給他一腳。

我想,我如今可能愛上他了【嘻嘻】。同時使用的時候注意不要將聲明式和命令式編程混合使用。

相關文章
相關標籤/搜索