重走Java基礎之Streams(四)

來源:重走Java基礎之Streams(四)
作者:知秋
博客:一葉知秋
轉(zhuǎn)載請注明來源和作者!


接上篇重走Java基礎之Streams(三)

使用Map.Entry的流在轉(zhuǎn)換后保留初始值

當你有一個Stream,你需要映射轉(zhuǎn)換但是想保留初始值,你可以使用下面的實用程序方法將'Stream`映射到Map.Entry:

public static <K, V> Function<K, Map.Entry<K, V>> 
entryMapper(Function<K, V> mapper)
{
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

然后你可以使用你的有權訪問原始值和映射轉(zhuǎn)換后值的轉(zhuǎn)換器來處理Streams:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
    .map(entryMapper(transformer));

然后,您可以繼續(xù)正常處理Stream。 這避免了創(chuàng)建中間集合的開銷。

將迭代器轉(zhuǎn)換為流

Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();    
Iterable<String> iterable = () -> iterator;
Stream<String> stream = StreamSupport.stream(iterable.spliterator(), false);

基于流來創(chuàng)建一個map

沒有重復鍵的簡單情況

Stream<String> characters = Stream.of("A", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}

可能存在重復鍵的情況

Collectors.toMapjavadoc的描述:

如果映射的鍵包含重復的(根據(jù)Object.equals(Object)),則會在執(zhí)行收集操作時會拋出IllegalStateException。 如果映射的鍵可能有重復,請使用toMap(Function,F(xiàn)unction,BinaryOperator)。

Stream<String> characters = Stream.of("A", "B", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(
                element -> element.hashCode(),
                element -> element,
                (existingVal, newVal) -> (existingVal + newVal)));

// map = {65=A, 66=BB, 67=C}

傳遞給Collectors.toMap(...)BinaryOperator生成在發(fā)生重復沖突情況下要存儲的值。 它可以:

  • 返回舊值,以流中的第一個值優(yōu)先,
  • 返回新值,以流中的最后一個值優(yōu)先,
  • 組合舊值和新值

按值分組

當你需要執(zhí)行等效的一個數(shù)據(jù)庫級聯(lián)“group by”操作(意思就是和此效果一樣的需求)時你可以使用 Collectors.groupingBy 。 為了說明,以下內(nèi)容創(chuàng)建了一個map,其中人們的姓名分別映射到姓氏:

List<Person> people = Arrays.asList(
    new Person("Sam", "Rossi"),
    new Person("Sam", "Verdi"),
    new Person("John", "Bianchi"),
    new Person("John", "Rossi"),
    new Person("John", "Verdi")
);

Map<String, List<String>> map = people.stream()
        .collect(
                // function mapping input elements to keys
                Collectors.groupingBy(Person::getName, 
                // function mapping input elements to values,
                // how to store values
                Collectors.mapping(Person::getSurname, Collectors.toList()))
        );

// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}

Live on Ideone

查找有關數(shù)值流的統(tǒng)計信息

Java 8提供了IntSummaryStatistics,DoubleSummaryStatisticsLongSummaryStatistics這些類,它們給出用于收集統(tǒng)計數(shù)據(jù)對象的狀態(tài),例如count,min,max,sumaverage。

Java SE 8

List naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
.mapToInt((x) -> x)     
.summaryStatistics();
System.out.println(stats);

運行結果如下:

Java SE 8

IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}

可能還有疑問,還是來張運行截圖吧:

獲取一個流的片段

skip: 返回一個丟棄原Stream的前N個元素后剩下元素組成的新Stream,如果原Stream中包含的元素個數(shù)小于N,那么返回空Stream;


limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數(shù)小于N,那就獲取其所有的元素;

Example:獲取一個包含30個元素的“Stream”,包含集合的第21到第50個(包含)元素。

final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);

Notes:

  • 如果n為負或maxSize為負,則拋出IllegalArgumentException
  • skip(long)limit(long)都是中間操作
  • 如果流包含少于n個元素,則skip(n)將返回一個空流
  • skip(long)limit(long)都是順序流管道上的廉價操作,但在有序并行管道上可能相當昂貴(指性能上)

再貼個例子:

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream()
.filter(num -> num != null
.distinct()
.mapToInt(num -> num * 2)
.peek(System.out::println)
.skip(2)
.limit(4)
.sum());

