Java 8 Stream API

這是我參與8月更文挑戰的第9天,活動詳情查看:8月更文挑戰java

Java 8 介紹

  1. Java 8 發版於 2014 年,多年過去,目前依舊是最經常使用的 JDK 版本
  2. Java 8 在加強代碼可讀性、簡化代碼方面,增長了不少功能,好比 Lambda、Stream 流操做、並行流(ParallelStream)、Optional 可空類型、新的日期時間類型等
  3. Lambda 表達式配合 Stream 流操做爲咱們平常編碼極大的提高了效率

Lambda 表達式

  1. 匿名類內部類雖然沒有類名,但仍是要給出方法定義git

  2. Lambda 表達式的初衷是進一步簡化匿名類的語法數組

  3. 在實現上,Lambda 表達式並非匿名類的語法糖安全

  4. Lambda 和匿名類的示例markdown

    public class ThreadCreator {
        public static Thread createThreadByAnonymous() {
            return new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Anonymous thread print");
                }
            });
        }
    
        public static Thread createThreadByLambda() {
            return new Thread(() -> {
                System.out.println("Lambda thread print");
            });
        }
    }
    複製代碼
  5. Lambda 表達式如何匹配類型接口?=> 函數式接口數據結構

    函數式接口是一種只有單一抽象方法的接口,使用 @FunctionalInterface 來描述,能夠隱式地轉換成 Lambda 表達式app

    使用 Lambda 表達式建立函數式接口的示例, 可讓函數成爲程序的一等公民,從而像普通數據同樣看成參數傳遞dom

    JDK 的 java.util.function 中提供了許多原生的函數式接口,如 Supplieride

    @FunctionalInterface
    public interface Supplier<T> {
    
        /** * Gets a result. * * @return a result */
        T get();
    }
    複製代碼

    使用 Lamda 或方法引用來獲得函數式接口的實例函數

    // 使用 Lambda 表達式提供 Supplier 接口實現,返回 OK 字符串
    Supplier<String> stringSupplier = () ->"OK";
    // 使用方法引用提供 Supplier 接口實現,返回空字符串
    Supplier<String> supplier = String::new;
    複製代碼

    方法引用是 Lambda 表達式的另外一種表現形式

    Lambda 表達式可用方法引用代替的場景:Lambda 表達式的主體僅包含一個表達式,且該表達式僅調用了一個已經存在的方法

    方法引用能夠是:

    • 類 : : 靜態方法
    • 類 : : new
    • 類 : : 實例方法((A, B) -> A.實例方法(B) <=> A 的類 : : 實例方法)
    • 任意對象 : : 實例方法

建立 Stream

  1. 利用 stream 方法將 list 或數組轉換爲流

    Arrays.asList("a1", "a2", "a3").stream().forEach(System.out::println);
    Arrays.stream(new int[]{1, 2, 3}).forEach(System.out::println);
    複製代碼

    非基本數據類型的數值也可使用 Arrays.asList 方法轉 list 再調 stream

  2. 經過 Stream.of 方法直接傳入多個元素構成一個流

    String[] arr = {"a", "b", "c"};
    Stream.of(arr).forEach(System.out::println);
    Stream.of("a", "b", "c").forEach(System.out::println);
    Stream.of(1, 2, "a").map(item -> item.getClass().getName())
        .forEach(System.out::println);
    複製代碼
  3. 經過 Stream.iterate 方法使用迭代的方式構造一個無限流,而後使用 limit 限制流元素個數

    Stream.iterate(2, item -> item * 2).limit(10).forEach(System.out::println);
    Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.TEN))
        .limit(10).forEach(System.out::println);
    複製代碼
  4. 經過 Stream.generate 方法從外部傳入一個提供元素的 Supplier 來構造無限流,再使用 limit 限制流元素個數

    Stream.generate(() -> "test").limit(3).forEach(System.out::println);
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
    複製代碼
  5. 經過 IntStream 或 DoubleStream 構造基本類型的流

    // IntStream 和 DoubleStream
    IntStream.range(1, 3).forEach(System.out::println);
    IntStream.range(0, 3).mapToObj(i -> "x").forEach(System.out::println);
    IntStream.rangeClosed(1, 3).forEach(System.out::println);
    DoubleStream.of(1.1, 2.2, 3.3).forEach(System.out::println);
    
    // 使用 Random 類建立隨機流
    new Random()
        .ints(1, 100)  // IntStream
        .limit(10)
        .forEach(System.out::println);
    
    // 注意基本類型流和裝箱後的流的區別
    Arrays.asList("a", "b", "c").stream()   // Stream<String>
        .mapToInt(String::length)       // IntStream
        .asLongStream()                 // LongStream
        .mapToDouble(x -> x / 10.0)     // DoubleStream
        .boxed()                        // Stream<Double>
        .mapToLong(x -> 1L)             // LongStream
        .mapToObj(x -> "")              // Stream<String>
        .collect(Collectors.toList());
    複製代碼

