1、为什么要函数式编程

不管是C语言,Java,还是数据库的SQL,函数Function绝对是我们最常打交道的对象。今天要介绍的“函数式编程”,是指Java8引入的新语法,虽然Java8在2014年就推出了,我想应该还是有很多同学不太了解。
我们以前接触的函数,基本都是接受变量参数,返回结果参数。这些参数可以是基本变量、对象和数组等,但能不能是一段代码块呢?Java里面代码块都能抽象成一个函数,所谓函数式编程,就是在定义一个函数时,它的参数也是一个函数的接口。在调用函数时,再去实现这个函数的接口,这个过程,也就出现了lambda表达式。

如下文,我们定义了一个函数 - 通知所有员工。

    /**
     * 通知员工
     * @param notify
     */
    public static void notifyEmployees( Consumer<Employee> notify){
        List<Employee>  employeeList=employeeMapper.getAllEmployees();
        for(Employee employee : employeeList){
            notify.accept(employee);
        }
    }

但是我们没有定义怎么去通知员工,可能是邮件通知,可能是短信通知,也有可能是微信通知,等等。我们将具体通知的方式,作为这个函数的一个传入参数。这里借助了Java自带的函数式接口 Consumer<T> ,后面会介绍它怎么用。那么我在调用这个方法的时候,就可以根据情况去实现这个函数式接口。

        //通过邮件
        notifyEmployees((employee)->{
            sendEmail(employee.getEmail());
        });

        //通过短信
        notifyEmployees((employee)->{
            sendSms(employee.getPhoneNumber());
        });
    // 等等 ...

2、Lambda表达式

Lambda表达式支持将代码块作为方法参数,Lambda 表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。

在上文实现函数式编程时,你或许感觉奇怪,为什么通过 -> 箭头之类的语法,就能实现接口?这就是Lambda表达式的语法,其实是对我们以前创建匿名内部类的简写。

匿名内部类也是一种简化代码的方式,我最早接触到的匿名内部类,就是在多线程中创建Thread对象时,通过实现Runnable接口来创建。

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"开始了!");
            }
        });
        thread.start();

实际上new Thread(Runnable target) 接收的参数,是实现了Runnable接口的类的对象。但由于我们关心的是接口里面的实现方法,而不是类的名字,所以用匿名内部类的方式来创建对象。
但是在看了Runnable接口的定义后,你会发现这个类只有一个抽象方法,这就又让人很不爽。我关心的只是这个类里面的这一个抽象方法,能不能不去实现这个类?Lambda表达式就应运而生了,它让代码看起来更简洁美观。

        Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName() + "开始了!"));
        thread.start();

3、函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。

我们再看一下Runnable这个函数式接口。

        @FunctionalInterface
        public interface Runnable {
            public abstract void run();
        }

函数式接口的特点如下:

  • 接口有且仅有一个抽象方法
  • 允许定义静态方法
  • 允许定义默认方法
  • 允许java.lang.Object中的public方法
  • 注解@FunctionInterface不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错,类似于@Override 注解。

Java8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在java.util.function下。

  1. Predicate<T> - 断言

接受参数对象T,返回一个boolean结果

        Predicate<String> isAdminPre=(String username)-> "admin".equals(username);

        System.out.println(isAdminPre.test("kerry"));
        System.out.println(isAdminPre.test("admin"));
  1. Consumer<T> - 消费者

接收参数对象T,不返回结果

        Consumer<String> msgCon = (String msg) -> {
            System.out.println("发送消息" + msg);
            System.out.println("消息发送完成");
        };

        msgCon.accept("01!01!我是02!");
  1. Function<T,R> - 函数

接收参数对象T,返回结果对象R

        Function<Date,String> timeFunc=(Date date)->{
            Calendar calendar=Calendar.getInstance();
            calendar.setTime(date);
            int hour=calendar.get(Calendar.HOUR_OF_DAY);
            if(hour>=6 && hour<12){
                return "上午";
            }else if(hour>=12&& hour<18){
                return "下午";
            }else {
                return "晚上";
            }
        };

        System.out.println(timeFunc.apply(new Date()));
  1. Supplier<T> - 供应者

不接受参数,提供对象T的创建工程

        Supplier<String> uuidSup=()-> UUID.randomUUID().toString().replace("-","");

        System.out.println(uuidSup.get());
  1. UnaryOperator<T> - 一元运算