Joining a stream to a single String

一個經(jīng)常遇到的用例是從流創(chuàng)建一個String,其中每個流轉(zhuǎn)換出的字符串之間由一個特定的字符分隔。 Collectors.joining()方法可以用于這個,就像下面的例子:

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

String result = fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", "));
           
System.out.println(result);

Output:

APPLE, BANANA, ORANGE, PEAR

Collectors.joining()方法也可以滿足前綴和后綴:

String result = fruitStream.filter(s -> s.contains("e"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", ", "Fruits: ", "."));
           
System.out.println(result);

Output:

Fruits: APPLE, ORANGE, PEAR.

Live on Ideone

Reduction(聚合) with Streams

聚合是將二進制操作應用于流的每個元素以產(chǎn)生一個值的過程。

IntStreamsum()方法是一個簡化的例子; 它對流的每個項應用加法,得到一個最終值:

這相當于(((1+2)+3)+4)

Stream的reduce方法允許創(chuàng)建自定義reduction。 可以使用reduce方法來實現(xiàn)sum()方法:

IntStream istr;
    
//Initialize istr
    
OptionalInt istr.reduce((a,b)->a+b);

返回Optional對象,以便可以恰當?shù)靥幚砜盏腟treams。

reduction的另一個示例是將 Stream<LinkedList<T>>組合成單個 LinkedList<T>

Stream<LinkedList<T>> listStream;
    
//Create a Stream<LinkedList<T>>
    
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

您還可以提供* identity元素*。 例如,用于加法的標識元素為0,如x + 0 == x。 對于乘法,identity元素為1,如x * 1 == x。 在上面的例子中,identity元素是一個空的LinkedList,因為如果你將一個空列表添加到另一個列表,你“添加”的列表不會改變:

Stream<LinkedList<T>> listStream;

//Create a Stream<LinkedList<T>>

LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

注意,當提供一個identity元素時,返回值不會被包裝在一個Optional中 ---- 如果在空流上調(diào)用,reduce()將返回identity元素。

二元運算符也必須是* associative *,意思是 (a+b)+c==a+(b+c)。 這是因為元件可以以任何順序進行聚合操作(reduced)。 例如,可以如下執(zhí)行上述加法reduction:

這個reduction(聚合操作)等同于寫((1+2)+(3+4))。 關聯(lián)性的屬性還允許Java并行地reduction Stream - 每個處理器可以reduction Stream的一部分并得到結果,最后通過reduction結合每個處理器的結果。

使用流排序

List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");

System.out.println(data);

List<String> sortedData = data.stream().sorted().collect(Collectors.toList());

System.out.println(sortedData);

Output:

[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]

它也可以使用不同的比較機制,因為有一個重載sorted版本,它使用比較器作為其參數(shù)。

此外,您可以使用lambda表達式進行排序:

List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());

這將輸出[Sydney, New York, Mumbai, London, California, Amsterdam]

你可以使用Comparator.reverseOrder() ,一個對自然排序進行強行reverse的比較器(反排序)。

List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()

流操作類別

流操作分為兩個主要類別,中間和終端操作,以及兩個子類,無狀態(tài)和有狀態(tài)。


中間操作:

一個中間操作總是* lazy *(延遲執(zhí)行),例如一個簡單的“Stream.map”。 它不會被調(diào)用,直到流實際上消耗。 這可以很容易地驗證:

Arrays.asList(1, 2 ,3).stream().map(i -> {
    throw new RuntimeException("not gonna happen");
    return i;
});

中間操作是流的常見構造塊,指在數(shù)據(jù)源之后操作鏈,并且通常末端跟隨有觸發(fā)流鏈式執(zhí)行的終端操作。


終端操作

終端操作是觸發(fā)流的消耗的。 一些最常見的是 Stream.forEach或“ Stream.collect。 它們通常放置在一系列中間操作之后,幾乎總是* eager *。


無狀態(tài)操作

無狀態(tài)意味著每個環(huán)節(jié)(可以理解成流的每個處理環(huán)節(jié))在沒有其他環(huán)節(jié)的上下文的情況下被處理。 無狀態(tài)操作允許流的存儲器高效處理。 像Stream.map和Stream.filter這樣的不需要關于流的其他環(huán)節(jié)的信息的操作被認為是無狀態(tài)的。


狀態(tài)操作