中間操做

Stream 經常使用 API:

image-20210729215659491

如下爲測試用實體類,代碼省略了 getter、settter 和構造方法

訂單項

public class OrderItem {
    private Long productId;// 商品ID
    private String productName;// 商品名稱
    private Double productPrice;// 商品價格
    private Integer productQuantity;// 商品數量
}
複製代碼

訂單

public class Order {
    private Long id;
    private Long customerId;// 顧客ID
    private String customerName;// 顧客姓名
    private List<OrderItem> orderItemList;// 訂單商品明細
    private Double totalPrice;// 總價格
    private LocalDateTime placedAt;// 下單時間
}
複製代碼

消費者

public class Customer {
    private Long id;
    private String name;// 顧客姓名
}
複製代碼

filter

filter 操做用做過濾,相似於 SQL 中的 where 條件,接收一個 Predicate 謂詞對象做爲參數,返回過濾後的流

能夠連續使用 filter 進行多層過濾

// 查找最近半年的金額大於40的訂單
orders.stream()
        .filter(Objects::nonNull) // 過濾null值
        .filter(order -> order.getPlacedAt()
                .isAfter(LocalDateTime.now().minusMonths(6))) // 最近半年的訂單
        .filter(order -> order.getTotalPrice() > 40) // 金額大於40的訂單
        .forEach(System.out::println);  
複製代碼

map

map 操做用作轉換,也叫投影,相似於 SQL 中的 select

// 計算全部訂單商品數量
// 1. 經過兩次遍歷實現
LongAdder longAdder = new LongAdder();
orders.stream().forEach(order ->
                        order.getOrderItemList().forEach(orderItem -> longAdder.add(orderItem.getProductQuantity())));

System.out.println("longAdder = " + longAdder);

// 2. 使用兩次 mapToLong 和 sum 方法實現
long sum = orders.stream().mapToLong(
    order -> order.getOrderItemList().stream()
    .mapToLong(OrderItem::getProductQuantity)
    .sum()
).sum();
複製代碼

flatMap

flatMap 是扁平化操做,即先用 map 把每一個元素替換爲一個流,再展開這個流

// 統計全部訂單的總價格
// 1. 直接展開訂單商品進行價格統計
double sum1 = orders.stream()
    .flatMap(order -> order.getOrderItemList().stream())
    .mapToDouble(item -> item.getProductQuantity() * item.getProductPrice())
    .sum();

// 2. 另外一種方式 flatMapToDouble,即 flatMap + mapToDouble,返回 DoubleStream
double sum2 = orders.stream()
    .flatMapToDouble(order ->
                     order.getOrderItemList()
                     .stream().mapToDouble(item -> 
                                           item.getProductQuantity() * item.getProductPrice())
                    )
    .sum();
複製代碼

sorted

sorted 是排序操做,相似 SQL 中的 order by 子句,接收一個 Comparator 做爲參數,可以使用 Comparator.comparing 來由大到小排列,加上 reversed 表示倒敘

// 大於 50 的訂單,按照訂單價格倒序前 5
orders.stream()
        .filter(order -> order.getTotalPrice() > 50)
        .sorted(Comparator.comparing(Order::getTotalPrice).reversed()) 
        .limit(5)
        .forEach(System.out::println);
複製代碼

skip & limit

skip 用於跳過流中的項,limit 用於限制項的個數

// 按照下單時間排序,查詢前 2 個訂單的顧客姓名
orders.stream()
    .sorted(Comparator.comparing(Order::getPlacedAt))
    .map(order -> order.getCustomerName())
    .limit(2)
    .forEach(System.out::println);

