Java8 - 流

作者: 程序猿蛋蛋哥 | 来源:发表于2020-03-15 00:23 被阅读0次

    1. 流的两个重要特点:流水线、内部迭代

    <1> 流水线:

    很多流操作本身会返回一个流,多个这样的操作链接起来,形成一个大的流水线。

    <2> 内部迭代:

    与使用迭代器显式迭代的集合不同,流使用内部迭代,即stream库内部帮你把迭代实现了。

    2. 流与集合的两个重要区别:结构、迭代

    <1> 结构:

    流是概念上固定的数据结构,其元素是需计算,即只有在需要的时候才计算值。
    集合是一个内存中的数据结构,它包含数据结构中目前所有的值,即集合中的每个元素都得先算出来才能添加到集合中。

    举列:
    集合就像DVD里的电影,它完整的保存了整个电影,是在之前已经处理完成,再放到DVD里。
    流就像网上在线看电影,不是非得把整个电影都下载完成后再来看,而是根据需要计算下载。

    <2> 迭代:

    流使用内部迭代,即stream库内部帮你把迭代做了。
    集合使用外部迭代,即需要自己使用Collection接口做外部显示迭代。

    内部迭代的好处:可以自动选择一种适合硬件的数据表示和并行实现,即以更优化的顺序进行处理。

    流-内部迭代.png 集合-外部迭代.png

    注意:
    流只能遍历一次,遍历完之后,这个流已经被消费掉了。你可以从原始数据源那里再获取一个新的流来重新遍历一遍。

    如下代码多次遍历流,导致抛出异常:

    Stream<String> stream = Arrays.asList("a", "b", "c").stream();
    stream.forEach(System.out::println);
    stream.forEach(System.out::println);
    
    多次遍历流,抛出异常:
    Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
        at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
        at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
        at com.fan.demo.DemoTest.main(DemoTest.java:115)
    

    3. 流的两类操作:中间操作、终端操作

    流的中间与终端操作.png

    <1> 中间操作:

    流的中间操作不执行任何处理,它们会返回一个流,多个中间操作一般都可以合并起来,在终端操作时一次性全部处理。

    <2> 终端操作:

    终端操作会从流的流水线生成结果,其结果不是流,而是如List、Integer、void的值。

    4. 流的使用

    构建流:


    Stream.of -- 通过显式值创建一个流(接受任意类型数量的参数)

    Stream<String> stream = Stream.of("Hello World", "James", "Ok");
    stream.map(String::toUpperCase).forEach(System.out::println);
    
    输出:
    HELLO WORLD
    JAMES
    OK
    
    创建一个空流:
    Stream<String> emptyStream = Stream.empty();
    

    Arrays.stream -- 通过数组创建一个流(接受一个数组作为参数)

    int[] numbers = {3, 2, 5, 11, 13, 7};
    int sum = Arrays.stream(numbers).sum();   --- 总和41
    

    Files.lines -- 由文件生成流(返回一个由指定文件中的各行构成的字符串流)

    Stream.iterate、Stream.generate -- 由函数生成无限流(应该使用limit(n)来加以限制)

    Stream.iterate方法接受一个初始值,实际应用在需要依次生成一系列值的时候,比如一些列日期:1月31日,2月1日
    注意:Stream.iterate方法是纯粹不变的:它没有修改现有状态,在每次迭代时会创建新的元组。

    Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
    输出:
    0
    2
    4
    6
    8
    
    使用Stream.iterate构建一个斐波那契数列:(斐波那契数列中前两个数字是0,1,后续的每个数字 = 前两个数字之和)
    先构建一个斐波那契元组,格式为:(数列中数字,后续数字),如下:
    Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(10).forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));
    输出:
    (0,1)
    (1,1)
    (1,2)
    (2,3)
    (3,5)
    (5,8)
    (8,13)
    (13,21)
    (21,34)
    (34,55)
    
    使用map提取每个元组中的第一个元素,则构建的斐波那契数列为:
    Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(10).map(t -> t[0]).forEach(System.out::println);
    输出:
    0
    1
    1
    2
    3
    5
    8
    13
    21
    34
    

    Stream.generate方法接受一个Supplier<T>类型的Lambda提供新的值

    Stream.generate(Math::random).limit(5).forEach(System.out::println);
    输出:
    0.7017580650263067
    0.5551699003184056
    0.17273970322460497
    0.5153472880449076
    0.8041474317735003
    

    筛选:


    filter(谓词) -- 用谓词筛选(谓词:一个返回boolean的函数)

    找出所有偶数,并返回列表
    List<Integer> numbers = Arrays.asList(8, 9, 100, 200, 8, 18, 23, 100);
    List<Integer> evenNumList = numbers.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());   --- [8, 100, 200, 8, 18, 100]
    

    distinct -- 去重(根据流所生成元素的hashCode和equals方法实现筛选各异元素)

    List<Integer> numbers = Arrays.asList(8, 9, 100, 200, 8, 18, 23, 100);
    numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
    
    输出:
    8
    100
    200
    18
    

    limit(n) -- 截短流(截取前n个元素)

    找出所有偶数,并截取走前2两个
    Stream.of(8, 9, 100, 200, 8, 18, 23, 100).filter(i -> i % 2 == 0).limit(2).forEach(System.out::println);
    
    输出:
    8
    100
    

    skip(n) -- 跳过元素(扔掉前n个元素,流中元素不足n个,返回空流)

    找出所有偶数,并扔掉前两个
    Stream.of(8, 9, 100, 200, 8, 18, 23, 100).filter(i -> i % 2 == 0).skip(2).forEach(System.out::println);
    
    输出:
    200
    8
    18
    100
    

    映射


    map(函数) -- 对流中每一个元素应用函数,将其映射为一个新元素(不去修改,创建新版本)

    对每个单词求其长度并返回列表
    List<String> words = Arrays.asList("Hello", "Are", "You", "Ok");
    List<Integer> wordLengths = words.stream().map(String::length).collect(Collectors.toList());   --- [5, 3, 3, 2]
    

    flatMap -- 对流中每个元素都转换成另一个流,并把所有的流连接起来成为一个流,即流的扁平化

    对给定的单词,得到不同字符的列表:
    List<String> words = Arrays.asList("Hello", "Are", "You", "Ok");
    List<String> uniqueCharacters = words.stream().map(w -> w.split("")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
    --- [H, e, l, o, A, r, Y, u, O, k]
    

    查找和匹配


    anyMatch(谓词) -- 流中是否至少有一个元素能匹配谓词

    是否至少有一个元素大于100
    boolean flag = Stream.of(8, 9, 100, 200, 8, 18, 23, 100).anyMatch(i -> i > 100);   --- true
    

    allMatch(谓词) -- 流中是否所有的元素都匹配谓词

    是否所有都为偶数
    boolean flag = Stream.of(8, 9, 100, 200, 8, 18, 23, 100).allMatch(i -> i % 2 == 0);   --- false
    

    noneMatch(谓词) -- 是否流中没有元素与谓词匹配

    是否没有元素大于200
    boolean flag = Stream.of(8, 9, 100, 200, 8, 18, 23, 100).noneMatch(i -> i > 200);   --- true
    

    findAny() -- 返回当前流中的任意元素

    返回当前流中长度大于2的任意元素
    Optional<String> word = Stream.of("Hello World", "James", "Ok").filter(e -> e.length() > 2).findAny();   --- Hello World
    
    如果流中没有满足条件的元素,则findAny()返回空(Optional.empty)
    
    找出流中长度大于2的任意元素,如果存在,按如下格式输出(结合Optional对象的ifPresent(函数) ):
    Stream.of("Hello World", "James", "Ok").filter(e -> e.length() > 2).findAny().ifPresent(s -> System.out.println("[" + s + "]'s length = "+ s.length()));   
    --- [Hello World]'s length = 11
    

    findFirst() -- 查找第一个元素

    对流中元素求平方,找出第一个能被3整除的元素
    Optional<Integer> firstElement = Stream.of(1, 2, 3, 4, 5, 6).map(x -> x * x).filter(x -> x % 3 == 0).findFirst();   --- 9
    

    归约


    reduce -- 归约(将流中元素反复结合起来,得到一个值)

    元素求和:
    int sum = Stream.of(5, 6, 7, 8).reduce(0, (a, b) -> a + b);   --- 26
    int sum = Stream.of(5, 6, 7, 8).reduce(0, Integer::sum);   --- 26
    无初始值,返回Optional对象
    Optional<Integer> sumOpt = Stream.of(5, 6, 7, 8).reduce((a, b) -> (a + b));   --- 26
    
    求最大值:
    int max = Stream.of(10, 20, 0, 11, 100, 250).reduce(0, Integer::max);   --- 250
    Optional<Integer> maxOpt = Stream.of(10, 20, 0, 11, 100, 250).reduce(Integer::max);   --- 250
    
    求最小值:
    int min = Stream.of(10, 20, 0, 11, 100, 250).reduce(0, Integer::min);   --- 0
    Optional<Integer> minOpt = Stream.of(10, 20, 0, 11, 100, 250).reduce(Integer::min);   --- 0
    
    map-reduce模式:(map和reduce的连接)
    计算单词的个数:
    int count = Stream.of("Hello", "James", "Ok").map(e -> 1).reduce(0, Integer::sum);   --- 3 (将流中每个元素映射成数字1)
    long count = Stream.of("Hello", "James", "Ok").count();   --- 3
    

    特化流 IntStream、DoubleStream、LongStream

    特化流IntStream、DoubleStream、LongStream分别将流中的元素特化为int、long和double,从而避免了暗含的装箱操作。

    mapToInt、mapToDouble、mapToLong -- 将流转化成特化流

    计算每个字符串的长度,并返回一个IntStream特化流(不是一个Stream<Integer>),然后调用IntStream接口中的sum方法求和:
    如果流为空,则sum默认返回0
    int sum = Stream.of("Hello World", "James", "Ok").mapToInt(String::length).sum();   --- 18
    
    数值范围:
    rangeClosed(参数1, 参数2) -- 生成两个参数之间的所有数字,包含第二个参数 
    
    IntStream evenNum = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
    System.out.println(evenNum.count());   --- 50
    
    range(参数1, 参数2) -- 生成两个参数之间的所有数字,不包含第二个参数
    
    IntStream evenNum = IntStream.range(1, 100).filter(n -> n % 2 == 0);
    System.out.println(evenNum.count());   --- 49
    

    boxed方法 -- 将特化流转回一般的对象流

    IntStream intStream = Stream.of("Hello World", "James", "Ok").mapToInt(String::length);
    Stream<Integer> stream = intStream.boxed();
    

    Optional原始类型特化版本:OptionalInt、OptionalDouble、OptionalLong

    计算每个字符串的长度,并返回一个IntStream特化流,调用其max方法,返回一个OptionalInt特化对象:
    OptionalInt maxLength = Stream.of("Hello World", "James", "Ok").mapToInt(String::length).max();   --- 11
    

    收集器:


    为了便于举例,定义一个Person类,如下:

    public class Person {
        private final String name;
        private final String sex;
        private final int age;
        private final Job job;
        private final boolean partyMember;
    
        public Person(String name, String sex, int age, Job job, boolean partyMember) {
            this.name = name;
            this.sex = sex;
            this.age = age;
            this.job = job;
            this.partyMember = partyMember;
        }
        public enum Job { TEACHER, WORKER, DOCTOR, DRIVER, COOK}
        ...省略get,set方法
    }
    
    构建personList列表:
    List<Person> personList = Arrays.asList(
        new Person("李铁蛋", "男",31, Person.Job.WORKER, true),
        new Person("王翠花", "女", 22, Person.Job.TEACHER, false),
        new Person("牛建国", "男", 62, Person.Job.DRIVER, false),
        new Person("Lucy Rose", "女", 42, Person.Job.DOCTOR, true),
        new Person("尼古拉斯蛋蛋", "男", 51, Person.Job.COOK, false)
    );
    
    构建流:
    Stream<Person> personStream = personList.stream();
    

    Collectors.toList() -- 把流中所有元素收集到一个List

    List<Person> persons = personStream.collect(Collectors.toList());
    

    Collectors.toSet() -- 把流中所有元素收集到一个Set,删除重复项

    Set<Person> persons = personStream.collect(Collectors.toSet());
    

    Collectors.toCollection -- 把流中所有元素收集到给定的供应源创建的集合

    Collection<Person> persons = personStream.collection(Collections.toCollection(ArrayList::new));
    

    Collectors.counting() -- 计算流中元素个数

    long count = personStream.collect(counting());
    等价于
    long count = personStream.count();
    

    对流中元素相应类型的属性求和

    Collectors.summingInt、Collectors.summingLong、Collectors.summingDouble:

    对所有人的年龄求和
    int ageSum = personStream.collect(Collectors.summingInt(Person::getAge));
    

    计算流中元素的平均值

    Collectors.averagingInt、Collectors.averagingLong、Collectors.averagingDouble(返回类型都为double):

    求所有人年龄的平均值
    double ageAver = personStream.collect(Collectors.averagingInt(Person::getAge));
    

    计算流中元素对应类型属性的统计值(最大值,最小值,总和,平均值,数量)

    Collectors.summarizingInt、Collectors.summarizingLong、Collectors.summarizingDouble:

    对所有人的年龄求统计值
    IntSummaryStatistics ageStatistics = personStream.collect(Collectors.summarizingInt(Person::getAge));
    输出:
    IntSummaryStatistics{count=5, sum=208, min=22, average=41.600000, max=62}
    

    Collectors.joining -- 连接字符串(内部使用StringBuilder来连接)

    String personName = personStream.map(Person::getName).collect(Collectors.joining(", "));
    

    Collectors.maxBy -- 按照指定比较器选出最大元素(被Optional包裹)

    选出年龄最大的人
    Optional<Person> maxAgePerson = personStream.collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
    

    Collectors.minBy -- 按照指定比较器选出最小元素(被Optional包裹)

    选出年龄最小的人
    Optional<Person> minAgePerson = personStream.collect(Collectors.minBy(Comparator.comparingInt(Person::getAge)));
    

    Collectors.reducing -- 从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值。

    对所有人的年龄求和
    int ageSum = personStream.collect(Collectors.reducing(0, Person::getAge, Integer::sum));
    

    Collectors.groupingBy -- 对流中元素进行分组,返回结果Map

    根据元素的一个属性的值对元素进行分组,分组结果是个Map,Map的key为属性的值,Map的value为属于这个属性的元素列表。

    按职业对人进行分组
    Map<Person.Job, List<Person>> personsByJob = personStream.collect(Collectors.groupingBy(Person::getJob));
    输出:
    {
    COOK=[Person{name='尼古拉斯蛋蛋', sex='男', age=51, job=COOK}], 
    TEACHER=[Person{name='王翠花', sex='女', age=22, job=TEACHER}], 
    DRIVER=[Person{name='牛建国', sex='男', age=62, job=DRIVER}], 
    DOCTOR=[Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR}], 
    WORKER=[Person{name='李铁蛋', sex='男', age=31, job=WORKER}]
    }
    

    按分的子组收集数据:即传递收集器作为groupingBy的第二个参数。

    例1:按性别分组,并计算每组的人数:
    Map<String, Long> sexCount = personStream.collect(Collectors.groupingBy(Person::getSex, Collectors.counting()));
    输出:
    {女=2, 男=3}
    
    例2:按性别分组,并查找每组中年龄最大的人:
    Map<String, Optional<Person>> maxAgeBySex = personStream.collect(Collectors.groupingBy(Person::getSex, Collectors.maxBy(Comparator.comparingInt(Person::getAge))));
    输出:
    {
    女=Optional[Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR}], 
    男=Optional[Person{name='牛建国', sex='男', age=62, job=DRIVER}]
    }
    说明:这个Map中的值是Optional,是由于maxBy收集器返回的类型,实际上,如果不存在某个性别,就不会对应一个Optional.empty()值,根本不会出现在Map的键中,所以这个Optional包装器在这里不是很有用。
    

    支持多级分组,即可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中元素分类的二级标准。

    先按性别分组,然后再按职业分组:
    Map<String, Map<Person.Job, List<Person>>> personBySexAndJob = personStream.collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getJob)));
    输出:
    {
    女={
        DOCTOR=[Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR}], 
        TEACHER=[Person{name='王翠花', sex='女', age=22, job=TEACHER}]
       }, 
    男={
        COOK=[Person{name='尼古拉斯蛋蛋', sex='男', age=51, job=COOK}], 
        DRIVER=[Person{name='牛建国', sex='男', age=62, job=DRIVER}], 
        WORKER=[Person{name='李铁蛋', sex='男', age=31, job=WORKER}]
       }
    }
    

    Collectors.collectingAndThen -- 包裹另一个收集器,对其结果应用转换函数

    这个工厂方法接受两个参数,即要转换的收集器和转换函数。该方法相当于对旧收集器的一个包装,collect操作的最后一步将返回值用转换函数做一个映射。

    如下例子:collectingAndThen包裹起maxBy收集器,而转换函数Optional::get则把返回的Optional中的值提取出来。
    
    按性别进行分组,并找出每组中年龄最大的人:
    Map<String, Person> personMap = personStream.collect(Collectors.groupingBy(Person::getSex,
                    Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Person::getAge)),Optional::get)));
    输出:
    {
    女=Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR, partyMember=true}, 
    男=Person{name='牛建国', sex='男', age=62, job=DRIVER, partyMember=false}
    }
    

    Collectors.partitioningBy(谓词) -- 分区(分组的特殊情况),即谓词(返回一个boolean值的函数)作为分类函数,进行分组。

    根据是否为党员对人进行分组:
    Map<Boolean, List<Person>> personMap = personStream.collect(Collectors.partitioningBy(Person::isPartyMember));
    输出:
    {
    false=[
    Person{name='王翠花', sex='女', age=22, job=TEACHER, partyMember=false}, 
    Person{name='牛建国', sex='男', age=62, job=DRIVER, partyMember=false}, 
    Person{name='尼古拉斯蛋蛋', sex='男', age=51, job=COOK, partyMember=false}
    ], 
    true=[
    Person{name='李铁蛋', sex='男', age=31, job=WORKER, partyMember=true}, 
    Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR, partyMember=true}
    ]
    }
    

    可以传递收集器作为partitioningBy的第二个参数:

    例1:统计党员与非党员的人数:
    Map<Boolean, Long> personCount = personStream.collect(Collectors.partitioningBy(Person::isPartyMember, Collectors.counting()));
    输出:{false=3, true=2}
    
    例2:先按是否为党员,然后再按性别进行分组:
    Map<Boolean, Map<String, List<Person>>> personMap = personStream.collect(Collectors.partitioningBy(Person::isPartyMember, Collectors.groupingBy(Person::getSex)));
    输出:
    {
    false={
        女=[Person{name='王翠花', sex='女', age=22, job=TEACHER, partyMember=false}], 
        男=[
            Person{name='牛建国', sex='男', age=62, job=DRIVER, partyMember=false}, 
            Person{name='尼古拉斯蛋蛋', sex='男', age=51, job=COOK, partyMember=false}
           ]
    }, 
    true={
        女=[Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR, partyMember=true}], 
        男=[Person{name='李铁蛋', sex='男', age=31, job=WORKER, partyMember=true}]
    }
    }
    
    例3:先按是否为党员分组,然后找出年龄最大的人:
    Map<Boolean, Person> personMap = personStream.collect(Collectors.partitioningBy(Person::isPartyMember,
                    Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Person::getAge)), Optional::get)));
    输出:
    {
    false=Person{name='牛建国', sex='男', age=62, job=DRIVER, partyMember=false}, 
    true=Person{name='Lucy Rose', sex='女', age=42, job=DOCTOR, partyMember=true}
    }
    
    例4:写一个方法,接受参数int n,并将前n个自然数分为质数和非质数:
    
    判断一个数是否为质数的方法:
    public static boolean isPrime(int candidate) {
        int candidateRoot = (int) Math.sqrt(candidate);
        return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
    }
    创建一个包含n个自然数的流,用isPrime方法作为谓词进行分区:
    public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
        return IntStream.rangeClosed(1, n).boxed().collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
    }
    

    相关文章

      网友评论

        本文标题:Java8 - 流

        本文链接:https://www.haomeiwen.com/subject/zwrcjhtx.html