重走Java基础之Streams(四)

来源:重走Java基础之Streams(四)
作者:知秋
博客:一叶知秋
转载请注明来源和作者!


接上篇重走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.toMapjavadoc的描述:

如果映射的键包含重复的(根据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这些类,它们给出用于收集统计数据对象的状态,例如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<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

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

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

这相当于(((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系列暂时完结

部分参考示图源自:http://ifeve.com/stream/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,672评论 18 139
  • 本文采用实例驱动的方式,对JAVA8的stream API进行一个深入的介绍。虽然JAVA8中的stream AP...
    浮梁翁阅读 25,757评论 3 50
  • 来源:重走Java基础之Streams(二)作者:知秋(极乐科技知乎专栏原创作者)博客:一叶知秋 接上篇重走Jav...
    极乐君阅读 519评论 0 2
  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,493评论 0 4
  • 来源:重走Java基础之Streams(三)作者:知秋(极乐科技知乎专栏原创作者)博客:一叶知秋 接重走Java基...
    极乐君阅读 6,138评论 0 3