[Java进阶篇][函数式编程][Java 8+ Stream API]

[Java进阶篇][函数式编程][Java 8+ Stream API]

  • [Java进阶篇][函数式编程][Java 8+ Stream API]
    • 序言
    • Stream 是什么?
    • 创建流
    • 操作流
      • 迭代
        • forEach()方法
        • peek()方法
      • 映射
        • map()方法
        • flatMap()方法
      • 过滤
        • filter()方法
        • sort() 排序方法
        • distinct()方法
        • limit() 截断方法
        • skip() 跳过方法
      • 级联调用
      • 归约
        • reduce() 方法
        • count() 计数方法
      • 收集
        • Collect() 收集器方法
          • 高级生成
            • 分组
            • 分区
            • 多级收集
          • 归约
          • 收集后操作
      • 查找和匹配
        • find*() 寻找方法
        • anyMatch() 任意匹配方法
        • allMatch() 全匹配方法
        • noneMatch() 无匹配方法
        • takeWhile() 达标即获取方法
        • dropWhile() 达标即抛弃方法
      • 并行
        • parallel() 并行计算方法
      • 其他
        • unorder()无序结果方法
      • 自动生成
        • Stream.concat() 平铺
        • Stream.iterate() 迭代
        • Stream.generate() 生成
    • 结束语

序言

Java 8 以后 Java也引入了大量函数式编程的内容
从完全的命令式编程到加入一些函数式编程语言的特性

为了更好的帮助广大未接触过函数式编程的程序员使用这些特性
Java 8以后提供了Stream API
(虽然相比C# linq是差了不少 但对于广大长期停留在 1.5 1.6特性的老Javaer已经很好用了)

上篇文章介绍了Java8 以后Java引入了Lambda表达式
而Java8中的Stream与lambda表达式可以说是相伴相生的
通过Stream我们可以更友好的更为流畅更为语义化的操作集合
而操作的过程就要通过调用行为表达式(Lambada)来进行
这就更接近函数式编程 而不是传统的命令式

Stream api都位于java.util.stream包
其中就包含了最核心的Stream接口
一个Stream实例可以非常方便的以串行或者并行操作一组元素序列
大大降低了并发操作的难度

Stream 是什么?

Stream是一种可供流式操作的数据视图 有些类似数据库中视图的概念
它不改变源数据集合 如果对其进行改变的操作 它会返回
一个新的数据集合

所以很多人说 Stream的名字起的太不好了 容易和IO Stream搞混
不过木已成舟 我们要做的就是接受 并且尽可能的掌握

Stream具体有什么新特性 这类文章在网上也已经有很多
此处不再赘述
有需要的可以看看这些文章
Java Stream API入门篇

Java 除了基础Stream之外 还提供了三种其他数据类型的Stream

Stream不能使用构造器创建 因为其数据源多来自集合类
输入输出流,数组等 所以Stream一般使用工具方法创建
而Java 8 以后对于 这类常用的数据集合 已经添加了Stream
的创建方法

比如Arrays工具类的 Arrays.Stream方法

public static Stream stream(T[] array)

比如Collection接口的 stream方法

default Stream stream()

Stream接口内置的工具方法

public static Stream of(T… values)

创建流

  • 通过Stream接口的工具方法创建
Stream<String> stringStream=Stream.of("Apple", "Banana", "Coconut", "Damson");

IntStream intStream=IntStream.of(1,3,5,7,9);

LongStream longStream=LongStream.of(2l,4l,6l,8l);

DoubleStream doubleStream=DoubleStream.of(10d,20d,30d,40d,50d);
  • 通过Arrays工具类的Arrays.Stream方法从数组上创建
        Stream<String> stringStream=Arrays.stream(new String[]{"Saber","Lancer","Archer"});
  • 通过Collection接口的stream方法从集合上创建
 Stream<String> stream=list.stream();

这种创建方式也是最常用的方式 因为绝大部分流式操作都是用于对集合的操作

操作流

stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations)
所有的流操作会被组合到一个流式操作管道中,这点类似linux中的管道概念,将多个简单操作连接在一起组成一个功能强大的操作。一个流式操作管道首先会有一个数据源,这个数据源可能是数组、集合、生成器函数或是IO通道,流操作过程中并不会修改源中的数据;然后还有零个或多个中间操作,每个中间操作会将接收到的流转换成另一个流(比如filter);最后还有一个终止操作,会生成一个最终结果(比如sum)。流是一种惰性操作,所有对源数据的计算只在终止操作被初始化的时候才会执行。

总结下
1. 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,不会进行实际的数据变更
2. 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数,也就是说减少了内存占用。计算完成之后stream就会失效。

迭代

forEach()方法

Stream中最常用的一个方法 就是 forEach()方法
方法签名

