JAVA8 流 Stream 的使用

你可以把Java8的流看做花哨又懒惰的数据集迭代器。他们支持两种类型的操作:中间操作(e.g. filter, map)和终端操作(如count, findFirst, forEach, reduce). 中间操作可以连接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗类,产生一个最终结果。

JAVA8流的使用

详解Java8 Collect收集Stream的方法

collect就是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector接口来定义的。

案例:

最大值,最小值,平均值

// 为啥返回Optional? 如果stream为null怎么办, 这时候Optinal就很有意义了

 Optional<Dish> mostCalorieDish = dishes.stream().max(Comparator.comparingInt(Dish::getCalories)); 

Optional<Dish> minCalorieDish = dishes.stream().min(Comparator.comparingInt(Dish::getCalories)); 

Double avgCalories = dishes.stream().collect(Collectors.averagingInt(Dish::getCalories));

 IntSummaryStatistics summaryStatistics = dishes.stream().collect(Collectors.summarizingInt(Dish::getCalories)); 

double average = summaryStatistics.getAverage(); 

long count = summaryStatistics.getCount(); 

int max = summaryStatistics.getMax(); 

int min = summaryStatistics.getMin(); 

long sum = summaryStatistics.getSum();

连接收集器

String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining());    //直接连接

String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));   //逗号

list.stream().collect(Collectors.joings(",")) ;  //将List集合中的元素转换为一个流,然后将这些元素连接成一个以逗号分隔的字符串

toList

 //将原来的Stream映射为一个单元素流,然后收集为List

List<String> names = dishes.stream().map(Dish::getName).collect(toList()); 

toSet

//将Type收集为一个set,可以去重复

Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());                                            

toMap

 //Dish::getType:表示使用 Dish 对象的 getType() 方法作为 Map 的键。  d -> d:表示将每个 Dish 对象本身作为 Map 的值

Map<Type, Dish> byType = dishes.stream().collect(toMap(Dish::getType, d -> d));                  

有时候可能需要将一个数组转为map,做缓存,方便多次计算获取。toMap提供的方法k和v的生成函数。(注意,上述demo是一个坑,不可以这样用!!!, 请使用toMap(Function, Function, BinaryOperator)) 。

toMap(Function, Function, BinaryOperator) 是 Collectors.toMap() 方法的一个重载版本,用于在收集元素到 Map 的过程中处理重复键的情况。让我们来解释这三个参数的含义:

第一个参数 Function:这个函数指定了如何从元素中提取键。在这个参数中,你可以传入一个函数,用于从元素中提取键值。在这个例子中,Dish::getType 表示使用 Dish 对象的 getType() 方法作为键。

第二个参数 Function:这个函数指定了如何从元素中提取值。在这个参数中,你可以传入一个函数,用于从元素中提取值。在这个例子中,d -> d 表示将每个 Dish 对象本身作为值。

第三个参数 BinaryOperator:这个函数定义了当出现重复键时如何处理这些值。如果 Map 中已经存在相同的键,BinaryOperator 将被调用来决定如何合并现有值和新值。常见的操作包括覆盖现有值、保留现有值或者自定义合并逻辑。

//Dish::getType 用于提取键;Function.identity() 用于提取值,这里表示保留原始值;(existing, replacement) -> replacement 是一个 BinaryOperator,它表示当出现重复键时,选择新值覆盖旧值。

Map<Type, Dish> byType = dishes.stream().collect(Collectors.toMap(Dish::getType, Function.identity(), (existing, replacement) -> replacement));

// (existing, replacement) -> existing 是一个 BinaryOperator,它表示当出现重复键时,选择保留已经存在的值而不覆盖。

Map<Type, Dish> byType = dishes.stream() .collect(Collectors.toMap(Dish::getType, Function.identity(), (existing, replacement) -> existing));


自定义收集器

自定义归约reducing

前面几个都是reducing工厂方法定义的归约过程的特殊情况,其实可以用Collectors.reducing创建收集器。比如,求和

