重走 Java 基础之 Streams(四)

1,073 阅读10分钟
接上篇重走Java基础之Streams (三)

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

当你有一个Stream,你需要映射转换但是想保留初始值,你可以使用下面的实用程序方法将’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));
}

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

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

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

将迭代器转换为流

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

基于流来创建一个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.toMap在 javadoc 的描述:

如果映射的键包含重复的(根据Object.equals(Object)),则会在执行收集操作时会抛出IllegalStateException。 如果映射的键可能有重复,请使用toMap(Function,Function,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生成在发生重复冲突情况下要存储的值。 它可以:

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

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

  • 组合旧值和新值


按值分组

当你需要执行等效的一个数据库级联“group by”操作(意思就是和此效果一样的需求)时你可以使用 Collectors.groupingBy 。 为了说明,以下内容创建了一个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

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

Java 8提供了IntSummaryStatisticsDoubleSummaryStatisticsLongSummaryStatistics这些类,它们给出用于收集统计数据对象的状态,例如count,min,max,sum和average。

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

一个经常遇到的用例是从流创建一个String,其中每个流转换出的字符串之间由一个特定的字符分隔。 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

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

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

这相当于(((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<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中 —- 如果在空流上调用,reduce()将返回identity元素。

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

这个reduction(聚合操作)等同于写((1+2)+(3+4))。 关联性的属性还允许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版本,它使用比较器作为其参数。

此外,您可以使用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()

流操作类别

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

  • 中间操作

一个中间操作总是 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”(用于ints),LongStream(用于longs)和DoubleStream(用于doubles)提供专用的Stream。 除了是针对它们各自的原语的优化实现,它们还提供了几个特定的终端方法,通常用于数学运算。 例如:

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

将流的结果收集到数组中

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

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

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

可以找到符合条件的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或在注册应用程序后的初始密码。 这可以很容易地使用Streams实现。

首先,我们需要初始化一个随机数生成器。 为了增强生成的Strings的安全性,使用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<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系列暂时完结

部分参考示图源自:并发编程网 - ifeve.com

往期回顾:

重走Java基础之Streams(三)

重走Java基础之Streams(二)

重走Java基础之Streams(一)
  • 原文链接:一叶知秋
  • 作者:知秋
  • [ 转载请保留原文出处、作者。]