void forEach(Consumer< ? super T> action);

遍历集合 对每个元素执行 action动作

终结操作

这个方法可以在很多场合取代for循环

传统for循环

  List list=...

  for (String s : list) {
            System.out.println(s);       
        }

使用forEach

        list.stream().forEach(x-> System.out.println(x));

相比for循环 forEach的最大优势是可以和其他Stream级联调用
形成一组联合的语义化的操作

forEach是一个终结操作 所以被调用后Stream就被消费掉了
如果想在完成一轮迭代后还可以使用这个流 那么可以使用peek方法

peek()方法

方法签名

Stream peek(Consumer< ? super T> action);
可以看到peek的入参和forEach一样 就是 返回不同 peek方法仍返回一个Stream
而forEach作为终结方法 无返回值

中间操作

List.stream().peek(x-> {
            System.out.println(x);
        }).collect(Collectors.toList());

可以在迭代输出一遍流内容后 继续收集流到新的列表中

要注意的是peek不能和count,findFirst,anyMatc等短路方法连用,和短路方法连用时
peek会不起作用

映射

map()方法

另一个较常见的操作就是map方法
方法签名

Stream map(Function< ? super T, ? extends R> mapper);

包装操作(mapper)方法
作用是 遍历集合 并对所有元素执行mapper包装操作
然后返回 操作的结果元素组成一个新的Stream

简单的说,就是对每个元素按照某种操作进行转换,
转换前后Stream中元素的个数不会改变,
但元素的类型取决于转换之后的类型。

和forEach看起来挺相似的
但map的优势是其返回Stream
所以可以继续级联调用其他操作

中间操作

示例

        创建一个新String
        List stringlist=list.stream()
                        .map(x->x.toUpperCase()).collect(Collectors.toList());

        级联调用forEach()方法
        list.stream().map(x->x.toUpperCase()).forEach(x-> System.out.println(x));

flatMap()方法

平铺并包装的方法
方法签名

Stream flatMap(Function< ? super T, ? extends Stream< ? extends R>> mapper)

作用是 可以把集合中的复杂结构数据 拆分成单个数据 便于继续处理
类似把原stream中的所有元素都”摊平”之后组成的Stream,转换前后元素的个数和类型都可能会改变。

中间操作

示例

        List fruitList=Stream
                .of("apple","banana","coconut","Damson","Filbert","Lemon")
                .collect(Collectors.toList());

        List meatList=Stream
                .of("beef","pork","chicken","lamp","oyster","salmon","tuna")
                .collect(Collectors.toList());


        List> bigList=Stream.of(fruitList,meatList).collect(Collectors.toList());

        bigList.stream().flatMap(x->x.stream()).forEach(System.out::println);

输出结果

apple
banana
coconut
Damson
Filbert
Lemon
beef
pork
chicken
lamp
oyster
salmon
tuna

平铺后进行深层操作

bigList.stream().flatMap(x->x.stream().map(y->"eat "+y)).forEach(System.out::println);

结果

eat apple
eat banana
eat coconut
eat Damson
eat Filbert
eat Lemon
eat beef
eat pork
eat chicken
eat lamp
eat oyster
eat salmon
eat tuna

当然这样的操作和一下代码是等效的

   bigList.stream().flatMap(x->x.stream()).map(y->"eat "+y).forEach(System.out::println);

除了对应Stream的flatMap()方法外
Java还提供了对应其他几个流的方法

方法 原始流 转换流
flatMapToInt Stream IntStream
flatMapToLong Stream LongStream
flatMapToDouble Stream DoubleStream

过滤

filter()方法

过滤方法 方法签名

Stream filter(Predicate< ? super T> predicate);

入参是一个断言式函数接口
此方法的作用是 遍历集合 对集合中的每一个参数执行判断操作
符合的元素 加入到结果集中 并返回包含结果集的新Stream

中间操作

  List list=Stream.of("apple","banana","coconut","Damson","Filbert","Lemon").collect(Collectors.toList());

        list.stream().filter(x->x.contains("a")).forEach(x-> System.out.println(x));

结果

apple
banana
Damson

sort() 排序方法

对流中元素进行排序 要求流中元素元素必须实现比较接口Comparable或者显式的传入比较器Comparator
中间操作

List integerList=Stream
               .of(215,3308,7748956,-8,-567,12,9,453).boxed().collect(Collectors.toList());

integerList.stream().sorted().forEach(System.out::println);

因为流中的元素已经实现了比较接口 所以无需显式的传入比较器
结果

-567
-8
9
12
215
453
3308
7748956

distinct()方法

去重方法 作用是返回一个去除重复元素之后的Stream
源码中注释提到 此方法调用时最好先排序 排序后调用的时间复杂度是固定的
很明显 这个方法 去重的比较是调用 元素的 .equals()方法的

