Stream是Java8中,操作集合的一個重要特性。
-
從iteration到Stream操作
當你操作一個集合的時候,你通常的做法是迭代每一個元素,然后處理你想要的事情。舉個例子:
String contents = new String(Files.readAllBytes( Paths.get("alice.txt")), StandardCharsets.UTF_8); // 讀取文件到字符串中 List<String> words = Arrays.asList(contents.split("[\\P{L}]+")); // 截取words
現在我們來迭代操作它:
int count=0;
for (String w : words) { if (w.length() > 12) count++; }
這段代碼有什么問題嗎?除了並行處理不是很好以為我想是沒有。在Java8中,相同的操作是這么處理的:
long count = words.stream().filter(w -> w.length() > 12).count();
so cool!從代碼中,我們就能非常容易的看出它要表達的意思,filter是針對words的過濾。
有人會問,這樣的操作的確讓人很是興奮,但是剛才說到的並行處理它能解決嗎?答案是不可以,但是Java8給我們提供了非常好的API,並行處理如下:
long count = words.parallelStream().filter(w -> w.length() > 12).count();
通過將stream()修改為parallelStream(),這樣就可以並行的進行過濾和統計了。
從表面上,stream看起來和集合很像,你可以自由的操作它。但是有以下幾點不同:
- stream不存儲集合元素
- stream操作不修改源數據,他們是返回一個新的streams來承載結果
- stream操作都會盡可能的進行延遲加載。這意味着當需要使用結果的時候它才會才運行。
當你使用stream的時候,需要關注一下3個階段:
- 創建一個stream
- 指定的中間操作將初始stream轉化為其他stream
- 最終的操作會產生一個結果,在調用最終操作前都會延遲執行的。在這之后,stream不會再被使用。
在上面的例子中,stream()和parallelStream()創造了stream,filter方法轉化了它,count方法是最終的操作。
注意:stream操作不是按照引用的順序來執行的。在例子中,知道count調用才執行。當count方法請求第一個元素,filter方法開始請求元素,知道找到一個長度大於12的元素。
-
Stream創建
Java8中,你就可以將集合的操作都利用stream來處理,如果你有一個數組,使用靜態的Stream.of方法來實現:
// 分隔后返回String[] Stream<String> words = Stream.of(content.split("[\\P{L}]+"));
of方法定義:
public static<T> Stream<T> of(T t); public static<T> Stream<T> of(T... values);
所以你可以構造很多參數的stream。
使用Arrays.stream(array, from, to),可以得到數組的一部分。
想要創建一個空的stream,可以使用Stream.empty方法:
Stream<String> silence = Stream.empty();
stream接口有2個靜態方法來構造無限的streams。generate方法是通過無參函數的。比如:
Stream<String> echos = Stream.generate(() -> "Echo");
or
Stream<Double> randoms = Stream.generate(Math::random);
如果想要產生一個無限的序列,例如:0 1 2 3 ...,可以使用iterate方法來實現:
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
-
filter.map和flatMap方法
map方法也可以達到上面例子的效果,例如:
Stream<String> lowercaseWords = words.map(String::toLowerCase);
使用Lambda表達式如下:
Stream<Character> firstChars = words.map(s -> s.charAt(0));
上面這段代碼是將words中每個詞的第一個字符取出來。
在Java8之前,如果你想將一段字符串分隔出來,代碼如下:
public static Stream<Character> characterStream(String s) { List<Character> result = new ArrayList<>(); for (char c : s.toCharArray()) result.add(c); return result.stream(); }
調用characterStream("boat"), 將返回['b','o','a','t']
如果用map來操作的話,如下:
Stream<Stream<Character>> result = words.map(w -> characterStream(w));
這里也可以使用flatMap來替代map:
Stream<Character> letters = words.flatMap(w -> characterStream(w));
- 提取子Stream和合並Stream
stream.limit(n)方法和SQL中的limit很像,就是取前n個符合條件的數據,例如:
List<Person> persons = Arrays.asList( new Person("安紅", "女", 12), new Person("索隆", "男", 23), new Person("路飛", "男", 22) ); Stream<Person> partialPersonStream = persons.stream().limit(2); // 將Stream轉換為List List<Person> partialPerson = partialPersonStream.collect(Collectors.toList()); Assert.assertTrue(2 == partialPerson.size()); Assert.assertTrue("安紅".equals(partialPerson.get(0).getName())); Assert.assertTrue("索隆".equals(partialPerson.get(1).getName()));
stream.skip(n)為跳過前n個數據,例如:
List<Person> persons = Arrays.asList( new Person("安紅", "女", 12), new Person("索隆", "男", 23), new Person("路飛", "男", 22) ); // 跳過前2個 Stream<Person> skipPersonStream = persons.stream().skip(2); List<Person> skipPersons = skipPersonStream.collect(Collectors.toList()); Assert.assertTrue(1 == skipPersons.size()); Assert.assertTrue("路飛".equals(skipPersons.get(0).getName()));
stream.concat()方法可以對stream進行追加操作,例如:
Stream<String> combined = Stream.concat(characterStream("Hello"), characterStream("world"));
這里stream的結果為['h','e','l','l','o','w','o','r','l','d']
也許大家會覺得這樣的操作,一旦debug起來會比較困難,但是大家不用着急,peek方法就會很好的解決這個問題了:
Object[] powers = Stream.iterate(1.0, p->p*2).peek(e->System.out.println(e)).limit(20).toArray();
這樣就可以將每次得到的結果打印出來了,怎么樣,Java8更干、更爽、更貼心吧?
- 狀態變換
distinct方法用來獲取唯一性數據,清除重復數據
// distinct - 清除重復數據 Stream<String> uniqueWordStream = Stream.of("a", "b", "b", "c", "a", "d").distinct(); List<String> uniqueWords = uniqueWordStream.collect(Collectors.toList()); Assert.assertTrue(4 == uniqueWords.size()); // a, b, c, d
- Optional
max和min分別返回最大值和最小值,例如:
Optional<String> largest = words.max(String::compareToIgnoreCase); if (largest.isPresent()) { System.out.println("largest: " + largest.get()); }
// max - 獲取最大值 Optional<Person> maxAgePerson = persons.stream().max((p1, p2) -> Integer.compare(p1.getAge(), p2.getAge())); if (maxAgePerson.isPresent()) { Person person = maxAgePerson.get(); Assert.assertTrue("索隆".equals(person.getName())); }
findFirst會返回第一個數據:
// 返回Q開頭的第一個數據 Optional<String> startWithQ = words.filter(s->s.startWith("Q")).findFirst();
如果想匹配所有符合要求的,可以使用如下:
// 匹配所有Q開頭的字符串 Optional<String> startsWithQ = words.parallel().filter(s->s.startWith("Q")).findAny();
如果想要查找是否存在匹配,可以如下:
boolean isWordStartWithQ = words.parallel().anyMatch(s->s.startWith("Q"));
Optional對象是可以是一個T的包裝類,也可以不是對象。它主要是為了替換引用類型T對象或者null。正確使用的話會更安全。
- 結果的處理
結果處理使用collect來處理,例子如下:
// stream的結果收集和處理 Stream<Person> stream = persons.stream().filter(p->p.getAge() > 20); Set<Person> setResult = stream.collect(Collectors.toSet()); // 獲取list中name一列的數據 Stream<String> nameStream = persons.stream().map(p->p.getName()); //Stream<String> nameStream = stream.map(p->p.getName()); // Error: 不要這么操作,否則會有問題,無法找到p List<String> names = nameStream.collect(Collectors.toList()); Assert.assertTrue(3 == names.size()); // 將所有名字連接起來,逗號分隔 String nameStr = nameStream.collect(Collectors.joining(",")); // Error: 執行到這里的時候就會報錯了,因為nameStream已經關閉了 Assert.assertTrue("安紅,索隆,路飛".equals(nameStr));
- Collecting into Maps
toMap用來處理將數據轉化為map結構,例子如下:
// Colloecting into Maps Map<String, Integer> peoples = persons.stream().collect(Collectors.toMap(Person::getName, Person::getAge)); Stream<Locale> locales = Stream.of(Locale.getAvailableLocales()); Map<String, String> languageNames = locales.collect( Collectors.toMap( l -> l.getDisplayLanguage(), l -> l.getDisplayLanguage(l), (existingValue, newValue) -> existingValue));
- 分組
分組使用函數groupingBy,例子如下:
Stream<Locale> locales1 = Stream.of(Locale.getAvailableLocales()); Map<String, List<Locale>> countryToLocales = locales1.collect(Collectors.groupingBy(Locale::getCountry)); List<Locale> swissLocales = countryToLocales.get("CH");
- 並行Stream
例子如下:
// parallel stream ConcurrentMap<String, List<Person>> result = persons.stream().parallel().collect(Collectors.groupingByConcurrent(Person::getName));