zoukankan      html  css  js  c++  java
  • 常用函数式接口与Stream API简单讲解

    常用函数式接口与Stream API简单讲解

    Stream简直不要太好使啊!!!

    常用函数式接口

    • Supplier<T>,主要方法:T get(),这是一个生产者,可以提供一个T对象。
    • Consumer<T>,主要方法:void accept(T),这是一个消费者,默认方法:andthen(),稍后执行。
    • Predicate<T>,主要方法:boolean test(T t),这是一个判断者,默认方法:and():且,or():或,negate():非。
    • Function<T,R>,主要方法:R apply(T t),这是一个修改者,默认方法:compose():优先执行,andThen(),稍后执行,identity():直接传自身。
    Function f1 = e -> e + 1;
    Function f2 = e -> e * 1;
    f1.andthen(f2).apply(t)//即f2.apply(f1.apply(t))
    f1.compose(f2).apply(t)//即f1.apply(f2.apply(t))
    而identify()相当于 s -> s
    

    常用Stream方法:

    中间操作是惰性求值,中间操作过后返回的还是Stream,因此可以继续操作;结束操作是立即求值,返回具体的数据。

    流不能被重用,因此推荐链式写法。

    上边仅仅是Stream的方法,经常使用的还有Stream的静态方法,比如:Stream.concat(),用来合成两个流;Stream.of()方法,直接获取流。

    • 使用流处理int数组时,可以使用Arrars.stream(arr)或者IntStream.of(arr)进行初始的流转化,而Stream.of(arr)会把arr当做数组对象传入,而非直接操作数组。(Arrays.stream 是为了弥补Stream.of无法使用数组参数而做的补偿方案.比如a[]传入Stream.of会被当成一个对象而传入Arrays.stream可以当成一个数组)

    具体区别可以由下面几个栗子看出来:

    int[] arr = {1,2,3,4};
    System.out.println(
            Arrays.stream(arr).count());//4,因为数组里有4个数
    System.out.println(
            Stream.of(arr).count());//1,因为只有一个数组,并且这里的流不能调用sum()方法求和,因为引用对象没法相加
    Stream.of(1,2,3,4).count();//4,因为有四个Integer,注意这里Stream.of()方法会自动装箱,把int变为Integer,这里也不能调用sum()方法
    Arrays.stream(new int[]{1,2,3,4}).count();//4,等价于第一种情况,并且此时还有个box()方法可以装箱,因此使用流处理拆装箱非常方便
    //但是!如果Stream.of()参数是一个引用类型的数组(注意是一个),比如String[]:
    System.out.println(
            Stream.of(new String[]{"My", "Java", "My", "Life!"}).count());//4
    //它又可以正常使用,
    //但是!!如果里面是两个String[]数组,它又不好使了:
    String[] s1 = {"My", "Java", "My", "Life!"};
    String[] s2 = {""};
    System.out.println(
            Stream.of(s1,s2).count());//2!!!
    

    小结:说得好,数组我选择Arrays.Stream()! (`へ´*)ノ

    主要方法介绍:

    forEach
    forEach():void forEach(Consumer<? super E> action),
    即对每个元素执行action,也就是最常见的遍历

    Stream.of("My","Java","My","Life!")
            .forEach(System.out::println);//打印每个元素
    

    filter
    filter():Stream<T> filter(Predicate<? super T> predicate),筛选,即挑选出符合predicate的元素

    Stream.of("My", "Java", "My", "Life!")
            .filter(s -> s.length() > 4);//筛选出长度大于4的元素,此时流里的元素为:"Java","Life!"
    

    distinct
    distinct():Stream<T> distinct(),去重

    Stream.of("My", "Java", "My", "Life!")
            .distinct();//"My","Java","Life!"
    

    sorted
    sorted():Stream<T> sorted()Stream<T> sorted(Comparator<? super T> comparator),排序,无参为自然排序,有参为自定义比较器排序

    Stream.of("My", "Java", "My", "Life!")
            .sorted()//"Java","Life!","My","My"无参,也就是按照默认排序(这里就是ASCII码)
            .forEach(System.out::println);
    Stream.of("My", "Java", "My", "Life!")
            .sorted((s1, s2) -> s2.length() - s1.length())//"Life!","Java","My","My",按照长度进行降序排序
            .forEach(System.out::print);
    

    map
    map():<R> Stream<R> map(Function<? super T,? extends R> mapper),转换,也就是执行Function的功能

    class Student{
        private String name;
    
        public Student(String name){
            this.name = name;
        }
    }
    
    Stream.of("My", "Java", "My", "Life!")
            .map(String::toLowerCase)//转化为小写
            .map(Student::new);//接着以名字为参数转化Student流
    

    flatMap
    flatMap():<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper),扁平化转换流,与map()不同的是,map()返回一个结果,而当返回结果数目未知时不太方便,使用flatMap()会将多个数目不一的流合为一个流

    Stream.of(Arrays.asList("My"), Arrays.asList("Java", "My", "Life!"))//此时,这是List流,里面是两个List对象,List里面才是字符串数据
            .flatMap(list -> list.stream())//扁平化,即把List流转化为字符串流,这里也可以使用方法引用:.flatMap(Collection::stream)
            .forEach(System.out::println);
    

    count
    count():long count(),统计流中元素的数量,注意返回值为long

    Stream.of("My", "Java", "My", "Life!")
            .count();//4L
    

    limit
    limit():Stream<T> limit(long maxSize),限制长度,也可以理解为取前maxSize个元素,maxSize可以大于元素数目

    Stream.of("My", "Java", "My", "Life!")
            .limit(2);//"My","Java"
    

    skip
    skip():Stream<T> skip(long n),跳过前n个元素,和limit差不多

    Stream.of("My", "Java", "My", "Life!")
            .skip(2);//"My","Life!"
    

    toArray
    toArray:Object[] toArray()<A> A[] toArray(IntFunction<A[]> generator),无参即返回Object[]数组,而有参时可以返回特定类型数组

    Stream.of("My", "Java", "My", "Life!")
            .toArray(String[]::new);//String[]数组,内容为:["My", "Java", "My", "Life!"]
    

    parallel
    parallel:S parallel()转化为并行流,但是单核甚至双核的情况下转化并行流反而会降低效率。

    max
    max():Optional<T> max(Comparator<? super T> comparator),以自定义比较方法返回一个最大对象,注意这里返回的是Optional对象。

    Stream.of("My", "Java", "My", "Life!")
            .max((o1, o2) -> o2.length()-o1.length())
            .get();//"My"
    
    Stream.of("My", "Java", "My", "Life!")
            .max(Comparator.comparingInt(String::length))//"Life!",这里返回的是Optional,里面盛的是"Life!"
            .stream()//这里调用的是Optional的方法,其实这里没有必要,直接调用get()方法就可以拿到"Life!"字符串了
            .forEach(System.out::println);
    

    说到了max()就不得不提一下Optional是什么,简单来说Optional就是一个容器,用来盛放数据,并且它是容许为null的,这样就不用反复判断值是否为空了。可以看一下Optional的方法:

    public final class Optional<T> {
        //静态方法,返回空Optional对象
        public static<T> Optional<T> empty() {
        }
    
        //静态方法,返回Optional对象,value不能为空
        public static <T> Optional<T> of(T value) {
            return new Optional<>(value);
        }
    
        //静态方法,返回Optional对象,值允许为null
        public static <T> Optional<T> ofNullable(T value) {
            return value == null ? empty() : of(value);
        }
    
        //虽然值允许为null,但还是在调用get时还是会抛出NoSuchElementException异常
        public T get() {
        }
    
        //所以此方法可以判断值是否存在
        public boolean isPresent() {
        }
    
        //如果存在,则执行action
        public void ifPresent(Consumer<? super T> action) {
        }
    
        //JDK9方法↓
        //如果值非空,执行action,值为null则执行emptyAction
        public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
        }
    
        //Optional对象也可以执行与Stream相同的部分方法
        public Optional<T> filter(Predicate<? super T> predicate) {
        }
        public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        }
        public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
        }
    
        //JDK9方法↓
        //如果为null,则返回一个supplier提供的对象,与orElse和orElseGet方法类似。
        public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
        }
    
        //JDK9方法↓
        //可以把Optional对象继续变成流
        public Stream<T> stream() {
        }
    
            public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
            if (value != null) {
                return value;
            } else {
                throw exceptionSupplier.get();
            }
        }
    
        //重写equals方法
        @Override
        public boolean equals(Object obj) {
        }
    
        //重写hashCode方法
        @Override
        public int hashCode() {
        }
    
        //重写toString方法
        @Override
        public String toString() {
            return value != null
                ? String.format("Optional[%s]", value)
                : "Optional.empty";
        }
    }
    

    orElse和orElseGet:
    orElseorElseGet这两个方法放上源码比较好理解:
    首先,在值为null时,两个方法并无区别,都能获取到T;
    但是,在非空时,如果orElse()的参数是一个方法,该方法也会被执行,而orElseGet()的参数supplier并不会执行,这得益于Lambda的延迟执行。因此,相同情况下orElseGet()性能更优。或者直接使用or,如果JDK允许的话

        public T orElse(T other) {
            return value != null ? value : other;
        }
        public T orElseGet(Supplier<? extends T> supplier) {
            return value != null ? value : supplier.get();
        }
    

    举个小栗子就能明白了:

    static String B() {
            System.out.println("B()执行了");
            return "B";
    }
    
    public static void main(final String... args) {
        System.out.println(Optional.ofNullable(null).orElse(B()));
        System.out.println(Optional.ofNullable(null).orElseGet(() -> B()));
    }
    

    上面这种情况执行结果为:

    B()执行了
    B
    B()执行了
    B
    

    并没有什么问题,但是当值非空的时候:

    static String B() {
        System.out.println("B()执行了");
        return "B";
    }
    
    public static void main(final String... args) {
        System.out.println(Optional.ofNullable("A").orElse(B()));
        System.out.println(Optional.ofNullable("A").orElseGet(() -> B()));
    }
    

    执行结果为:

    B()执行了
    A
    A
    

    很明显orElseGet更节约性能,Lambda的延迟执行特性还是比较好用的。


    咳咳,扯远了,继续说Stream的常用方法:

    min
    min():Optional<T> min(Comparator<? super T> comparator),与max类似,注意max()min()最好使用引用方法,而不是自己定义Comparator,举个栗子:

    Stream.of("My", "Java", "My", "Life!")
            .min((o1, o2) -> o2.length()-o1.length());//"Life!"
    

    这里使用的是min()方法,自定义Comparator为常见的降序,但是最后得到的反而是长度最长的,完全违背了函数式见文知意的思想。

    所以想要比较长度的话这样写:

    Stream.of("My", "Java", "My", "Life!")
            .max(Comparator.comparingInt(String::length));
    Stream.of("My", "Java", "My", "Life!")
            .min(Comparator.comparingInt(String::length));
    

    或者,你觉得max(),min(),count(),sum()这些不好用,还有下面两个多能工供你使用

    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<U> combiner);

    一个reduce操作(也称为折叠)接受一系列的输入元素,并通过重复应用操作将它们组合成一个简单的结果,也就是持续迭代
    理解reduce的含义重点就在于理解"累 加 器" 的概念,并且每次运算的结果会作为下一次运算的一个参数
    Stream的一个参数和两个参数的方法的基本逻辑都是如此,差别仅仅在于一个参数的是result R = T1 ,然后再继续与剩下的元素参与运算

    分别为一个参数、两个参数、三个参数,这里需要先介绍一下这几个新接口:

    • BiFunction
      • public interface BiFunction<T, U, R>
      • R apply(T t, U u)
      • 默认方法andthen
      • 它与Function不同点在于它接收两个输入返回一个输出; 而Function接收一个输入返回一个输出。注意它的两个输入、一个输出的类型可以是不一样的。
    • BinaryOperator
      • public interface BinaryOperator<T> extends BiFunction<T,T,T>
      • 有两个静态方法:minBymaxBy
      • BiFunction的三个参数可以是一样的也可以不一样;而BinaryOperator就直接限定了其三个参数必须是一样的;因此BinaryOperator与BiFunction的区别就在这。它表示的就是两个相同类型的输入经过计算后产生一个同类型的输出。
    • BiConsumer
      • void accept(T t, U u)
      • Consumer变种,接收两个参数,不返回

    reduce一个接口参数可以很方便的进行求最大值、最小值、和,代码如下:

    Integer sum = s.reduce((a, b) -> a + b).get();
    Integer max = s.reduce((a, b) -> a >= b ? a : b).get();
    Integer min = s.reduce((a, b) -> a <= b ? a : b).get();
    

    reduce两个参数的对比一个参数的多了一个初始化的值,

    T result = identity;
    for (T element : this stream){
        result = accumulator.apply(result, element)
    }
    return result;
    

    所以reduce(0, (a, b) -> a + b)reduce((a, b) -> a + b)是完全等价的。

    reduice三个参数较为复杂,前两个参数与二参基本相同,第三个参数用于在并行计算下,合并各个线程的计算结果。需要注意的是,并行情况下初始值(也就是第一个参数)会被多次计算,具体可以参看此博客

    collect

    collect
    collect():<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)<R, A> R collect(Collector<? super T, A, R> collector)
    收集装置,可以将元素以特定方式收集起来。需要了解的是收集器的参数决定了收集器的行为
    这里需要说一下Collector接口,这里先上几个重要的静态方法:

    生成Collection

    • toCollection()将流转化为集合,参数为集合实现方式,比如:
    Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.toCollection(ArrayList::new));//以ArrayList的形式转化为集合
    
    • toList()toset()这两个可以视为上面的简化版本,无参,默认为ArrayList和HashSet实现:
    Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.toList());
    

    生成Map

    • toMap与上面类似,只不过需要指定map的键和值:
    Stream.of("My", "Java", "my", "Life!")
            .collect(Collectors.toMap(Function.identity(),//指定Key
                    s -> s.length()))//指定Value
    //注意,这里键是不能重复的,否则会抛出异常,所以我把第二个"My"改成了"my",想要键可重复请继续往下看。
    //运行结果
    {Java=4, Life!=5, My=2, my=2}
    
    • partitioningBy()适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,一参为一个Predicate接口,二参时可以将第二个参数指定为再一个Collector。
    Map<Boolean, List<String>>  map = Stream.of("My", "Java", "my", "Life!")
            .collect(Collectors.partitioningBy(s -> s.length() > 3));//以长度是否大于3分组
    //运行结果
    {false=[My, my], true=[Java, Life!]}
    
    //二参可以指定一个下游收集器
    Map<Boolean, Long> map = Stream.of("My", "Java", "my", "Life!")
            .collect(Collectors.partitioningBy(s -> s.length() > 4, Collectors.counting()));//以长度是否大于4分组,并计数
    //运行结果
    {false=3, true=1}
    
    • groupingBy():上面的partitioningBy()只能将数据分成两类,根本不够用是不是,groupingBy()则可以做到更多。它分别有一参、二参和三参:
    //一参可以当做partitioningBy用
    Map<Boolean, List<String>> map = Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.groupingBy(s -> s.length() > 4));//以长度是否大于4分组
    //运行结果
    {false=[My, Java, My], true=[Life!]}
    //也可以当做增强型
    Map<Integer, List<String>> map = Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.groupingBy(String::length));//以长度分组
    //运行结果
    {2=[My, My], 4=[Java], 5=[Life!]}
    
    //二参就更好用了
    Map<String, Long> map = Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.groupingBy(
                    Function.identity(),Collectors.counting()));//指定键为元素,值为出现次数,这种情况下键是可以重复的
    //运行结果
    {Java=1, Life!=1, My=2}
    //或者
    Map<Integer, Long> map = Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.groupingBy(String::length,Collectors.counting()));//以长度为键,出现次数为值
    //运行结果
    {2=2, 4=1, 5=1}
    
    //三参就更厉害了
    Map<Integer, List<String>> map = Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.groupingBy(
                    String::length,Collectors.mapping(s->s.toUpperCase(),Collectors.toList())));//注意这里下游收集齐还包含着更下游的收集齐
    //运行结果
    {2=[MY, MY], 4=[JAVA], 5=[LIFE!]}
    

    你可能觉得三参的collect用不到,但是请看这里:

    下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个Employee对象,可通过如下方式做到:

    // 按照部门对员工分布组,并只保留员工的名字
    Map<Department, List<String>> byDept = employees.stream()
                   .collect(Collectors.groupingBy(Employee::getDepartment,
                           Collectors.mapping(Employee::getName,// 下游收集器
                                   Collectors.toList())));// 更下游的收集器
    

    超强!参见这篇博客

    • joining()这是用来连接字符串的,无参为直接连接,一参指定连接符,三参可以指定连接符和首位字符:
    Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.joining());//MyJavaMyLife!
    Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.joining("-")//My-Java-My-Life!
    Stream.of("My", "Java", "My", "Life!")
            .collect(Collectors.joining("-","[","]"))//[My-Java-My-Life!]
    

    小结:用了Stream之后再也不想用普通的for循环了有木有啊!!!

  • 相关阅读:
    20172322 《程序设计与数据结构》第六周学习总结
    20172322 2018-2019-1 10月19日课上测试报告
    20172322 《程序设计与数据结构》第五周学习总结
    20172322 《程序设计与数据结构》第四周学习总结
    172322 2018-2019-1 《Java软件结构与数据结构》实验一报告
    20172322 《程序设计与数据结构》第三周学习总结
    20172322 《程序设计与数据结构》第二周学习总结
    20172303 20172322 2017-2018-2 暑假作业 结对编程项目-舒尔特方格(增补:计时器的加入与页面优化)
    20172322 《程序设计与数据结构》第一周学习总结
    20172322 2017-2018-2《程序设计与数据结构》(上)课程总结
  • 原文地址:https://www.cnblogs.com/lixin-link/p/11020407.html
Copyright © 2011-2022 走看看