资讯专栏INFORMATION COLUMN

Java 8th 函数式编程:lambda 表达式

luffyZh / 719人阅读

摘要:自定义函数式接口我们在前面例子中实现的苹果筛选接口就是一个函数式接口定义如下,正因为如此我们可以将筛选逻辑参数化,并应用表达式仅包含一个抽象方法,依照定义可以将其视为一个函数式接口。

Lambda 表达式是 java 8th 给我们带来的几个重量级新特性之一,借用 lambda 表达式可以让我们的程序设计更加简洁。最近新的项目摒弃了 6th 版本,全面基于 8th 进行开发,本文将探讨 行为参数化lambda 表达式 , 以及 方法引用 等知识点。

一. 行为参数化

行为参数化简单的说就是将方法的逻辑以参数的形式传递到方法中,方法主体仅包含模板类通用代码,而一些会随着业务场景而变化的逻辑则以参数的形式传递到方法之中,采用行为参数化可以让程序更加的通用,以应对频繁变更的需求。

这里我们以 java 8 in action 中的例子进行说明。考虑一个业务场景,假设我们需要通过程序对苹果按照一定的条件进行筛选,我们先定义一个苹果实体:

public class Apple {
    /** 编号 */
    private Long id;
    /** 颜色 */
    private Color color;
    /** 重量 */
    private Float weight;
    /** 产地 */
    private String origin;

    public Apple() {
    }

    public Apple(Long id, Color color, Float weight, String origin) {
        this.id = id;
        this.color = color;
        this.weight = weight;
        this.origin = origin;
    }

    // 省略getter和setter
}

用户最开始的需求可能只是简单的希望能够通过程序筛选出绿色的苹果,于是我们可以很快的通过程序实现:

