Java8新特性(三) 之 Stream API

StreamAPI-Class

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-Package

可以看到Stream API裏的參數大多都是函數式接口的各種形態。

還不知道函數式接口的同學快來看看這兩篇

Java8的Lambda表達式你瞭解嗎?

Java8的函數式接口你真的瞭解嗎?

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異常,因爲流已經被關閉

流通常由三部分構成:

  1. 數據源:數據源一般用於流的獲取,比如本文開頭那個過濾用戶的例子中users.stream()方法。
  2. 中間處理:中間處理包括對流中元素的一系列處理,如:過濾(filter()),映射(map()),排序(sorted())。
  3. 終端處理:終端處理會生成結果,結果可以是任何不是流值,如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博客


求關注、分享、在看!!! 你的支持是我創作最大的動力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章