文章目录

  • 一、行为参数化
  • 二、Lambda表达式
  • 2.1 Lambda表达式的定义与形式
  • 2.2 基于函数式接口使用 lambda 表达式
  • 2.2.1 自定义函数式接口
  • 2.2.2 jdk 自带的函数式接口
  • 2.2.3 一些需要注意的事情
  • 三、方法引用


一、行为参数化

简单说,就是interface作为参数,并同时实现interface。当interface只有一个抽象方法,我们可以@FunctionalInterface来注释,说明这是一个函数式接口。java8以前,实现interface,我们是使用匿名类实现,比如java.util.Comparator;而java8我们可以使用lambda表达式来简化这个实现。
举个例子

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

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

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

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

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

使用lambda表达式简化

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

二、Lambda表达式

2.1 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>

T -> boolean

IntPredicate, LongPredicate, DoublePredicate

Consumer<T>

T -> void

IntConsumer, LongConsumer, DoubleConsumer

Funcation<T, R>

T -> R

IntFuncation, IntToDoubleFunction, IntToLongFunction, LongFuncation…

Supplier<T>

() -> T

BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

UnaryOperator<T>

T -> T

BinaryOperator<T>

(T, T) -> T

BiPredicate<L, R>

(L, R) -> boolean

BiConsumer<T, U>

(T, U) -> void

BiFunction<T, U, R>

(T, U) -> R

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

  • Predicate<T>
@FunctionalInterface
public interface Predicate<T> {

    /**
     * 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 <T> List<T> filter(List<T> numbers, Predicate<T> predicate) {
    Iterator<T> itr = numbers.iterator();
    while (itr.hasNext()) {
        if (!predicate.test(itr.next())) {
            itr.remove();
        }
        itr.next();
    }
    return numbers;
}

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

PredicateDemo pd = new PredicateDemo();
List<Integer> 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<T>
@FunctionalInterface
public interface Consumer<T> {

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

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

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

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

ConsumerDemo cd = new ConsumerDemo();
List<String> 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<T, R>
@FunctionalInterface
public interface Function<T, R> {

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

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

FunctionDemo fd = new FunctionDemo();
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a", "1", "2", "3", "4", "5", "6"));
List<Integer> result = fd.parse(list, (value) -> Integer.valueOf(value));
System.out.println(result);
// 输出:[1, 2, 3, 4, 5, 6]
2.2.3 一些需要注意的事情
  • 类型推断
    在编码过程中,有时候可能会疑惑我们的调用代码会具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等因素做正确的判定。在具体调用时,一些时候可以省略参数的类型以进一步简化代码:
// 筛选苹果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

// 某些情况下我们甚至可以省略参数类型,编译器会根据上下文正确判断
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
  • 局部变量
    上面所有例子中使用的变量都是 lambda 表达式的主体参数,我们也可以在 lambda 中使用实例变量、静态变量,以及局部变量,如下代码为在 lambda 表达式中使用局部变量:
int weight = 100;
List<Apple> 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 类型即可。

三、方法引用

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

// 采用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)