重走 Java 基础之 Streams(四)

本贴最后更新于 2627 天前,其中的信息可能已经沧海桑田

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

使用 Map.Entry 的流在转换后保留初始值

当你有一个 Stream,你需要映射转换但是想保留初始值,你可以使用下面的实用程序方法将'Stream` 映射到 Map.Entry:

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

然后你可以使用你的有权访问原始值和映射转换后值的转换器来处理 Stream s:

Set mySet;
Function transformer = SomeClass::transformerMethod;
Stream> entryStream = mySet.stream()
    .map(entryMapper(transformer));

然后,您可以继续正常处理 Stream。 这避免了创建中间集合的开销。

将迭代器转换为流

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

基于流来创建一个 map

没有重复键的简单情况

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

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

可能存在重复键的情况

Collectors.toMapjavadoc
的描述:

如果映射的键包含重复的(根据 Object.equals(Object)),则会在执行收集操作时会抛出 IllegalStateException。 如果映射的键可能有重复,请使用 toMap(Function,Function,BinaryOperator)。

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

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

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

传递给 Collectors.toMap(...)BinaryOperator 生成在发生重复冲突情况下要存储的值。 它可以:

  • 返回旧值,以流中的第一个值优先,

  • 返回新值,以流中的最后一个值优先,

  • 组合旧值和新值

    按值分组

当你需要执行等效的一个数据库级联“group by”操作(意思就是和此效果一样的需求)时你可以使用 Collectors.groupingBy 。 为了说明,以下内容创建了一个 map,其中人们的姓名分别映射到姓氏:

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

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

查找有关数值流的统计信息

Java 8 提供了IntSummaryStatisticsDoubleSummaryStatisticsLongSummaryStatistics这些类,它们给出用于收集统计数据对象的状态,例如 countminmaxsumaverage

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 中包含的元素个数小于 N,那么返回空 Stream;

limit: 对一个 Stream 进行截断操作,获取其前 N 个元素,如果原 Stream 中包含的元素个数小于 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 slice = collection.stream().skip(n).limit(maxSize);

Notes:

  • 如果 n 为负或 maxSize 为负,则抛出 IllegalArgumentException
  • skip(long)limit(long) 都是中间操作
  • 如果流包含少于 n 个元素,则 skip(n)将返回一个空流
  • skip(long)limit(long) 都是顺序流管道上的廉价操作,但在有序并行管道上可能相当昂贵(指性能上)

再贴个例子:

List 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

一个经常遇到的用例是从流创建一个 String,其中每个流转换出的字符串之间由一个特定的字符分隔。 Collectors.joining() 方法可以用于这个,就像下面的例子:

Stream 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

聚合是将二进制操作应用于流的每个元素以产生一个值的过程。

IntStreamsum() 方法是一个简化的例子; 它对流的每个项应用加法,得到一个最终值:Sum Reduction

这相当于 (((1+2)+3)+4)

Stream 的 reduce 方法允许创建自定义 reduction。 可以使用 reduce 方法来实现 sum() 方法:

IntStream istr;

//Initialize istr

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

返回 Optional 对象,以便可以恰当地处理空的 Streams。

reduction 的另一个示例是将 Stream> 组合成单个 LinkedList

Stream> listStream;

//Create a Stream>

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

您还可以提供* identity 元素*。 例如,用于加法的标识元素为 0,如 x + 0 == x。 对于乘法,identity 元素为 1,如 x * 1 == x。 在上面的例子中,identity 元素是一个空的 LinkedList,因为如果你将一个空列表添加到另一个列表,你“添加”的列表不会改变:

Stream> listStream;

//Create a Stream>

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

注意,当提供一个 identity 元素时,返回值不会被包装在一个 Optional 中 ---- 如果在空流上调用,reduce() 将返回 identity 元素。

