函数式编程——Java中的lambda表达式

背景

在JDK1.8之前,我们经常会遇到下面这几种场景:

无法传入方法,只能传入对象
	Thread thread = new Thread(new Runnable() {
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread());
	    }
	});
即使是简单方法,也仍然需要创建完整函数体
    public int add(String s) {
        return a + b;
    }

这两种场景的缺点是:

  • 代码冗余严重
  • 使用不灵活

然而,在JDK1.8中,迎来了lambda表达式,上述两种代码段可以改造为下面的形式:

	Thread thread = new Thread(() -> System.out.println(Thread.currentThread()));
	(c1, c2) -> c1 + c2

具体是怎么操作的,我们接下来慢慢分析

介绍

函数式编程的一个特点就是函数可以作为参数和返回值,可以把函数当成一个字段值来进行使用,在JDK1.8之前,我们是没办法传入一个方法的,只能将方法封装到对象中,这样就带来了很大的不便,于是在JDK1.8中,引入了以Function为首的一批为函数式编程服务的接口

lambda表达式

lambda表达式的形式通常为:

(T t, V v) -> { /* code */ }

通常我们不能单独声明一个lambda表达式,就像我们不能单独声明一个字面值常量却不将它赋给任何变量,我们通常会手动约束需要使用lambda表达式参数的格式,所以可以简写成以下形式:

(t, v) -> { /* code */ }

再进一步化简

t, v -> /* code */

如果没有参数,则形式可以为:

() -> /* code */

@FunctionalInterface注解

在JDK1.8中,很多类(如Comparator)上都新增了一个@FunctionalInterface的注解,这个注解的原理在本篇中不深究,我们通过举一个例子来让大家了解这个注解的用处

这个注解是注解在接口上的,如下:

扫描二维码关注公众号,回复: 5908615 查看本文章
@FunctionalInterface
public interface Movable {

    void move(String person);
}

如果是在JDK1.7中,假如我们想临时创建一个实现Movable接口的对象,一般采用以下方式:

        Movable movable = new Movable() {
            
            @Override
            public void move(String person) {
            	// 这里我们就简单地打印下内容
                System.out.println(person);
            }
        };

但是现在,我们可以采用以下的方式:

	Movable movable = person -> System.out.println(person);

因为我们直接将参数原样输出,所以有以下的简化方式:

	Movable movable = System.out::println;

是不是简洁度一下子就上去了

我们把注解去掉,发现依然能编译通过,甚至也能正常运行,那可能就要怀疑了,这个注解岂不是没用?其实Java提供的很多原生注解基本都是用来约束开发的,也就是让你在码代码的时候就能发现错误,总比写完了编译运行的时候报错要好

JDK1.8中,把lambda表达式叫做SAM类型(Single Abstract Method),即单一抽象方法类型,从字面上来看是说一个接口中只有一个抽象方法

@FunctionalInterface作为一个编译级错误检查注解,当你想用一个lambda表达式来代替一个接口(更准确地说是接口中的方法)时,就需要满足以下规范,否则就会在编译时报错:

  • 注解的类型必须是一个接口
  • 接口中只能有一个抽象方法(静态方法、默认方法和重写Object的方法除外)

满足这两个条件的就可以用以上形式来方便地进行编码

Function系列接口

刚刚我们只说了怎么通过lambda表达式方便地创建一个实现接口的对象,现在我们就要来了解JDK1.8中提供的最重要的功能,将lambda表达式作为参数和返回值使用

既然是Function系列接口,那么肯定不止Function接口这一个,不过我们先把重点放在Function上,如下

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

	/* 在本方法执行前执行另一个方法 */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

	/* 在本方法执行后执行另一个方法 */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

可以看出,Function接口中只有一个抽象方法apply(),负责接收T参数,然后返回R参数,剩下的compose()方法可以在执行本方法前执行传入Function接口的apply()方法,andThen()则是与之相反,在本身方法之后执行

说了那么多,Function始终还是一个接口,到底怎么实例化?这时候又得说回lambda表达式了,Function系列接口都是通过一个lambda表达式来进行实例化的,如下:

        Function<Integer, String> func = num -> {
            System.out.println("Run Success...");
            return "[num]" + num;
        };

        String out = func.apply(20);
        System.out.println("out: " + out);

输出结果:

out: [num]20

还可以使用compose()方法在方法执行前进行其他操作,使用方法如下:

        Function<Integer, String> func = num -> "[num]" + num;
        Function<Integer, Integer> before = num -> num + 100;

        String out = func.compose(before).apply(20);
        System.out.println("out: " + out);

输出结果:

out: [num]120

这里一定要注意一点,如果我们把原Function接口声明为传入T参数输出R类型的话,before接口对象传入的参数一定要是最终的apply()方法传入参数的类型或其父类,返回对象一定要是T或T的子类,这里留一个简单的问题,建议自己思考一下andThen()方法传入的Function接口,它的参数和返回值需要满足需要满足什么约束

既然说了是Function接口系列,那自然有一系列类似的接口,总不能都是只传一个参数,返回一个参数的这种简单形式

这里我借鉴了一下java.util.function-Function接口这篇博文的表格,如下

结尾

JDK1.8中的函数式编程的特性基本就是这样了,在开发中虽然不推荐过度使用,不过很多时候合理地运用可以减少很多重复的代码,lambda表达式还是很值得学习的

猜你喜欢

转载自blog.csdn.net/qq_37435078/article/details/89048253