狀態(tài)性意味著對每個環(huán)節(jié)的操作取決于(一些)流的其他環(huán)節(jié)。 這需要保留一個狀態(tài)。 狀態(tài)操作可能會與長流或無限流斷開。 像Stream.sorted 這樣的操作要求在處理任何環(huán)節(jié)之前處理整個流,這將在足夠長的流的環(huán)節(jié)中斷開。 這可以通過長流(run at your own risk)來證明(說的太拗口了,其實就是棧的遞歸操作,下一步的運行依靠上一步的結果來執(zhí)行,假如太深,就可能出現(xiàn)問題,看下面例子就知道了):

// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);

這將導致由于Stream.sorted的狀態(tài)的內(nèi)存不足:

// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);

原始流

Java為三種類型的原語“IntStream”(用于ints),LongStream(用于longs)和DoubleStream(用于doubles)提供專用的Stream。 除了是針對它們各自的原語的優(yōu)化實現(xiàn),它們還提供了幾個特定的終端方法,通常用于數(shù)學運算。 例如:

IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0

將流的結果收集到數(shù)組中

可以通過Stream.toArray()方法獲得一個數(shù)組:

List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");

String[] filteredFruits = fruits.stream()
    .filter(s -> s.contains("a"))
    .toArray(String[]::new);     

// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));

String[]::new是一種特殊的方法引用:構造函數(shù)引用。

查找匹配條件的第一個元素

可以找到符合條件的Stream 的第一個元素。

在這個例子中,我們將找到第一個平方超過了50000的Integer。

IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
    .filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
    .findFirst(); // Find the first filtered element

這個表達式將返回一個帶有結果的OptionalInt對象。

注意,使用無限的Stream,Java將繼續(xù)檢查每個元素,直到找到一個結果。 在一個有限的Stream,如果Java運行檢查了所以元素,但仍然找不到一個結果,它返回一個空的OptionalInt對象。

使用Streams生成隨機字符串

有時,創(chuàng)建隨機的Strings有時是有用的,或許作為Web服務的會話ID或在注冊應用程序后的初始密碼。 這可以很容易地使用Streams實現(xiàn)。

首先,我們需要初始化一個隨機數(shù)生成器。 為了增強生成的Strings的安全性,使用SecureRandom是一個好主意。

Note:創(chuàng)建一個SecureRandom是相當消耗資源的,所以最好的做法是只做一次,并且不時地調(diào)用它的一個setSeed()方法來重新設置它。

private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20)); 
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example

當創(chuàng)建隨機的String時,我們通常希望它們只使用某些字符(例如,只有字母和數(shù)字)。 因此,我們可以創(chuàng)建一個返回一個boolean的方法,稍后可以用它來過濾Stream。

//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
    //check for range to avoid using all unicode Letter (e.g. some chinese symbols)
    return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}

接下來,我們可以使用RNG生成一個特定長度的隨機字符串,包含通過我們的 useThisCharacter檢查的字符集。

public String generateRandomString(long length){
    //Since there is no native CharStream, we use an IntStream instead
    //and convert it to a Stream<Character> using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream<Character> randomCharStream = rng
    .ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT)
    .mapToObj(i -> (char)i).filter(c -> this::useThisCharacter)
    .limit(length);

    //now we can use this Stream to build a String utilizing the collect method.
    String randomString = randomCharStream
    .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
    .toString();
    return randomString;
}

關于Stream系列暫時完結

部分參考示圖源自:http://ifeve.com/stream/

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,951評論 19 139
  • 本文采用實例驅(qū)動的方式,對JAVA8的stream API進行一個深入的介紹。雖然JAVA8中的stream AP...
    浮梁翁閱讀 25,873評論 3 50
  • 來源:重走Java基礎之Streams(二)作者:知秋(極樂科技知乎專欄原創(chuàng)作者)博客:一葉知秋 接上篇重走Jav...
    極樂君閱讀 530評論 0 2
  • 第一章 為什么要關心Java 8 使用Stream庫來選擇最佳低級執(zhí)行機制可以避免使用Synchronized(同...
    謝隨安閱讀 1,518評論 0 4
  • 來源:重走Java基礎之Streams(三)作者:知秋(極樂科技知乎專欄原創(chuàng)作者)博客:一葉知秋 接重走Java基...
    極樂君閱讀 6,142評論 0 3