中间操作

List integerList=Stream.of(1,2,2,4,6,1,7,8,9,10,6,6,2,3).collect(Collectors.toList());

        integerList.stream().sorted().distinct().forEach(System.out::println);
        //结果1246789103

limit() 截断方法

截断当前流,返回包含前n个元素的流,当集合大小小于n时,则返回实际长度

  fruitList.stream().limit(3).forEach(System.out::print);
  //apple banana coconut

skip() 跳过方法

跳过流中的n个元素 如果n大于流的长度,则会返回一个空的集合。
中间操作

fruitList.stream().skip(3).forEach(System.out::print);
// Damson Filbert Lemon

级联调用

Stream流式操作的一大优势就是可以级联调用

上面的的bigList

bigList.stream()
.flatMap(x->x.stream())
.sorted().distinct()
.filter(y->y.length()<10)
.forEach(System.out::println);

结果

eat Lemon
eat apple
eat beef
eat lamp
eat pork
eat tuna

这样操作 就将两个集合中的所有元素 进行了平铺 包装 排序 去重 过滤 最后打印到控制台的一系列操作

如果使用for循环来写

List<List<String>> bigList=Stream.of(fruitList,meatList).collect(Collectors.toList());
        List<String> newList=new ArrayList<>(fruitList.size()+meatList.size());
        for (List<String> subList : bigList) {
            for (String s : fruitList) {
                if(!newList.contains(s))
                {
                    String str="eat "+s;
                    if(str.length()<10)
                    {
                        newList.add("eat "+s);
                    }
                }
            }
        }
        Collections.sort(newList);
        newList.forEach(System.out::println);

可以看到就复杂了很多 而且其中大量的命令式语句的语义很不明显
需要读者去仔细观察 思考 才知道到底在做什么

归约

reduce() 方法

这其实是个函数式编程概念,现在很火的大数据 应该都听过有个Map(映射)-Reduce(规约)操作
这里的reduce规约方法 其实和大数据里的reduce是很类似的
Map处理所有结合中的单一元素 而reduce将map处理过后的元素按规则组合起来 最后得到一个较小的结果
也可以称之为聚合操作

中间操作

Stream提供的reduce方法 作用是把元素组合起来。提供一个起始值(种子),然后依照运算规则(BinaryOperator),
和前面 Stream 的第一个、第二个、第 n 个元素组合。
因此可以这样理解,字符串拼接、数值的 SUM MAX MIN COUNTaverage 都是特殊的 reduce。

reduce()的方法定义有三种重写形式:

Optional< T> reduce(BinaryOperator< T> accumulator)
T reduce(T identity, BinaryOperator< T> accumulator)
< U> U reduce(U identity, BiFunction< U, ? super T,U> accumulator, BinaryOperator combiner)

三个重载参数不一样 但前两个还是很好理解的

第一个参数为Optional reduce(BinaryOperator accumulator)的规约操作
从Stream中的第一个参数器 逐个轮换执行accumulator的二元操作行为 最后返回一个非空容器Optional


        fruitList.stream().reduce((last,next)->last+" "+next).ifPresent(System.out::println);
        //结果 apple banana coconut Damson Filbert Lemon

代码上可见 reduce()方法入参传入了一个二元操作行为accumulator 内容是 字符串拼接 前元素+空格+后元素
作用自然是将集合中的所有元素 “apple”,”banana”,”coconut”,”Damson”,”Filbert”,”Lemon”
依次拼接起来 并且每个元素之前添加了一个空格

第二个参数为T reduce(T identity, BinaryOperator accumulator) 的规约操作 以指定的元素 T identity
为首元素 以Stream中的第一个元素为规约操作次元素 然后轮换执行accumulator的二元操作行为
因为有初始值 所以不可能为空 最后没有返回非空容器 而是直接返回值

 System.out.println(fruitList.stream().reduce("I like",(last,next)->last+" "+next));

//结果I like apple banana coconut Damson Filbert Lemon

代码上可见 reduce()方法入参传入了两个参数 一个初始元素字符串 “I like”,
一个二元操作行为accumulator 内容是 字符串拼接 前元素+空格+后元素
作用自然是将集合中的所有元素 “apple”,”banana”,”coconut”,”Damson”,”Filbert”,”Lemon” 依次拼接起来
并且每个元素之前添加了一个空格,同时在 拼接集合中的所有元素之前 添加了一个初始元素 “I like”
初始元素再拼接后面的元素

除了简单的字符串拼接 规约操作能做的很有很多复杂的功能 所以被称为Stream流中的多面手

求长度最大的字符串元素

bigList.stream().flatMap(x->x.stream()).reduce((x,y)->x.length()>y.length()?x:y).ifPresent(System.out::println);
//chicken