接收参数对象T,返回结果对象T

        UnaryOperator<String> trimUO=(String str)->{
            if(str ==null){
                return str;
            }
            return str.trim();
        };
        System.out.println(trimUO.apply("  Hello  "));
  1. BinaryOperator<T> - 二元运算

接收两个T对象,返回一个T对象结果

        BinaryOperator<Integer> maxBO=(Integer i1,Integer i2)-> i1>i2?i1:i2;
        System.out.println(maxBO.apply(13,20));

4、函数式数据处理Stream

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream API 借助于Lambda 表达式,极大的提高编程效率和程序可读性。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
我个人感觉,Stream有点像是用Java代码编写的Hadoop。Hadoop利用分布式服务器,提高并行计算的能力,而Stream也同样可以通过多线程,提高数据处理的能力。它们也同样用map和reduce,做数据的映射和整合。
流的操作类型分为两种:

  1. Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  2. Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

常见的流操作方法有如下:

  1. Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  1. Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  1. Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

关于Stream的知识点比较多,《Java 8 中的 Streams API 详解》 这篇文章是我看到比较好的Stream系列博客,建议大家直接看这篇文章。

5、非空处理Optional

Optional有点像Stream,是Java8推出的处理数据的包装类,很多方法的实现都和lambda表达式结合在一起,推近了函数式编程风格。Optional主要用来处理常见的空指针异常,学会它,你的代码会美观很多。

  1. 创建Optional实例

可通过 Optional.ofNullable(T value)和Optional.of(T value),前者允许被包装的对象为空。而后者,如果被包装的对象为空还是会报空指针异常。
从Optional实例中取回实际值对象的方法之一是使用 get()方法,不过这个方法会在值为null的时候抛出异常。要避免异常,你可以选择先用isPresent()验证是否有值。

        User user=new User();
        Optional<User> userOptional=Optional.ofNullable(user);
    //或
        Optional<User> userOptional=Optional.of(user);
  1. isPresent()

Optional对象的isPresent()方法返回boolean值,如果为空返回false,不为空返回true。

  1. ifPresent(Customer < T > customer)

该方法除了执行检查,还接受一个Consumer(消费者) 参数,如果对象不是空的,就对执行传入的 Lambda 表达式.

  1. 默认值 orElse()、orElseGet()

orElse(),如果有值则返回该值,否则返回传递给它的参数值。
orElseGet(),这个方法会在有值的时候返回值,如果没有值,它会执行作为参数传入的 Supplier 函数式接口,并将返回其执行结果。

        User newUser=Optional.ofNullable(user).orElse(new User("anonymous"));
    //或
        User newUser=Optional.ofNullable(user).orElseGet(()->{
            User sUser=new User("anonymous");
            sUser.setAge(25);
            return sUser;
        });

值得注意的是,就算对象不为空,orElse() 方法仍然会创建User对象,只是不会给newUser赋值。而orElseGet() 方法,只有当对象为空的时候才会创建 User 对象。这个差异,在方法调用量大的时候,对性能产生的影响差异比较大。

  1. 返回异常 orElseThrow()

orElseThrow() 会在对象为空的时候抛出异常,而不是返回默认值,这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出 NullPointerException。

  1. 数据转换 map()、flatMap()

map() 对值应用(调用)作为参数的函数,然后将返回的值包装在Optional中。这就使对返回值进行链试调用的操作成为可能 。
flatMap() 也需要函数作为参数,并对值调用这个函数,但是会直接返回结果,并不会包装在Optional中。

        User user=new User();
        String name=Optional.ofNullable(user)
                .map(User::getName)
                .orElse("anonymous");
        System.out.println(name);

map常用于链式方法判断中,例如上文代码实际上等于:

        User user=new User();
        String name="anonymous";
        if(user!=null){
            if(user.getName()!=null){
                name=user.getName();
            }
        }
        System.out.println(name);
  1. 过滤 filter()

filter() 接受一个 Predicate 参数,返回测试结果为 true 的值。如果测试结果为 false,会返回一个空的 Optional。

        User user=new User("Kerry");
        user.setEmail("kerry.wu@definesys.com");
        Optional<User> filterUser=Optional.ofNullable(user)
                .filter(u->u.getEmail()!=null&&u.getEmail().contains("@"));

KerryWu
633 声望157 粉丝

保持饥饿