// reducing收集器是一个可以用来累加元素的收集器,它接受三个参数:初始值、转换函数和累加器函数。

Integer totalCalories = dishes.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

//使用内置函数代替箭头函数

Integer totalCalories2 = dishes.stream().collect(reducing(0, Dish::getCalories, Integer::sum));

// reducing除了接收一个初始值,还可以把第一项当作初始值

Optional<Dish> mostCalorieDish = dishes.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));


当然也可以直接使用reduce

// map(Dish::getCalories)将每个菜肴对象映射为其热量值

Optional<Integer> totalCalories3 = dishes.stream().map(Dish::getCalories).reduce(Integer::sum);


虽然都可以,但考量效率的话,还是要选择下面这种

intsum = dishes.stream().mapToInt(Dish::getCalories).sum();

reducing

关于reducing的用法比较复杂,目标在于把两个值合并成一个值。

public static<T, U>

  Collector<T, ?, U> reducing(U identity,

                Function<? superT, ? extendsU> mapper,

                BinaryOperator<U> op)

首先看到3个泛型,

U是返回值的类型,比如上述demo中计算热量的,U就是Integer。

关于T,T是Stream里的元素类型。由Function的函数可以知道,mapper的作用就是接收一个参数T,然后返回一个结果U。对应demo中Dish。

?在返回值Collector的泛型列表的中间,这个表示容器类型,一个收集器当然需要一个容器来存放数据。这里的?则表示容器类型不确定。事实上,在这里的容器就是U[]。

关于参数:

identity是返回值类型的初始值,可以理解为累加器的起点。

mapper则是map的作用,意义在于将Stream流转换成你想要的类型流。

op则是核心函数,作用是如何处理两个变量。其中,第一个变量是累积值,可以理解为sum,第二个变量则是下一个要计算的元素。从而实现了累加。

reducing还有一个重载的方法,可以省略第一个参数,意义在于把Stream里的第一个参数当做初始值。

public static<T> Collector<T, ?, Optional<T>>

  reducing(BinaryOperator<T> op)

先看返回值的区别,T表示输入值和返回值类型,即输入值类型和输出值类型相同。还有不同的就是Optional了。这是因为没有初始值,而第一个参数有可能是null,当Stream的元素是null的时候,返回Optional就很意义了。

再看参数列表,只剩下BinaryOperator。BinaryOperator是一个三元组函数接口,目标是将两个同类型参数做计算后返回同类型的值。可以按照1>2? 1:2来理解,即求两个数的最大值。求最大值是比较好理解的一种说法,你可以自定义lambda表达式来选择返回值。那么,在这里,就是接收两个Stream的元素类型T,返回T类型的返回值。用sum累加来理解也可以。

上述的demo中发现reduce和collect的作用几乎一样,都是返回一个最终的结果,比如,我们可以使用reduce实现toList效果:

//手动实现toListCollector --- 滥用reduce, 不可变的规约---不可以并行

List<Integer> calories = dishes.stream().map(Dish::getCalories)

    .reduce(newArrayList<Integer>(),

        (List<Integer> l, Integer e) -> {

          l.add(e);

          returnl;

        },

        (List<Integer> l1, List<Integer> l2) -> {

          l1.addAll(l2);

          returnl1;

        }

    );

关于上述做法解释一下。

<U> U reduce(U identity,

         BiFunction<U, ? superT, U> accumulator,

         BinaryOperator<U> combiner);

U是返回值类型,这里就是List

BiFunction accumulator是是累加器,目标在于累加值和单个元素的计算规则。这里就是List和元素做运算,最终返回List。即,添加一个元素到list。

BinaryOperator combiner是组合器,目标在于把两个返回值类型的变量合并成一个。这里就是两个list合并。

这个解决方案有两个问题:一个是语义问题,一个是实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变归约。相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。错误的语义来使用reduce方法还会造成一个实际问题:这个归约不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。这就是collect适合表达可变容器上的归约的原因,更关键的是它适合并行操作。