求最大值

List integerList=IntStream
        .of(215,3308,7748956,-8,-567,12,9,453).boxed().collect(Collectors.toList());

integerList.stream().reduce((p1,p2)->p1.compareTo(p2)>=0?p1:p2).ifPresent(System.out::println);

//等价于 max()方法
integerList.stream().max(Integer::compareTo).ifPresent(System.out::println);

结果 7748956
求最小值

  integerList.stream().reduce((p1,p2)->p1.compareTo(p2)>=0?p2:p1).ifPresent(System.out::println);

//等价于 min()方法
integerList.stream().max(Integer::compareTo).ifPresent(System.out::println);

结果 -567

第三个参数为 U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 的规约操作 就比较复杂了

此方法是提供给并行流使用的,并行流的底层是基于Java Fork/Join框架的分支策略
把流分为多个分片同时并行处理,最后合并为结果。

[Java进阶篇][函数式编程][Java 8+ Stream API]_第1张图片
那么在这个重载的reduce方法中 前两个参数 和第二个方法并无区别
U identity即为规约初始值,BiFunction accumulator仍为二元操作 用于传入聚合操作的行为

而第三个参数BinaryOperator combiner则是合并器 用于指明 并行处理下
每个迭代分片的数据 如何聚合到总的数据中

     String result=bigList.stream().flatMap(x->x.stream())
                .reduce("I like eat ",(a,b)->a+b+" and ",(p1,p2)->p1+p2);

        System.out.println(result);

结果

I like eat apple and banana and coconut and Damson and Filbert and Lemon and beef and pork and chicken and lamp and oyster and salmon and tuna and 

count() 计数方法

返回当前流元素的数量


List integerList=Stream
        .of(215,3308,7748956,-8,-567,12,9,453).collect(Collectors.toList());

        long a=integerList.stream().count();

        System.out.println(a);
        //结果: 8

收集

Collect() 收集器方法

Stream.Collect() 收集器方法 和前面那些操作流内容的方法不同
此方法用于从流生成一个新的集合
生产一个新的集合 需要
1. 指明是哪种数据结构
2. 如何把流中的元素加入到此数据结构中
3. 如果是并行操作 还要指明如何合并每个分片数据

下面看Collect()的两个重载方法

R collect(Supplier< R> supplier,
BiConsumer< R, ? super T> accumulator,
BiConsumer< R, R> combiner);

此方法的参数 仔细一看 和reduce的方法参数很类似
看了源码后发现 确实是比较相像的
第一个参数 为Supplier生产型行为接口 用于传入创建新数据结构集合的行为
第二个参数 为二元消费型接口 用于传入如何把流中的元素加入集合的行为
而第三个参数 是并行模式下 如何合并每个分片数据的行为

以创建ArrayList为例

 List<String> upperStringList=bigList.stream()
                .flatMap(c->c.stream())
                .map(x->x.toUpperCase())
                .collect(()->new ArrayList<>(), 
                (List<String> list,String element)->list.add(element),
                        (List, sub) -> { List.addAll(sub);});

将bigList 转换为一个元素内容全部为大写字母的集合

第一个参数 Supplier生产行为 此处创建了一个ArrayList数组列表
第二个参数 二元消费型行为 指示了新创建的List列表要将每个流元素add到列表中
第三个参数 二元消费型行为 合并器 指列表将分片的子列表addAll到大列表中

创建一个以元素自身为key 元素转换全部大写字母为value的散列表HashMap

   Map<String,String> resultMap=bigList.stream()
                .flatMap(c->c.stream())
                .collect(()->new HashMap<String,String>(),
                        (HashMap<String,String> map,String element)->map.put(element,element.toUpperCase()),
                        (HashMap<String,String> Map,HashMap<String,String> sub) -> { Map.putAll(sub);});

第一个参数 Supplier生产行为 此处创建了一个HashMap散列表
第二个参数 二元消费型行为 指示了新创建的map要将每个流元素put到散列表中
第三个参数 二元消费型行为 合并器 指列表将分片的子散列列表addAll到大散列表中

当然上面两个完全表达太复杂 完全可以使用方法引用来简写

  List<String> upperStringList=bigList.stream()
                .flatMap(c->c.stream())
                .map(x->x.toUpperCase())
                .collect(ArrayList::new, List::add, List::addAll);


        Map<String,String> resultMap=bigList.stream()
                .flatMap(c->c.stream())
                .collect(HashMap::new,
                (HashMap<String,String> map,String element)->
                map.put(element,element.toUpperCase()),
                Map::putAll);

使用此收集器 显然可以按自己的需求 任意收集流内容到目标数据结构
但是 显然我们用到的目标数据结构 绝大多数都是Java自带的集合类
所以 Java已经内置了一个收集器工具类 Collectors
此类已经为我们提供了对应的收集器