public static List filterGreenApples(List apples) {
    List filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        // 筛选出绿色的苹果
        if (Color.GREEN.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

如果过了一段时间用户提出了新的需求,希望能够通过程序筛选出红色的苹果,于是我们又需要针对性的添加了筛选红色苹果的功能:

public static List filterRedApples(List apples) {
    List filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        // 筛选出红色的苹果
        if (Color.RED.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

更通用的实现是把颜色作为一个参数传递到方法中,这样就可以应对以后用户提出的各种颜色筛选需求:

public static List filterApplesByColor(List apples, Color color) {
    List filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        // 依据传入的颜色参数进行筛选
        if (color.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

这样的设计再也不用担心用户的颜色筛选需求变化了,但是不幸的是某一天用户提了一个需求希望能够筛选重量达到某一标准的苹果,有了前面的教训我们也把重量的标准作为参数传递给筛选函数:

public static List filterApplesByColorAndWeight(List apples, Color color, float weight) {
    List filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        // 依据颜色和重量进行筛选
        if (color.equals(apple.getColor()) && apple.getWeight() >= weight) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

这样通过传递参数的方式真的好吗?如果筛选条件越来越多,组合模式越来越复杂,我们是不是需要考虑到所有的情况,并针对每一种情况都实现相应的策略呢?并且这些函数仅仅是筛选条件的部分不一样,其余部分都是相同的模板代码(遍历集合),这个时候我们就可以将行为进行 参数化 处理,让函数仅保留模板代码,而把筛选条件抽离出来当做参数传递进来,在 java 8th 之前,我们通过定义一个过滤器接口来实现:

// 过滤器
public interface AppleFilter {
    boolean accept(Apple apple);
}

// 应用过滤器的筛选方法
public static List filterApplesByAppleFilter(List apples, AppleFilter filter) {
    List filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (filter.accept(apple)) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

通过上面行为抽象化之后,我们可以在具体调用的地方设置筛选条件,并将条件作为参数传递到方法中:

public static void main(String[] args) {
    List apples = new ArrayList<>();

    // 筛选苹果
    List filterApples = filterApplesByAppleFilter(apples, new AppleFilter() {
        @Override
        public boolean accept(Apple apple) {
            // 筛选重量大于100g的红苹果
            return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100;
        }
    });
}

上面的行为参数化方式采用匿名类实现,这样的设计在 jdk 内部也经常采用,比如 java.util.Comparatorjava.util.concurrent.Callable 等,使用这类接口的时候,我们都可以在具体调用的地方用匿名类指定函数的具体执行逻辑,不过从上面的代码块来看,虽然很极客,但是不够简洁,在 java 8th 中我们可以通过 lambda 表达式进行简化:

// 筛选苹果
List filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

如上述所示,通过 lambda 表达式极大精简了代码,同时行为参数让我们的程序极大的增强了可扩展性。

二. Lambda 表达式 2.1 Lambda 表达式的定义与形式

我们可以将 lambda 表达式定义为一种 __简洁、可传递的匿名函数__,首先我们需要明确 lambda 表达式本质上是一个函数,虽然它不属于某个特定的类,但具备参数列表、函数主体、返回类型,甚至能够抛出异常;其次它是匿名的,lambda 表达式没有具体的函数名称;lambda 表达式可以像参数一样进行传递,从而简化代码的编写,其格式定义如下:

参数列表 -> 表达式

参数列表 -> {表达式集合}

需要注意 lambda 表达式隐含了 return 关键字,所以在单个的表达式中,我们无需显式的写 return 关键字,但是当表达式是一个语句集合的时候则需要显式添加 return 关键字,并用花括号 {} 将多个表达式包围起来,下面看几个例子:

// 1. 返回给定字符串的长度(隐含return语句)
(String s) -> s.length()

// 2. 始终返回42的无参方法(隐含return语句)
() -> 42

// 3. 包含多行表达式,需用花括号括起来,并显示添加return
(int x, int y) -> {
    int z = x * y;
    return x + z;
}
2.2 基于函数式接口使用 lambda 表达式

lambda 表达式的使用需要借助于 __函数式接口__,也就是说只有函数式接口出现地方,我们才可以将其用 lambda 表达式进行简化。那么什么是函数接口?函数接口的定义如下:

函数式接口定义为仅含有一个抽象方法的接口。

按照这个定义,我们可以确定一个接口如果声明了两个或两个以上的方法就不叫函数式接口,需要注意一点的是 java 8th 为接口的定义引入了默认的方法,我们可以用 default 关键字在接口中定义具备方法体的方法,这个在后面的文章中专门讲解,如果一个接口存在多个默认方法,但是仍然仅含有一个抽象方法,那么这个接口也符合函数式接口的定义。

2.2.1 自定义函数式接口

我们在前面例子中实现的苹果筛选接口就是一个函数式接口(定义如下),正因为如此我们可以将筛选逻辑参数化,并应用 lambda 表达式:

@FunctionalInterface
public interface AppleFilter {
    boolean accept(Apple apple);
}

AppleFilter 仅包含一个抽象方法 accept(Apple apple),依照定义可以将其视为一个函数式接口。在定义时我们为该接口添加了 @FunctionalInterface 注解,用于标记该接口是一个函数式接口,不过该注解是可选的,当添加了该注解之后,编译器会限制了该接口只允许有一个抽象方法,否则报错,所以推荐为函数式接口添加该注解。

2.2.2 jdk 自带的函数式接口

jdk 为 lambda 表达式已经内置了丰富的函数式接口,如下表所示(仅列出部分):

函数式接口 函数描述符 原始类型特化
Predicate T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer T -> void IntConsumer, LongConsumer, DoubleConsumer
Funcation T -> R IntFuncation, IntToDoubleFunction, IntToLongFunction, LongFuncation...
Supplier () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator (T, T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate (L, R) -> boolean
BiConsumer (T, U) -> void
BiFunction (T, U) -> R

其中最典型的三个接口是 PredicateConsumer,以及 Function,其余接口几乎都是对这三个接口的定制化,下面就这三个接口举例说明其用处,针对接口中提供的逻辑操作默认方法,留到后面介绍接口的 default 方法时再进行说明。

Predicate

@FunctionalInterface
public interface Predicate {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

Predicate 的功能类似于上面的 AppleFilter,利用我们在外部设定的条件对于传入的参数进行校验并返回验证通过与否,下面利用 Predicate 对 List 集合的元素进行过滤:

private  List filter(List numbers, Predicate predicate) {
    Iterator itr = numbers.iterator();
    while (itr.hasNext()) {
        if (!predicate.test(itr.next())) {
            itr.remove();
        }
        itr.next();
    }
    return numbers;
}

上述方法的逻辑是遍历集合中的元素,通过 Predicate 对集合元素进行验证,并将验证不过的元素从集合中移除。我们可以利用上面的函数式接口筛选整数集合中的偶数:

PredicateDemo pd = new PredicateDemo();
List list = new ArrayList<>();
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
list = pd.filter(list, (value) -> value % 2 == 0);
System.out.println(list);
// 输出:[2, 4, 6]

Consumer

@FunctionalInterface
public interface Consumer {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

Consumer 提供了一个 accept 抽象函数,该函数接收参数并依据传递的行为应用传递的参数值,下面利用 Consumer 遍历字符串集合并转换成小写进行打印:

private  void forEach(List list, Consumer consumer) {
    for (final T value : list) {
        // 应用行为
        consumer.accept(value);
    }
}

利用上面的函数式接口,遍历字符串集合并以小写形式打印输出:

ConsumerDemo cd = new ConsumerDemo();
List list = new ArrayList<>();
list.addAll(Arrays.asList("I", " ", "Love", " ", "Java", " ", "8th"));
cd.forEach(list, (value) -> System.out.print(value.toLowerCase()));
// 输出:i love java 8th

Function

@FunctionalInterface
public interface Function {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

Funcation 执行转换操作,输入类型 T 的数据,返回 R 类型的结果,下面利用 Function 对字符串集合转换成整型集合,并忽略掉不是数值型的字符:

private List parse(List list, Function function) {
    List result = new ArrayList<>();
    for (final String value : list) {
        // 应用数据转换
        if (NumberUtils.isDigits(value)) result.add(function.apply(value));
    }
    return result;
}

下面利用上面的函数式接口,将一个封装字符串的集合转换成整型集合,忽略不是数值形式的字符串:

FunctionDemo fd = new FunctionDemo();
List list = new ArrayList<>();
list.addAll(Arrays.asList("a", "1", "2", "3", "4", "5", "6"));
List result = fd.parse(list, (value) -> Integer.valueOf(value));
System.out.println(result);
// 输出:[1, 2, 3, 4, 5, 6]
2.2.3 一些需要注意的事情

类型推断

在编码过程中,有时候可能会疑惑我们的调用代码会具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等因素做正确的判定。在具体调用时,一些时候可以省略参数的类型以进一步简化代码:

// 筛选苹果
List filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

// 某些情况下我们甚至可以省略参数类型,编译器会根据上下文正确判断
List filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

局部变量

上面所有例子中使用的变量都是 lambda 表达式的主体参数,我们也可以在 lambda 中使用实例变量、静态变量,以及局部变量,如下代码为在 lambda 表达式中使用局部变量:

int weight = 100;
List filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);

上述示例我们在 lambda 中使用了局部变量 weight,不过在 lambda 中使用局部变量还是有很多限制,学习初期 IDE 可能经常会提示我们 Variable used in lambda expression should be final or effectively final 的错误,即要求在 lambda 表达式中使用的变量必须 __显式声明为 final 或事实上的 final 类型__。

为什么要限制我们直接使用外部的局部变量呢?主要原因在于内存模型,我们都知道实例变量在堆上分配的,而局部变量在栈上进行分配,lambda 表达式运行在一个独立的线程中,了解 JVM 的同学应该都知道栈内存是线程私有的,所以局部变量也属于线程私有,如果肆意的允许 lambda 表达式引用局部变量,可能会存在局部变量以及所属的线程被回收,而 lambda 表达式所在的线程却无从知晓,这个时候去访问就会出现错误,之所以允许引用事实上的 final(没有被声明为 final,但是实际中不存在更改变量值的逻辑),是因为对于该变量操作的是变量副本,因为变量值不会被更改,所以这份副本始终有效。这一限制可能会让刚刚开始接触函数式编程的同学不太适应,需要慢慢的转变思维方式。

实际上在 java 8th 之前,我们在方法中使用内部类时就已经遇到了这样的限制,因为生命周期的限制 JVM 采用复制的策略将局部变量复制一份到内部类中,但是这样会带来多个线程中数据不一致的问题,于是衍生了禁止修改内部类引用的外部局部变量这一简单、粗暴的策略,只不过在 8th 之前必须要求这部分变量采用 final 修饰,但是 8th 开始放宽了这一限制,只要求所引用变量是 “事实上” 的 final 类型即可。

三. 方法引用

方法引用可以更近一步的简化代码,有时候这种简化让代码看上去更加直观,先看一个例子:

/* ... 省略apples的初始化操作 */

// 采用lambda表达式
apples.sort((Apple a, Apple b) -> Float.compare(a.getWeight(), b.getWeight()));

// 采用方法引用
apples.sort(Comparator.comparing(Apple::getWeight));

方法引用通过 :: 将方法隶属和方法自身连接起来,主要分为三类:

静态方法

(args) -> ClassName.staticMethod(args)
转换成:
ClassName::staticMethod

参数的实例方法

(args) -> args.instanceMethod()
转换成:
ClassName::instanceMethod  // ClassName是args的类型

外部的实例方法

(args) -> ext.instanceMethod(args)
转换成:
ext::instanceMethod(args)

鉴于作者水平有限,文中不免有错误之处,欢迎批评指正
个人博客:www.zhenchao.org

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70650.html

相关文章

  • Java 8th 函数编程:Optional 类型

    摘要:当满足条件时执行传入的参数化操作。最后提醒一点,好用但不能滥用,在设计一个接口方法时是否采取类型返回需要斟酌,一味的使用会让代码变得比较啰嗦,反而破坏了代码的简洁性。鉴于作者水平有限,文中不免有错误之处,欢迎批评指正个人博客 NullPointException 可以说是所有 java 程序员都遇到过的一个异常,虽然 java 从设计之初就力图让程序员脱离指针的苦海,但是指针确实是实际...

    RayKr 评论0 收藏0
  • 深入浅出 Java 8 Lambda 表达

    摘要:在支持一类函数的语言中,表达式的类型将是函数。匿名函数的返回类型与该主体表达式一致如果表达式的主体包含一条以上语句,则表达式必须包含在花括号中形成代码块。注意,使用表达式的方法不止一种。 摘要:此篇文章主要介绍 Java8 Lambda 表达式产生的背景和用法,以及 Lambda 表达式与匿名类的不同等。本文系 OneAPM 工程师编译整理。 Java 是一流的面向对象语言,除了部分简...

    wdzgege 评论0 收藏0
  • Java8-1-初识Lambda表达函数接口

    摘要:而在中,表达式是对象,它们必须依附于一类特别的对象类型函数式接口。即表达式返回的是函数式接口类型。 Java8被称作Java史上变化最大的一个版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和Stream API。这两者也可以结合在一起使用。首先来看下什么是Lambda表达式。Lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,感觉看到这个解释...

    jzman 评论0 收藏0
  • 企业级lambda表达,让你对lambda有更好的理解

    摘要:但,如果加入了函数式编程,也就是将方法作为形参传递,这必然让开发者为难。但是,其他语言早就使用了函数式编程,比如最常见脚本语言。这就是函数式编程,传递的是一个函数直到,才引用了函数式编程,也就是我们所说的表达式。 导读 Java从jdk1发展到今天,方法的形参类型可以是基本变量,可以是jdk自带的类型,也可以是用户自定义的类型,但是,方法能不能作为形参来传递?我们希望java能够像其他...

    justCoding 评论0 收藏0
  • 企业级lambda表达,让你对lambda有更好的理解

    摘要:但,如果加入了函数式编程,也就是将方法作为形参传递,这必然让开发者为难。但是,其他语言早就使用了函数式编程,比如最常见脚本语言。这就是函数式编程,传递的是一个函数直到,才引用了函数式编程,也就是我们所说的表达式。 导读 Java从jdk1发展到今天,方法的形参类型可以是基本变量,可以是jdk自带的类型,也可以是用户自定义的类型,但是,方法能不能作为形参来传递?我们希望java能够像其他...

    SegmentFault 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<