// 按照下單時間排序,查詢第 3 和第 4 個訂單的顧客姓名
orders.stream()
    .sorted(Comparator.comparing(Order::getPlacedAt))
    .map(order -> order.getCustomerName())
    .skip(2).limit(2)
    .forEach(System.out::println);
複製代碼

distinct

distinct 操做的做用是去重,相似 SQL 中的 distinct

// 去重的下單用戶
orders.stream()
    .map(Order::getCustomerName)
    .distinct()
    .forEach(System.out::println);


// 全部購買過的商品
orders.stream()
    .flatMap(order -> order.getOrderItemList().stream())
    .map(OrderItem::getProductName)
    .distinct()
    .forEach(System.out::println);
複製代碼

終結操做

forEach

上文中已經屢次使用,內部循環流中的全部元素,對每個元素進行消費

forEachOrder 和 forEach 相似,但能保證消費順序

count

返回流中項的個數

toArray

轉換流爲數組

anyMatch

短路操做,有一項匹配就返回 true

// 查詢是否存在總價在 100 元以上的訂單
boolean b = orders.stream()
                .filter(order -> order.getTotalPrice() > 50)
                .anyMatch(order -> order.getTotalPrice() > 100);
複製代碼

其餘短路操做:

allMatch:所有匹配才返回 true

noneMatch:都不匹配才返回 true

findFirst:返回第一項的 Optional 包裝

findAny:返回任意一項的 Optional 包裝,串行流通常返回第一個

reduce

概括,一邊遍歷,一邊將處理結果保持起來,代入下一次循環

重載方法有三個,截圖來自 IDEA 參數提示:

reduce 重載方法

// 一個參數
// 求訂單金額總價
Optional<Double> reduce = orders.stream()
    .map(Order::getTotalPrice)
    .reduce((p, n) -> {
        return p + n;
    });
// 兩個參數
// 可指定一個初始值,初始值類型須要和 p、n 一致
Double reduce2 = orders.stream()
    .map(Order::getTotalPrice)
    .reduce(0.0, (p, n) -> {
        return p + n;
    });
複製代碼

三個參數的 reduce 方法:

能夠接收一個目標結果類型的初始值,一個串行的處理函數,一個並行的合併函數

// 將全部訂單的顧客名字進行拼接
// 第一個參數爲目標結果類型,這裏設置爲空的 StringBuilder
// 第二個參數 BiFunction,參數爲上一次的 StringBuilder 和流中的下一項,返回新的 StringBuilder
// 第三個參數 BinaryOperator,參數都是 StringBuilder,返回合併後的 StringBuilder
StringBuilder reduce = orders.stream()
    .reduce(new StringBuilder(), 
            (sb, next) -> {
        		return sb.append(next.getCustomerName() + ",");
    		}, 
            (sb1, sb2) -> {
        		return sb1.append(sb2);
    		});
複製代碼

其餘概括方法:

max/min 求最大/最小值,接收一個比較器做爲參數

對於 LongStream 等基礎類型 Stream,則不須要傳入比較器參數

collect

collect 是收集操做,對流進行終結操做,把流導出爲咱們須要的數據結構

collect 須要接收一個收集器 Collector 對象做爲參數,JDK 內置的 Collector 的實現類 Collectors 包含了許多經常使用的導出方式

collect 經常使用 API:

image-20210801115310365

導出流爲集合:

  1. toList 和 toUnmodifiableList
// 轉爲 List(ArrayList)
orders.stream().collect(Collectors.toList());
// 轉爲不可修改的 List
orders.collect(Collectors.toUnmodifiableList());
複製代碼
  1. toSet 和 toUnmodifiableSet
// 轉爲 Set
orders.stream().collect(Collectors.toSet());
// 轉爲不可修改的 Set
orders.stream().collect(Collectors.toUnmodifiableSet());
複製代碼
  1. toCollection 指定集合類型,如 LinkedList
orders.stream().collect(Collectors.toCollection(LinkedList::new));
複製代碼

導出流爲 Map:

  1. toMap,第三個參數能夠指定鍵名重複時選擇鍵值的規則
// 使用 toMap 獲取訂單 ID + 下單用戶名的 Map
orders.stream()
    .collect(Collectors.toMap(Order::getId, Order::getCustomerName))
    .entrySet().forEach(System.out::println);