这就有了第二个Collect()的重载方法

< R, A> R collect(Collector< ? super T, A, R> collector)

此方法传入一个java.util.stream.Collector接口的实现类参数
(这个实现类由Collectors工具类调用)
实现将流的内容输出为一个Java集合数据结构的功能

        List upperStringList=bigList.stream()
                .flatMap(c->c.stream())
                .map(x->x.toUpperCase())
                .collect(Collectors.toList());

将bigList 转换为一个元素内容全部为大写字母的集合
相比上面的传入全部收集行为的代码 已经大大简化并且可以复用

Set set=bigList.stream()
                .flatMap(c->c.stream())
                .collect(Collectors.toSet());

上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier collectionFactory)方法完成。

创建普通的ArrayList和HashSet toList,toSet默认创建的就是这两种集合
ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet hashSet = stream.collect(Collectors.toCollection(HashSet::new));

创建LinkedHashSet
Set set=bigList.stream()
  .flatMap(c->c.stream())
  .collect(Collectors.toCollection(LinkedHashSet::new));
创建linkedList
List linkedList=bigList.stream()
  .flatMap(c->c.stream())
  .map(x->x.toUpperCase())
  .collect(Collectors.toCollection(LinkedList::new));

创建HashMap
Map hashMap=bigList.stream()
  .flatMap(c->c.stream()).collect(Collectors.toMap(key->key,value->"I like eat"+value));
高级生成

前面说了 可以使用Collectors.toMap()方法生成Map
只要正确指定生成键和值的行为即可

而高级生成可以利用Collectors_的分组 分区等功能对流内容进行操作 生成Map
此处有点接近C# linq的功能了

分组

在数据库操作中,我们可以通过GROUP BY关键字对查询到的数据进行分组,java8的流式处理也为我们提供了这样的功能Collectors.groupingBy来操作集合

groupingBy(Function< ? super T, ? extends K> classifier)


List<Student> studentList =
        List.of(new Student("ASDFG1", "Lilith", "woman", "1A", 17),
                new Student("MLOPJ2", "Goulie", "woman", "1A", 16),
                new Student("BHJSD8", "Sui", "woman", "2B", 19),
                new Student("KAEWQ4", "Ju", "woman", "2B", 18));       

Map<String,List<Student>> mapGroupByClass=
        studentList.stream().collect(Collectors.groupingBy(Student::getClazz));

如果用SQL表示 差不多是这样的语句

"select * from stream group by student.clazz"

如果你能 从Stream联想到SQL的写法 恭喜你 你学会了Linq (XD)

分组结果

mapGroupByClass.forEach((key, value) ->
        {
            System.out.println("按班级分组 班级:" + key + " 学生:");
            value.stream().forEach(student -> {
                System.out.println(student.toString());
            });
        });

//结果
按班级分组 班级:2B 学生:
Student(id=BHJSD8, name=Sui, gender=woman, Clazz=2B, age=19)
Student(id=KAEWQ4, name=Ju, gender=woman, Clazz=2B, age=18)
按班级分组 班级:1A 学生:
Student(id=ASDFG1, name=Lilith, gender=woman, Clazz=1A, age=17)
Student(id=MLOPJ2, name=Goulie, gender=woman, Clazz=1A, age=16)
分区

分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:true或false,目的是将待分区集合按照某个二值逻辑(满足条件,或不满足)分成互不相交的两部分,java8的流式处理利用Collectors.partitioningBy()方法实现分区

 Map> result =
                studentList.stream()
                        .collect(Collectors.partitioningBy((Student student)->student.getAge()>=18));

        result.forEach((k,v)->{
            System.out.println("按学生是否大于等于18岁分区:"+k);
            v.stream().forEach(x-> System.out.println(x.toString()));
        });

//结果
按学生是否大于18岁分区:false
Student(id=ASDFG1, name=Lilith, gender=woman, Clazz=1A, age=17)
Student(id=MLOPJ2, name=Goulie, gender=woman, Clazz=1A, age=16)
按学生是否大于18岁分区:true
Student(id=BHJSD8, name=Sui, gender=woman, Clazz=2B, age=19)
Student(id=KAEWQ4, name=Ju, gender=woman, Clazz=2B, age=18)
Student(id=BHGIH0, name=Tiger, gender=man, Clazz=2B, age=18)
Student(id=AQBUI3, name=Suoer, gender=man, Clazz=1A, age=18)
多级收集

在SQL中使用group by是为了协助其他查询,以上面的Student类为例 1先根据班级分组 2 在根据性别分组

Java类库设计者也考虑到了这种情况,提供了重载的groupingBy()方法。重载的groupingBy()方法可以在分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。