二元运算符也必须是* associative *,意思是 (a+b)+c==a+(b+c)。 这是因为元件可以以任何顺序进行聚合操作(reduced)。 例如,可以如下执行上述加法 reduction:

Other sum reduction

这个 reduction(聚合操作)等同于写 ((1+2)+(3+4))。 关联性的属性还允许 Java 并行地 reduction Stream - 每个处理器可以 reduction Stream 的一部分并得到结果,最后通过 reduction 结合每个处理器的结果。

使用流排序

List 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 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版本,它使用比较器作为其参数。

此外,您可以使用 lambda 表达式进行排序:

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

这将输出 [Sydney, New York, Mumbai, London, California, Amsterdam]

你可以使用 Comparator.reverseOrder() ,一个对自然排序进行强行 reverse 的比较器(反排序)。

List reverseSortedData = data.stream().sorted(Comparator.reverseOrder()

流操作类别

流操作分为两个主要类别,中间和终端操作,以及两个子类,无状态和有状态。


中间操作

一个中间操作总是* lazy *(延迟执行),例如一个简单的“Stream.map”。 它不会被调用,直到流实际上消耗。 这可以很容易地验证:

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

中间操作是流的常见构造块,指在数据源之后操作链,并且通常末端跟随有触发流链式执行的终端操作。


终端操作

终端操作是触发流的消耗的。 一些最常见的是 Stream.forEach 或“ Stream.collect。 它们通常放置在一系列中间操作之后,几乎总是* eager *。


无状态操作

无状态意味着每个环节(可以理解成流的每个处理环节)在没有其他环节的上下文的情况下被处理。 无状态操作允许流的存储器高效处理。 像 Stream.map 和 Stream.filter 这样的不需要关于流的其他环节的信息的操作被认为是无状态的。


状态操作

状态性意味着对每个环节的操作取决于(一些)流的其他环节。 这需要保留一个状态。 状态操作可能会与长流或无限流断开。 像 Stream.sorted 这样的操作要求在处理任何环节之前处理整个流,这将在足够长的流的环节中断开。 这可以通过长流(run at your own risk)来证明(说的太拗口了,其实就是栈的递归操作,下一步的运行依靠上一步的结果来执行,假如太深,就可能出现问题,看下面例子就知道了):

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

这将导致由于 Stream.sorted 的状态的内存不足:

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

原始流

Java 为三种类型的原语“IntStream”(用于 int s),LongStream(用于 long s)和 DoubleStream(用于 double s)提供专用的 Stream。 除了是针对它们各自的原语的优化实现,它们还提供了几个特定的终端方法,通常用于数学运算。 例如:

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

将流的结果收集到数组中

可以通过 Stream.toArray() 方法获得一个数组:

List 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 是一种特殊的方法引用:构造函数引用。

查找匹配条件的第一个元素

可以找到符合条件的 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 将继续检查每个元素,直到找到一个结果。 在一个有限的 Stream,如果 Java 运行检查了所以元素,但仍然找不到一个结果,它返回一个空的 OptionalInt 对象。

使用 Streams 生成随机字符串

有时,创建随机的 Strings 有时是有用的,或许作为 Web 服务的会话 ID 或在注册应用程序后的初始密码。 这可以很容易地使用 Stream s 实现。

首先,我们需要初始化一个随机数生成器。 为了增强生成的 String s 的安全性,使用 SecureRandom 是一个好主意。

Note:创建一个 SecureRandom 是相当消耗资源的,所以最好的做法是只做一次,并且不时地调用它的一个 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

当创建随机的 String 时,我们通常希望它们只使用某些字符(例如,只有字母和数字)。 因此,我们可以创建一个返回一个 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 using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream 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/

往期回顾:

重走 Java 基础之 Streams(三)

重走 Java 基础之 Streams(二)

重走 Java 基础之 Streams(一)

  • 原文链接:一叶知秋

  • 作者:知秋

  • [ 转载请保留原文出处、作者。]

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...