总结:reduce适合不可变容器归约,collect适合可变容器归约。collect适合并行

分组

数据库中经常遇到分组求和的需求,提供了group by原语。在Java里, 如果按照指令式风格(手动写循环)的方式,将会非常繁琐,容易出错。而Java8则提供了函数式解法。

比如,将dish按照type分组。和前面的toMap类似,但分组的value却不是一个dish,而是一个List。

Map<Type, List<Dish>> dishesByType = dishes.stream().collect(groupingBy(Dish::getType));

这里

publicstatic<T, K> Collector<T, ?, Map<K, List<T>>>

  groupingBy(Function<? superT, ? extendsK> classifier)

参数分类器为Function,旨在接收一个参数,转换为另一个类型。上面的demo就是把stream的元素dish转成类型Type,然后根据Type将stream分组。其内部是通过HashMap来实现分组的。groupingBy(classifier, HashMap::new, downstream);

除了按照stream元素自身的属性函数去分组,还可以自定义分组依据,比如根据热量范围分组。

既然已经知道groupingBy的参数为Function, 并且Function的参数类型为Dish,那么可以自定义分类器为:

private CaloricLevel getCaloricLevel(Dish d) {

  if(d.getCalories() <= 400) {

   returnCaloricLevel.DIET;

  } elseif(d.getCalories() <= 700) {

   returnCaloricLevel.NORMAL;

  } else{

   returnCaloricLevel.FAT;

  }

}

再传入参数即可

Map<CaloricLevel, List<Dish>> dishesByLevel = dishes.stream().collect(groupingBy(this::getCaloricLevel));

多级分组

groupingBy还重载了其他几个方法,比如

publicstatic<T, K, A, D>

  Collector<T, ?, Map<K, D>> groupingBy(Function<? superT, ? extendsK> classifier,

                     Collector<? superT, A, D> downstream)

泛型多的恐怖。简单的认识一下。classifier还是分类器,就是接收stream的元素类型,返回一个你想要分组的依据,也就是提供分组依据的基数的。所以T表示stream当前的元素类型,K表示分组依据的元素类型。第二个参数downstream,下游是一个收集器Collector. 这个收集器元素类型是T的子类,容器类型container为A,reduction返回值类型为D。也就是说分组的K通过分类器提供,分组的value则通过第二个参数的收集器reduce出来。正好,上个demo的源码为:

public static<T, K> Collector<T, ?, Map<K, List<T>>>

  groupingBy(Function<? superT, ? extendsK> classifier) {

    returngroupingBy(classifier, toList());

  }

将toList当作reduce收集器,最终收集的结果是一个List<Dish>, 所以分组结束的value类型是List<Dish>。那么,可以类推value类型取决于reduce收集器,而reduce收集器则有千千万。比如,我想对value再次分组,分组也是一种reduce。

//多级分组

Map<Type, Map<CaloricLevel, List<Dish>>> byTypeAndCalory = dishes.stream().collect(

  groupingBy(Dish::getType, groupingBy(this::getCaloricLevel)));

byTypeAndCalory.forEach((type, byCalory) -> {

 System.out.println("----------------------------------");

 System.out.println(type);

 byCalory.forEach((level, dishList) -> {

  System.out.println("\t"+ level);

  System.out.println("\t\t"+ dishList);

 });

});

验证结果为:

----------------------------------

FISH

  DIET

    [Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]

  NORMAL

    [Dish(name=salmon, vegetarian=false, calories=450, type=FISH)]

----------------------------------

MEAT

  FAT

    [Dish(name=pork, vegetarian=false, calories=800, type=MEAT)]

  DIET

    [Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]

  NORMAL

    [Dish(name=beef, vegetarian=false, calories=700, type=MEAT)]

----------------------------------

OTHER

  DIET

    [Dish(name=rice, vegetarian=true, calories=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calories=120, type=OTHER)]

  NORMAL

    [Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]

总结:groupingBy的核心参数为K生成器,V生成器。V生成器可以是任意类型的收集器Collector。