重载方法 两个参数 classifier分类器 downstream下游收集器

groupingBy(Function< ? super T, ? extends K> classifier,
Collector< ? super T, A, D> downstream)

多级分组 1先根据班级分组 2 在根据性别分组

List<Student> studentList =
        List.of(new Student("ASDFG1", "Lilith", "woman", "1A", 17),
                new Student("MLOPJ2", "Goulie", "woman", "1A", 16),
                new Student("BHJSD8", "Sui", "woman", "2B", 19),
                new Student("KAEWQ4", "Ju", "woman", "2B", 18),
                new Student("BHGIH0", "Tiger", "man", "2B", 18),
                new Student("AQBUI3", "Suoer", "man", "1A", 18));

        Map<String, Map<String, List<Student>>> mapGroupByClassAndGender =
                studentList.stream()
                        .collect(
                        Collectors.groupingBy(Student::getClazz, 
                        Collectors.groupingBy(Student::getGender))
                        );

多级分组结果

      mapGroupByClassAndGender.forEach((key,value)->{
            System.out.println("按班级分组 班级:" + key);
            value.forEach((subKey,subValue)->{
                System.out.println("按性别分组 性别:"+subKey+ " 学生:");
                subValue.forEach(student -> {
                    System.out.println(student.toString());
                });
            });
        });
//结果
按班级分组 班级:2B
按性别分组 性别:woman 学生:
Student(id=BHJSD8, name=Sui, gender=woman, Clazz=2B, age=19)
Student(id=KAEWQ4, name=Ju, gender=woman, Clazz=2B, age=18)
按性别分组 性别:man 学生:
Student(id=BHGIH0, name=Tiger, gender=man, Clazz=2B, age=18)
按班级分组 班级:1A
按性别分组 性别:woman 学生:
Student(id=ASDFG1, name=Lilith, gender=woman, Clazz=1A, age=17)
Student(id=MLOPJ2, name=Goulie, gender=woman, Clazz=1A, age=16)
按性别分组 性别:man 学生:
Student(id=AQBUI3, name=Suoer, gender=man, Clazz=1A, age=18)

此groupingBy方法的 第二个参数 下游收集器 并不是Function行为接口
而是一个Collector收集器操作 所以此处还可以传入其他的收集器操作
比如求和、计数、平均值、类型转换等

比如 统计每个分类下元素的数量

     Map mapGroupByCount =
                studentList.stream()
                     .collect(Collectors.groupingBy
                     (Student::getClazz, Collectors.counting()));

        mapGroupByCount
        .forEach((k,v)-> System.out.println("key:"+k+" count:"+v));

//结果
key:2B count:3
key:1A count:3

生成 其他嵌套Map

    Map> resultMap =
                studentList.stream()
                        .collect( 
Collectors.groupingBy(Student::getClazz,                                 Collectors.toCollection(LinkedList::new)));

生成 分组和平均值的Map

 Map result=
                studentList.stream()
                        .collect(
                        Collectors.groupingBy
                        (Student::getGender,
                        Collectors.averagingInt(Student::getAge)));
归约

收集器也提供了相应的归约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()实现。

Collectors.reducing()reduce()有类似之处
比如也可以进行求最大值 最小值的操作

studentList.stream()
.collect(Collectors.maxBy(Comparator.comparing(Student::getAge)))
.ifPresent(System.out::println);

studentList.stream().
collect(Collectors.minBy(Comparator.comparing(Student::getAge)))
.ifPresent(System.out::println);

感觉和reduce没什么区别 其实也没什么区别 不过是不同的写法而已

studentList.stream().map(Student::getAge).max(Integer::compareTo).ifPresent(System.out::println);
studentList.stream().map(Student::getAge).min(Integer::compareTo).ifPresent(System.out::println);

对流中某个数据进行求和操作 此处是对学生年龄求和

Integer res=studentList.stream().collect(Collectors.summingInt(Student::getAge));

这里是个整型结果 所以使用了整型收集方法 Collectors也提供了summingLong,summingDouble等其他类型的方法

当然用reduce()也能做到

studentList.stream().map(Student::getAge).reduce(( x, y)->x+y).ifPresent(System.out::println);

对流中某个数据进行求平均值操作 此处是对学生年龄求和

Double res=studentList.stream().collect(Collectors.averagingInt(Student::getAge));

现在我们已经知道 收集器的归约操作 可以分别求和 求平均值 求数量 求极大求极小

那么我们如果这些结果 都需要呢 难道要创建多个Stream吗?
现在传统for循环 可以一次性做到 Stream当然也能一次做到
Collectors收集器提供了 收集结果集统计类来实现聚合求值结果的功能