//使用 toMap 獲取下單用戶名 + 最近一次下單時間的 Map
orders.stream()
    .collect(Collectors.toMap(Order::getCustomerName, Order::getPlacedAt,
                              (x, y) -> x.isAfter(y) ? x : y))
    .entrySet().forEach(System.out::println);
複製代碼
  1. toUnmodifiableMap:返回一個不可修改的 Map

  2. toConcurrentMap:返回一個線程安全的 Map

分組導出:

在 toMap 中遇到重複的鍵名,經過指定一個處理函數來選擇一個鍵值保留

大多數狀況下,咱們須要根據鍵名分組獲得 Map,Collectors.groupingBy 是更好的選擇

重載方法有三個,截圖來自 IDEA 參數提示:

image-20210801124509215

一個參數,等同於第二個參數爲 Collectors.toList,即鍵值爲 List 類型

// 按照下單用戶名分組,鍵值是該顧客對應的訂單 List
Map<String, List<Order>> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getCustomerName));
複製代碼

兩個參數,第二個參數用於指定鍵值類型

// 按照下單用戶名分組,鍵值是該顧客對應的訂單數量
Map<String, Long> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getCustomerName, 
                                               Collectors.counting()));
複製代碼

三個參數,第二個參數用於指定分組結果的 Map 類型,第三個參數用於指定鍵值類型

// 按照下單用戶名分組,鍵值是該顧客對應的全部商品的總價
Map<String, Double> collect = orders.stream()
        .collect(Collectors.groupingBy(Order::getCustomerName,
    						Collectors.summingDouble(Order::getTotalPrice)));
// 指定分組結果的 Map 爲 TreeMap 類型
Map<String, Double> collect = orders.stream()
        .collect(Collectors.groupingBy(Order::getCustomerName,
                                       TreeMap::new,
                                       Collectors.summingDouble(Order::getTotalPrice)));
複製代碼

分區導出:

分區使用 Collectors.partitioningBy,就是將數據按照 TRUE 或者 FALSE 進行分組

// 按照是否有下單記錄進行分區 
customers.stream()
                .collect(Collectors.partitioningBy(customer -> orders
                        .stream()
                        .mapToLong(Order::getCustomerId)
                        .anyMatch(id -> id == customer.getId())
                ));
// 等價於 
customers.stream()
                .collect(Collectors.partitioningBy(customer -> orders
                        .stream()
                        .filter(order -> order.getCustomerId() == customer.getId())
                        .findAny()
                        .isPresent()
                ));
複製代碼

類中間操做:

Collectors 還提供了相似於中間操做的 API,方便在收集時使用,如 counting、summingDouble、maxBy 等

Collectors.maxBy

// 獲取下單量最多的商品,三種方式
// 使用收集器 maxBy
Map.Entry<String, Integer> e1 = orders.stream()
    .flatMap(order -> order.getOrderItemList().stream())
    .collect(Collectors.groupingBy(OrderItem::getProductName,
                                   Collectors.summingInt(OrderItem::getProductQuantity)))
    .entrySet()
    .stream()
    .collect(Collectors.maxBy(Map.Entry.<String, Integer>comparingByValue()))
    .get();
// 使用中間操做 max
Map.Entry<String, Integer> e2 = orders.stream()
    .flatMap(order -> order.getOrderItemList().stream())
    .collect(Collectors.groupingBy(OrderItem::getProductName,
                                   Collectors.summingInt(OrderItem::getProductQuantity)))
    .entrySet()
    .stream()
    .max(Map.Entry.<String, Integer>comparingByValue())
    .get();
// 由大到小排序,再 findFirst
Map.Entry<String, Integer> e3 = orders.stream()
    .flatMap(order -> order.getOrderItemList().stream())
    .collect(Collectors.groupingBy(OrderItem::getProductName,
                                   Collectors.summingInt(OrderItem::getProductQuantity)))
    .entrySet()
    .stream()
    .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
    .findFirst()
    .get();
// 注:
// Map.Entry.<String, Integer>comparingByValue().reversed()
// 能夠與下面語句等價
// Comparator.comparing(Map.Entry<String, Integer>::getValue).reversed()
複製代碼

Collectors.joining

// 下單用戶名去重後拼接
String collect = orders.stream()
    .map(Order::getCustomerName)
    .distinct()
    .collect(Collectors.joining(","));
 // .collect(Collectors.joining(",", "[", "]")); // 可指定前綴和後綴
複製代碼
相關文章
相關標籤/搜索