使用 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.toMap
在 javadoc
的描述:
如果映射的键包含重复的(根据 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]}
查找有关数值流的统计信息
Java 8 提供了IntSummaryStatistics
,DoubleSummaryStatistics
和 LongSummaryStatistics
这些类,它们给出用于收集统计数据对象的状态,例如 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 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.
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
:
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:
这个 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/
往期回顾:
-
原文链接:一叶知秋
-
作者:知秋
-
[ 转载请保留原文出处、作者。]
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于