IntSummaryStatistics iss=studentList.stream().collect(Collectors.summarizingInt(Student::getAge));
System.out.println(iss.getCount());
System.out.println(iss.getAverage());
System.out.println(iss.getMax());
System.out.println(iss.getMin());
System.out.println(iss.getSum());
//结果
6
17.666666666666668
19
16
106

Java8 以后 String 添加了Join方法用于连接字符串
收集器同样提供了归约的字符串拼接方法 就是Collectors.joining()

studentList.stream().map(Student::getName).collect(Collectors.joining())
//结果 LilithGoulieSuiJuTigerSuoer
studentList.stream().map(Student::getName).collect(Collectors.joining(","))
//结果 Lilith,Goulie,Sui,Ju,Tiger,Suoer
studentList.stream().map(Student::getName).collect(Collectors.joining(",","[","]"))
//结果 [Lilith,Goulie,Sui,Ju,Tiger,Suoer]

上面的这些独立定义的方法 其实都是Collectors.reducing()的一个特殊情况
和max,min,count等方法可以由reduce实现一样 上面的这些收集器也可由
Collectors.reducing()的实现
可以说,先前讨论过的方法只是为了方便程序员开发而已,
但是,方便程序员开发的程序提升可读性恰恰是头等大事.
java8之后提供迥异于传统命令式编程的函数式Stream接口
正是为了帮助程序员 更清晰的 更语义化的进行编程

收集后操作

Collector.collectingAndThen()

此方法 可以 对收集后生成的结果 进行继续操作

Integer size=studentList.stream().map(x->"I love"+x.getName())
.collect(Collectors.collectingAndThen(Collectors.toList(),list->list.size()));

收集为List后 继续对List操作 获取其size大小

查找和匹配

