java8中的函数式编程

作者: agile4j | 来源:发表于2018-07-06 18:19 被阅读107次

    作者:刘仁鹏
    参考资料:

    1. 使用 Java8 Optional 的正确姿势
    2. 《Java8函数式编程》Richard Warburton 著 王群峰 译

    1.Lambda表达式与函数式接口

    1.Lambda表达式简介

    • Lambda表达式的作用:使得Java可以把 函数 像对象一样作为语言的 一等公民 来对待。从而:
      1. 能够编写出 可读性更强、更加紧凑、抽象级别更高的代码
      2. 易于编写出可在多核CPU上高效运行、线程安全 的代码
      3. 在编写 回调函数 和事件处理程序时,可以摆脱匿名内部类的冗繁
      4. ...
    • Java8的lambda表达式其实是 匿名类语法糖 ,本质上仍然是对象。
    • 函数成为 一等公民 的含义:可以将函数 赋值 给变量,可以在 参数 中传递函数,可以让一个方法的 返回值 是函数...
      即,凡是对象可以出现的地方,函数 都可以出现。

    2.Lambda表达式的用法

    • 一个lambda表达式可由 用逗号分隔的参数列表->符号函数体三部分组成,例如:
    Arrays.asList("a", "b", "c").forEach((String item) -> System.out.println(item));
    
    • 因为 参数 item的 类型 可由编译器 推测 出来,因此也可简写成:
    Arrays.asList("a", "b", "c").forEach(item -> System.out.println(item));
    
    • 上面的两种写法,等价于在Java8之前的版本中,使用 匿名类 的实现:
    Arrays.asList("a", "b", "c").forEach(
            new Consumer<String>() {
                @Override
                public void accept(String s) {
                    System.out.println(s);
                }
            }
    );
    
    • 如果lambda的函数体 非单一语句 ,需要把函数体放在 一对花括号 中:
    Arrays.asList("a", "b", "c").forEach((String item) -> {
        System.out.println(item);
        System.out.println(item);
    });
    
    • lambda可以使用外部作用域的 常量 (如果是变量而非常量,会 隐式地 转为常量,建议显式地声明为常量):
    public class Test {
    
        private int num = 1;
    
        public static void main(String[] args) {
            String separator = ",";
            Test test = new Test();
            Arrays.asList("a", "b", "c").forEach(
                    item -> System.out.println(item + separator + test.num));
            //separator = ";"; 
            //编译期错误 Variable used in lambda expression should be final or effectively final
            //test = new Test(); 
            //编译期错误 Variable used in lambda expression should be final or effectively final
        }
    
    }
    
    • lambda有可能会返回一个值。返回值 的类型也可由编译器 推断 出来。如果lambda的 函数体只有一行 的话,可以不显式使用 return语句,下面两个代码是等价的:
    Arrays.asList("a", "b", "c").sort((s1, s2) -> s1.compareTo(s2));
    
    Arrays.asList("a", "b", "c").sort((s1, s2) -> {
        return s1.compareTo(s2);
    });
    

    3.函数式接口简介

    • 所有的lambda表达式都可以替换成匿名类的实现方式,但并非所有的匿名类都能用lambda来表示。那么什么样的匿名类一定能用lambda来表示呢?那就是 实现于只有一个abstract方法的接口的匿名类 (不算Object中的方法,以及static方法和default方法)。而这种特殊的接口,在Java8中称为 函数式接口 ,用注解 @FunctionalInterface 表示:
    @FunctionalInterface
    public interface TestFunctional{
        void method();
    }
    
    • 但为什么只有这样的接口,才能和lambda配合使用呢?其实不难理解。因为lambda本质是 没有显式重写方法省略了重写方法名 的匿名类,而一个lambda只能对一个方法进行重写。
    • Java中所有 lambda表达式类型 都是某个具体的函数式接口
    • 一个 函数式接口 即使不加 @FunctionalInterface 注解,也可以与lambda配合使用,但这样的函数式接口是 容易出错 的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种 脆弱性 并且能够 明确声明 接口作为函数式接口的意图,建议显式使用该注解

    4.默认方法与静态方法

    • Java 8用 默认方法静态方法 这两个新概念来扩展接口的声明。
    • 默认方法 使用 default 关键字定义,默认方法与抽象方法不同,不需要被实现类来具体实现,但是可以被实现类继承或重写。默认方法的出现使Java可以 在扩展接口功能的同时保证向后兼容 (只要是Java1到Java7写出的代码,在Java8中依然可以编译通过)。例如Collection中的stream方法,如果Java8没有对默认方法的支持,那么所有的子类都需要对stream方法提供实现,显然无法保证向后兼容。
    • 静态方法 使用 static 关键字定义,与一般java类中的静态方法一样。静态方法的出现是为了 让工具方法和相关的类或接口放在一起,而不是放到另一个工具类中。例如下面的代码,如果Java8没有对静态方法的支持,那么ofAll方法必须放到另外的工具类Rulers中,显然不如直接放到Ruler中友好。
    • 示例:
    @FunctionalInterface
    public interface Ruler<T> {
    
        /**
         * 校验T
         */
        void check(T checkTarget);
    
        /**
         * 或操作
         */
        @SuppressWarnings("unchecked")
        default Ruler<T> or(Ruler<T>... rulers) {
            return checkTarget -> {
                try {
                    check(checkTarget);
                } catch (CheckException e) {
                    ofAll(rulers).check(checkTarget);
                }
            };
        }
    
        /**
         * Ruler整合
         */
        @SafeVarargs
        static <T> Ruler<T> ofAll(Ruler<T>... rulers) {
            return (checkTarget -> Arrays.stream(rulers).forEach(ruler -> ruler.check(checkTarget)));
        }
    
    }
    

    5.四大常用的函数式接口

    • 消费型接口 Consumer<T>
    /**
     * @name 消费型接口
     * @use Consumer<T>
     * @param T 传入参数
     * @fun 接受一个参数 无返回值
     * */
    Consumer<String> con = (str) -> System.out.println(str);
    con.accept("我是消费型接口!");
    //输出:我是消费型接口!
    
    • 供给型接口 Supplier<R>
    /**
     * @name 供给型接口
     * @use Supplier<R>
     * @param R 返回值类型
     * @fun 无参数 有返回值
     * */
    Supplier<Date> supp = () -> new Date();
    Date date = supp.get();
    System.out.println("当前时间:" + date);
    //输出:当前时间:Wed Jul 04 08:05:10 CST 2018
    
    • 函数型接口 Function<T,R>
    /**
     * @name 函数型接口
     * @use Function<T,R>
     * @param T 传入参数
     * @return R 返回值类型
     * @fun 接受一个参数 有返回值
     * */
    Function<String, String> fun = (str) -> "hello," + str;
    String str = fun.apply("tom");
    System.out.println(str);
    //输出:hello,tom
    
    • 断定型接口 Predicate<T>
    /**
     * @name 断定型接口
     * @use Predicate<T>
     * @param T 传入参数
     * @return Boolean 返回一个Boolean型值
     * @fun 接受一个参数 返回Boolean型值
     * */
    Predicate<Integer> pre = (num) -> num > 0;
    Boolean flag = pre.test(10);
    System.out.println(flag);
    //输出:true
    
    • 还有更多功能丰富的函数式接口,可自行了解。

    2.方法引用

    类型 示例
    构造器引用 Class::new
    静态方法引用 Class::static_method
    任意对象的方法引用 instance::method
    特定对象的方法引用 Class::method
    • 下面,我们以定义了4个方法的Car这个类为例子,区分Java8中支持的4种不同的方法引用。
    public static class Car {
        public static Car create(final Supplier<Car> supplier) {
            return supplier.get();
        }
    
        public static void collide(final Car car) {
            System.out.println("Collided " + car.toString());
        }
    
        public void follow(final Car another) {
            System.out.println("Following the " + another.toString());
        }
    
        public void repair() {
            System.out.println("Repaired " + this.toString());
        } 
    }
    

    1.构造器引用

    • 第一种方法引用是构造器引用,它的语法是Class::new,或者更一般的Class<T>::new。请注意构造器 没有参数
    final Car car1 = Car.create(() -> new Car()); //lambda表达式
    final Car car2 = Car.create(Car::new); //方法引用
    

    2.静态方法引用

    • 第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法 接受一个Car类型的参数
    final List<Car> cars = Arrays.asList(car1, car2);
    cars.forEach((Car it) -> Car.collide(it)); //lambda表达式
    cars.forEach(Car::collide); //方法引用
    

    3.任意对象的方法引用

    • 第三种方法引用是特定类的任意对象的方法引用,它的语法是Class::method。请注意,这个方法 没有参数
    cars.forEach((Car it) -> it.repair()); //lambda表达式
    cars.forEach(Car::repair); //方法引用
    

    4.特定对象的方法引用

    • 第四种方法引用是特定对象的方法引用,它的语法是instance::method。请注意,这个方法 接受一个Car类型的参数
    final Car police = new Car();
    cars.forEach((Car it) -> police.follow(it)); //lambda表达式
    cars.forEach(police::follow); //方法引用
    

    3.Optional

    • Java8引入 Optional 来通过一系列的 链式调用优雅地 解决 null安全问题

    1.Optional的三种构造方式

    • Optional.of(obj) :它要求传入的obj不能是null值, 否则会抛出NPE。
    • Optional.empty() :返回一个持有null的Optional实例。
    • Optional.ofNullable(obj) :它以一种智能的,宽容的方式来构造一个Optional实例。传null进到就得到Optional.empty(),非null就调用Optional.of(obj)。

    2.Optional的错误使用

    1. 使用 isPresent() 方法:
      isPresent() 与 obj != null 没有任何分别,并不会使代码变得优雅

    2. 使用 get() 方法:
      没有 isPresent() 作铺垫的 get() 调用在IDEA中会收到警告

      Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value, get() will throw an exception
      调用 Optional.get() 前不事先用 isPresent() 检查值是否可用. 假如 Optional 不包含一个值, get() 将会抛出一个异常

    3. Optional 类型作为 类/实例属性方法参数 时:
      把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中是强力不推荐的

      Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.
      使用任何像 Optional 的类型作为字段或方法参数都是不可取的. Optional 只设计为类库方法的, 可明确表示可能无值情况下的 返回类型 . Optional 类型不可被序列化, 用作字段类型会出问题的

    4. 错误示例:

    //不要这么写:
    Optional<User> userOpt = selectUserById(id);
    if (userOpt.isPresent()) {
        return userOpt.get().getName();
    } else {
        return "none";
    }
    
    //这其实与我们以前不使用Optional时的代码没有任何区别:
    User user = getUserById(id);
    if (user != null) {
        return user.getName();
    } else {
        return "none";
    }
    
    //正确的写法:
    return selectUserById(id).map(User::getName).orElse("none");
    

    3.Optional的常见用法

    • 存在即返回,无则提供默认值
      public T orElse(T other)
    return opt.orElse(null);
    //而不是 return opt.isPresent() ? opt.get() : null;
    
    • 存在即返回, 无则由函数来产生
      public T orElseGet(Supplier<? extends T> other)
    return user.orElseGet(() -> fetchAUserFromDatabase()); 
    //而不要 return user.isPresent() ? user: fetchAUserFromDatabase();
    
    • 存在才对它做点什么
      public void ifPresent(Consumer<? super T> consumer)
    user.ifPresent(System.out::println);
     
    //而不要
    if (user.isPresent()) {
      System.out.println(user.get());
    }
    
    • Optional最重要的用法
      public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
      map方法通常会搭配 orElse orElseGet orElseThrow 方法 使用:
    return selectUserById(id)
            .map(u -> u.getUsername())
            .map(name -> name.toUpperCase())
            .orElse(null);
               
    //在java8之前的版本,等价的写法是这样的:
    User user = getUserById(id);
    if (user != null) {
        String name = user.getUsername();
        if (name != null) {
            return name.toUpperCase();
        } else {
            return null;
        }
    } else {
        return null;
    }
    
    • flatMap filter 方法的使用与map类似,这里就不展开了。

    4.Stream

    1.Stream简介

    • Stream是用函数式编程方式,在 集合类 上进行复杂操作的工具:
    public static void main(String[] args) {
        long count1 = Stream.of(1, 2, 3)
                .filter(it -> it > 2)
                .count();
    
        long count2 = 0;
        List<Integer> list = Arrays.asList(1, 2, 3);
        for (Integer it : list) {
            if (it > 2) {
                count2++;
            }
        }
    
    
        System.out.println("count1:" + count1 + "---count2:" + count2);
        //输出:count1:1---count2:1
    }
    
    • 上述代码实现了一种“获取集合中值大于2的元素个数”的功能。整个过程被 分解 为两种更简单的操作:过滤计数。看似进行了两次遍历操作,事实上,类库巧妙的设计使得只对集合进行了一次遍历:Stream方法的返回值并不是一个新集合,而是一个Stream对象,是用来创建新集合的 配方(算法)。
    • 像filter这样只 描述 Stream,而不产生新集合的方法叫做 惰性求值方法;而像count这样用来从Stream中产生值的方法叫做 及早求值方法
    • 只有调用了 及早求值方法 ,Stream中之前注入的 惰性求值方法 才会被真正的运行,否则,惰性求值方法 只会像 势能 一样,悬而不发:
    // 势能存储
    Stream<Integer> potentialEnergy = Stream.of(1, 2, 3)
            .filter(it -> {
                System.out.println("hello");
                return it > 2;
            });
    System.out.println("world");
    
    // 势能释放
    long count = potentialEnergy.count();
    System.out.println("count:" + count);
    
    // 尝试将势能再次释放(抛出异常)
    long countAgain = potentialEnergy.count();
    System.out.println("countAgain:" + countAgain);
    
    //输出:
    //world 
    //hello
    //hello
    //hello
    //count:1
    //java.lang.IllegalStateException: stream has already been operated upon or closed
    
    • 判断一个方法是 惰性求值 的还是 及早求值 的很简单:看它的 返回值。如果是 Stream 类型的就是惰性求值;否则就是及早求值。
    • 对Stream的使用方式是:形成一个惰性求值的 ,最后调用一个及早求值的方法返回想要的结果,或执行预期的操作。

    为方便演示,这里定义了一个字段、两个实体类、一个main方法。下文的示例将放到main方法中,会用到这部分代码。

    public class Test {
    
        public static void main(String[] args) {
            //示例代码
        }
    
        private static List<Business> businessList = Arrays.asList(
                new Business(1L, "家政", Arrays.asList(
                        new Category(11L, "保洁"),
                        new Category(12L, "保姆")
                )),
                new Business(2L, "速运", Arrays.asList(
                        new Category(21L, "搬家"),
                        new Category(22L, "货运")
                ))
        );
    
        @Data
        @AllArgsConstructor
        static class Business {
            private long id;
            private String name;
            private List<Category> categoryList;
        }
    
        @Data
        @AllArgsConstructor
        static class Category {
            private long id;
            private String name;
        }
    }
    

    2.常用惰性求值方法

    • map:一对一映射
    businessList.stream()
            .map(Business::getName)
            .forEach(System.out::println);
    //输出:
    //家政
    //速运
    
    • flatMap:一对多映射
    businessList.stream()
            .flatMap(business -> business.getCategoryList().stream())
            .forEach(System.out::println);
    //输出:
    //Test.Category(id=11, name=保洁)
    //Test.Category(id=12, name=保姆)
    //Test.Category(id=21, name=搬家)
    //Test.Category(id=22, name=货运)
    
    • filter:过滤
    businessList.stream()
            .filter(business -> business.getId() > 1)
            .forEach(System.out::println);
    //输出:
    //Test.Business(id=2, name=速运, categoryList=[Test.Category(id=21, name=搬家), Test.Category(id=22, name=货运)])
    

    3.常用及早求值方法

    • forEach:对每个元素执行指定操作(演示略)
    • reduce:归约
    long sumOfCategoryId = businessList.stream()
            .flatMap(business -> business.getCategoryList().stream())
            .map(Category::getId)
            .reduce((x, y) -> x + y).orElse(0L);
    System.out.println(sumOfCategoryId);
    //输出:66
    
    • collect:收集
    List categoryIdList = businessList.stream()
            .flatMap(business -> business.getCategoryList().stream())
            .map(Category::getId)
            .collect(Collectors.toList());
    System.out.println(categoryIdList);
    //输出:[11, 12, 21, 22]
    

    5.Collector

    • Collector(收集器) 是一种通用的,从流生成复杂值的结构。
    • 标准类库java.util.stream.Collectors,已经提供了一些常用的收集器
    • 如果标准类库中的收集器无法满足需求,也可 定制 一个收集器。
    • 接下来先对标准库中的收集器做简要介绍,然后再说明如何定制收集器。

    1.转换成其他集合

    • toList(),toSet(),toCollection()
    //当希望使用集合对象/实现来收集值时,可以使用toCollection
    TreeSet rst = businessList.stream()
            .map(Business::getName)
            .collect(Collectors.toCollection(TreeSet::new));
    System.out.println(rst);
    //输出:[家政, 速运]
    

    2.转换成值

    • 按某种特定顺序生成一个值:maxBy(),minBy()
    //获取id值最大的业务线
    Optional<Business> rst = businessList.stream()
            .collect(Collectors.maxBy(Comparator.comparing(Business::getId)));
            //等价于 .max(Comparator.comparing(Business::getId));
    System.out.println(rst.map(Business::getId).orElse(0L));
    //输出:2
    
    • 实现一些常用的数值计算:averagingXX(),summingXX()
    //求所有业务线的id均值
    Double rst = businessList.stream()
            .collect(Collectors.averagingDouble(Business::getId));
    System.out.println(rst);
    //输出:1.5
    

    3.数据分块

    • 将一个流根据指定的Predicate对象,分解成两个集合


      图片.png-66.9kB图片.png-66.9kB
    //根据业务线id是否大于1分块
    Map<Boolean, List<Business>> rst = businessList.stream()
            .peek(business -> business.setCategoryList(null))
            .collect(Collectors.partitioningBy(business -> business.getId() > 1));
    System.out.println(rst);
    //输出:{false=[Test.Business(id=1, name=家政, categoryList=null)], true=[Test.Business(id=2, name=速运, categoryList=null)]}
    

    4.数据分组

    • 数据分开只能将数据分成两部分,而数据分组不受此限制,可以更自然地分割数据(类似与SQL中的 group by 操作)


      图片.png-61.2kB图片.png-61.2kB
        @Data
        @AllArgsConstructor
        static class Custom {
            private long id;
            private String name;
            private String city;
        }
        
        public static void main(String[] args) {
            List<Custom> customList = Arrays.asList(
                    new Custom(1L, "张三", "北京"),
                    new Custom(2L, "李四", "北京"),
                    new Custom(3L, "tom", "纽约"),
                    new Custom(4L, "jerry", "纽约"),
                    new Custom(5L, "thomas", "纽约")
            );
    
            //根据城市分组
            Map<String, List<Custom>> rst = customList.stream()
                    .collect(Collectors.groupingBy(Custom::getCity));
            System.out.println(rst);
        }
    //输出:{纽约=[Test.Custom(id=3, name=tom, city=纽约), Test.Custom(id=4, name=jerry, city=纽约), Test.Custom(id=5, name=thomas, city=纽约)], 北京=[Test.Custom(id=1, name=张三, city=北京), Test.Custom(id=2, name=李四, city=北京)]}
    

    5.字符串

    • 通过指定 分隔符、前缀、后缀 的方式生成一个格式化后的字符串:joining()
    String rst = businessList.stream()
            .map(Business::getName)
            .collect(Collectors.joining(",", "[", "]"));
    System.out.println(rst);
    //输出:[家政,速运]
    

    6.组合收集器

    • 组合收集器分为 主收集器下游收集器,主收集器会用到下游收集器,下游收集器是用来生成最终values的 配方
    List<Custom> customList = Arrays.asList(
            new Custom(1L, "张三", "北京"),
            new Custom(2L, "李四", "北京"),
            new Custom(3L, "tom", "纽约"),
            new Custom(4L, "jerry", "纽约"),
            new Custom(5L, "thomas", "纽约")
    );
    
    Map<String, List<Long>> rst = customList.stream()
            .collect(Collectors.groupingBy
                    (Custom::getCity,
                            Collectors.mapping(Custom::getId, Collectors.toList())));
    System.out.println(rst);
    //输出:{纽约=[3, 4, 5], 北京=[1, 2]}
    

    7.定制收集器

    • 收集器接口的方法介绍
    1. supplier():用来创建容器的工厂方法,和reduce操作的第一个参数类似,是后续操作的 初值
      图片.png-38.2kB图片.png-38.2kB
    2. accumulator():该方法的作用和reduce操作的第二个参数一样,用来 结合 之前操作的结果和当前值,生成并返回新的值。
      图片.png-40kB图片.png-40kB
    3. combiner():如果有多个容器,会通过该方法 合并为一个容器。
      图片.png-38.7kB图片.png-38.7kB
    4. finisher():对容器进行 转换 以得到预期的结果值。
      finisher.png-35.2kBfinisher.png-35.2kB
    • 定制一个字符串拼接收集器
        public static void main(String[] args) {
            List<Custom> customList = Arrays.asList(
                    new Custom(1L, "张三", "北京"),
                    new Custom(2L, "李四", "北京"),
                    new Custom(3L, "tom", "纽约"),
                    new Custom(4L, "jerry", "纽约"),
                    new Custom(5L, "thomas", "纽约")
            );
    
            String rst = customList.parallelStream()
                    .map(Custom::getName)
                    .collect(new MyStrCollector());
    
            System.out.println(rst);
            //输出:[张三,李四,tom,jerry,thomas]
        }
    
        //泛型含义:<待收集元素的类型,累加器的类型,最终结果的类型>
        static class MyStrCollector implements Collector<String, StringBuilder, String> {
    
            private final String prefix = "[";
            private final String separator = ",";
            private final String suffix = "]";
    
            @Override
            public Supplier<StringBuilder> supplier() {
                return StringBuilder::new;
            }
    
            @Override
            public BiConsumer<StringBuilder, String> accumulator() {
                return (stringBuilder, item) -> {
                    stringBuilder.append(separator).append(item);
                };
            }
    
            @Override
            public BinaryOperator<StringBuilder> combiner() {
                return StringBuilder::append;
            }
    
            @Override
            public Function<StringBuilder, String> finisher() {
                return (stringBuilder -> prefix + stringBuilder.toString().substring(1) + suffix);
            }
    
            @Override
            public Set<Characteristics> characteristics() {
                return Collections.emptySet();
            }
        }
    
    

    end

    相关文章

      网友评论

        本文标题:java8中的函数式编程

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