比如,V生成器可以是计算数目的, 从而实现了sql语句中的select count(*) from table A group by Type

Map<Type, Long> typesCount = dishes.stream().collect(groupingBy(Dish::getType, counting()));

System.out.println(typesCount);

-----------

{FISH=2, MEAT=3, OTHER=4}

sql查找分组最高分select MAX(id) from table A group by Type

Map<Type, Optional<Dish>> mostCaloricByType = dishes.stream()

    .collect(groupingBy(Dish::getType, maxBy(Comparator.comparingInt(Dish::getCalories))));

这里的Optional没有意义,因为肯定不是null。那么只好取出来了。使用collectingAndThen

Map<Type, Dish> mostCaloricByType = dishes.stream()

  .collect(groupingBy(Dish::getType,

    collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));

到这里似乎结果出来了,但IDEA不同意,编译黄色报警,按提示修改后变为:

Map<Type, Dish> mostCaloricByType = dishes.stream()

  .collect(toMap(Dish::getType, Function.identity(),

    BinaryOperator.maxBy(comparingInt(Dish::getCalories))));

是的,groupingBy就变成toMap了,key还是Type,value还是Dish,但多了一个参数!!这里回应开头的坑,开头的toMap演示是为了容易理解,真那么用则会被搞死。我们知道把一个List重组为Map必然会面临k相同的问题。当K相同时,v是覆盖还是不管呢?前面的demo的做法是当k存在时,再次插入k则直接抛出异常:

java.lang.IllegalStateException: Duplicate key Dish(name=pork, vegetarian=false, calories=800, type=MEAT)

  at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)

正确的做法是提供处理冲突的函数,在本demo中,处理冲突的原则就是找出最大的,正好符合我们分组求最大的要求。(真的不想搞Java8函数式学习了,感觉到处都是性能问题的坑)

继续数据库sql映射,分组求和select sum(score) from table a group by Type

Map<Type, Integer> totalCaloriesByType = dishes.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));

然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接收特定类型元素的收集器适应不同类型的对象。我么来看一个使用这个收集器的实际例子。比如你想得到,对于每种类型的Dish,菜单中都有哪些CaloricLevel。我们可以把groupingBy和mapping收集器结合起来,如下所示:

Map<Type, Set<CaloricLevel>> caloricLevelsByType = dishes.stream().collect(groupingBy(Dish::getType, mapping(this::getCaloricLevel, toSet())));

这里的toSet默认采用的HashSet,也可以手动指定具体实现toCollection(HashSet::new)

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称为分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组:true or false. 例如,如果你是素食者,你可能想要把菜单按照素食和非素食分开:

Map<Boolean, List<Dish>> partitionedMenu = dishes.stream().collect(partitioningBy(Dish::isVegetarian));

当然,使用filter可以达到同样的效果:

List<Dish> vegetarianDishes = dishes.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

分区相对来说,优势就是保存了两个副本,当你想要对一个list分类时挺有用的。同时,和groupingBy一样,partitioningBy一样有重载方法,可以指定分组value的类型。

Map<Boolean, Map<Type, List<Dish>>> vegetarianDishesByType = dishes.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));

Map<Boolean, Integer> vegetarianDishesTotalCalories = dishes.stream().collect(partitioningBy(Dish::isVegetarian, summingInt(Dish::getCalories)));

Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = dishes.stream().collect(partitioningBy(Dish::isVegetarian,collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));

作为使用partitioningBy收集器的最后一个例子,我们把菜单数据模型放在一边,来看一个更加复杂也更为有趣的例子:将数组分为质数和非质数。

首先,定义个质数分区函数:

private booleanisPrime(intcandidate) {

  int candidateRoot = (int) Math.sqrt((double) candidate);

  return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);

}

然后找出1到100的质数和非质数

Map<Boolean, List<Integer>> partitionPrimes = IntStream.rangeClosed(2, 100).boxed().collect(partitioningBy(this::isPrime));

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

推荐阅读更多精彩内容