以下这些方法 主要用于从流数据中获取一个值
底层主要是Stream的findOps(查找)和matchOps(匹配)方法
懂Scala的可能会有较强既视感(逃~~~~

find*() 寻找方法

  • findFirst 查找第一个方法
    这是一个 终结操作也是短路操作,它总是返回 Stream 的第一个元素,如果没有则返回空。
this.studentList.stream().findFirst().ifPresent(System.out::println);
  • findAny 查找任意一个
    相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个
this.studentList.stream().filter(x->x.getClazz().equals("2B")).findAny().ifPresent(System.out::println);


anyMatch() 任意匹配方法

anyMatch是检测流中元素是否存在一个或多个满足指定的参数行为,如果满足则返回true
例 检测是否有学生姓名以L开头

boolean anyMatch=studentList.stream().anyMatch(std->std.getName().startsWith("L"));
//true

allMatch() 全匹配方法

allMatch用于检测流中元素是否全部都满足指定的参数行为,如果全部满足则返回true
例 检测是否所有的学生都已满18周岁

boolean allMatch=studentList.stream().allMatch(std->std.getAge()>18);
//false

noneMatch() 无匹配方法

noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true

例 检测是否存在1A班的学生

boolean noneMatch=studentList.stream().anyMatch(std->"1A".equals(std.getClazz()));
//true

takeWhile() 达标即获取方法

dropWhile用于逐个匹配流中元素 符合条件的元素即获取 直到不符合条件 之后的元素不在判断

studentList.stream().takeWhile(x->"woman".equals(x.getGender())).forEach(System.out::println);

结果

Student(id=ASDFG1, name=Lilith, gender=woman, Clazz=1A, age=17)
Student(id=MLOPJ2, name=Goulie, gender=woman, Clazz=1A, age=16)
Student(id=BHJSD8, name=Sui, gender=woman, Clazz=2B, age=19)
Student(id=KAEWQ4, name=Ju, gender=woman, Clazz=2B, age=18)

可以看到 结果是 流中元素 先后匹配成功 知道不是woman的student 不再处理 后面全部抛弃

dropWhile() 达标即抛弃方法

dropWhile用于逐个匹配流中元素 符合条件的元素即丢弃 直到不符合条件 之后的元素不在判断

studentList.stream().dropWhile(x->"woman".equals(x.getGender())).forEach(System.out::println);

结果和takeWhile() 是反的

Student(id=BHGIH0, name=Tiger, gender=man, Clazz=2B, age=18)
Student(id=AQBUI3, name=Suoer, gender=man, Clazz=1A, age=18)

并行

parallel() 并行计算方法

在传统的Java代码中 并行处理数据十分复杂
第一:你得明确的把包含的数据结构分成若干子部分.
第二:你要给每个子部分分配独立的线程.
第三:你需要在恰当的时候对他们进行同步,来避免不希望出现的竞争条件,等待所有线程完成,最后把这些结果合并起来.
可想而知 编写并行的代码是多么的困难

而Stream可以轻松的调用并行方法 把大规模的数据处理并行化

studentList.parallelStream()
studentList.stream().parallel()

使用这两种方式 即可快捷的得到一个并行处理流

不过很遗憾的是 Java的并行流也没有智能到应付所有的情况的地步
所以 不恰当的使用并行流 仍然会面临传统的多线程并发问题
如 可能使性能更差(比如一些依赖元素顺序的操作)

其他

unorder()无序结果方法

当不考虑有序时,调用unordered方法标记可以不关心顺序。这样可以加快一些方法的速度,比如limit,会返回流中任意n个元素

        studentList.stream().unordered().limit(3).forEach(System.out::println);

但有可能unordered后仍然是原来的排序

此方法主要用于并行流 标记后使并行流避免顺序执行 提升性能

自动生成

Stream.concat() 平铺

用于生成一个包含集合中所有元素的新Stream
功能和flatMap有些类似

 List girlStudentLists =
                List.of(new Student("ASDFG1", "Lilith", "woman", "1A", 17),
                        new Student("MLOPJ2", "Goulie", "woman", "1A", 16),
                        new Student("BHJSD8", "Sui", "woman", "2B", 19),
                        new Student("KAEWQ4", "Ju", "woman", "2B", 18)
                );

        List boyStudentLists =List.of(new Student("BHGIH0", "Tiger", "man", "2B", 18),
                new Student("AQBUI3", "Suoer", "man", "1A", 18));

        Stream studentStream=Stream.concat(girlStudentLists.stream(),boyStudentLists.stream());
        studentStream.forEach(System.out::println);

结果

Student(id=ASDFG1, name=Lilith, gender=woman, Clazz=1A, age=17)
Student(id=MLOPJ2, name=Goulie, gender=woman, Clazz=1A, age=16)
Student(id=BHJSD8, name=Sui, gender=woman, Clazz=2B, age=19)
Student(id=KAEWQ4, name=Ju, gender=woman, Clazz=2B, age=18)
Student(id=BHGIH0, name=Tiger, gender=man, Clazz=2B, age=18)
Student(id=AQBUI3, name=Suoer, gender=man, Clazz=1A, age=18)

Stream.iterate() 迭代

Stream< T> iterate(final T seed, final UnaryOperator< T> f)
iterate 跟 reduce 操作很像,接受一个初始值,和一个二元操作行为。然后初始值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。
由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
//0 3 6 9 12 15 18 21 24 27

生成 公差为3 的等差数列

Stream iterate(T seed, Predicate< ? super T> hasNext, UnaryOperator next)

iterate 也提供了一个重载方法
在这个方法中第二个参数hasNext是个谓词行为
判定是否结束 如果不符合就结束

        Stream.iterate(0, i->i<1000,n -> n + 3).forEach(x -> System.out.print(x + " "));

生成 公差为3 的等差数列 大于等于1000停止
如果使用此方法 就可以不使用limit终止

Stream.generate() 生成

Stream generate(Supplier< ? extends T> s)
通过传入 Supplier生产行为,你可以控制流自动生成元素。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

Stream.generate(()->new Random().nextInt()).limit(10).forEach(System.out::println);

生成10个整型随机数

Stream.generate(()->new Student()).limit(10).map(x->{
            x.setAge(new Random().nextInt());
            String uuid=UUID.randomUUID().toString().substring(0,5);
            x.setId(uuid);
            x.setName("TestStudent"+uuid);
            return x;
        }).forEach(System.out::println);
//结果
Student(id=a5b9b, name=TestStudenta5b9b, gender=null, Clazz=null, age=2001243543)
Student(id=7ea8a, name=TestStudent7ea8a, gender=null, Clazz=null, age=938468072)
Student(id=c8180, name=TestStudentc8180, gender=null, Clazz=null, age=1900938689)
Student(id=4078f, name=TestStudent4078f, gender=null, Clazz=null, age=906619179)
Student(id=5f831, name=TestStudent5f831, gender=null, Clazz=null, age=1561365434)
Student(id=472cc, name=TestStudent472cc, gender=null, Clazz=null, age=806196264)
Student(id=9c982, name=TestStudent9c982, gender=null, Clazz=null, age=1759347815)
Student(id=6301c, name=TestStudent6301c, gender=null, Clazz=null, age=975973857)
Student(id=c71e2, name=TestStudentc71e2, gender=null, Clazz=null, age=1860574007)
Student(id=0209e, name=TestStudent0209e, gender=null, Clazz=null, age=-947123494)        

生成10个测试类

结束语

Java8 的Stream博大精深 可以说是相对于传统命令式编程的一次巨大的进步
我们应该更主动 更大规模的使用Stream来替代以for循环为代表的命令式编程
以编写更清晰 可读性更好的代码
毕竟 现在这个年头 处理器和内存性能 早已不是瓶颈
再也不用计较函数式那点性能损失

你可能感兴趣的:(Java基础增强)