Java Lambda表达式简析

什么是Lambda表达式

Lambda表达式,是一种紧凑的、传递行为的方式.能够使代码产生更多的灵活性.
所以Oracle 在Java8添加了Lambda的支持

函数式的简单使用

我们先来看一个简单的例子
当我们需要创建一个临时的、可被Runner我们可能会这样编写

1
2
3
4
5
6
new Runnable() {
@Override
public void run() {
System.out.println("do some thing");
}
};

但在使用Lambda的情况下,可以这样进行编写
1
Runnable runnable = () -> System.out.println("do some thing");

这是由于Runnable的接口定义中,有且仅有一个抽象方法
1
2
3
4
5
// @FunctionalInterface 仅是标注这个接口将会是一个函数式接口,编译时会进行检查,是否存在多个方法或者其他条件
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

因为当接口中有且仅有一个抽象方法时,我们称其为函数式接口

函数式接口

我们可以在方法签名中使用函数式接口作为参数或者返回类型.
例如我们常常会做这样的事情,任务A和任务B周围都环绕着进行同一段冗余的代码(准备/清理)

假如现在你要读取一个文件中的一行,你可能会这样写

1
2
3
4
5
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {// 真正需要的是这行代码
return br.readLine();
}
}

但现在你的需求变成了,需要读取两行.如果将该方法复制一份,将会导致很多重复的代码
有了Lambda你可以将你想要的操作参数化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String processFile(Function<BufferedReader, String> action) {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {// 真正需要的是这行代码
return action.apply(br);
}
}
// 这样不管是想要读取一行
String oneLine = processFile(br -> {
try {
return br.readLine();
} catch (IOException ignore) {}
return "";
});
// 或者是想要读取两行 都能轻松实现
String twoLine = processFile((BufferedReader br) -> {
try {
return br.readLine() + br.readLine();
} catch (IOException ignore) {}
return "";
}));

自定义函数接口

当然上述的代码看起来有些别扭,这时因为自带的函数式接口的签名中因为没有定义将会抛出错误的类型,而br.readLine()会抛出错误,所以需要进行catch
不过为了美观,我们也能进行自定义函数式接口,大体上像Runnable一样

1
2
3
4
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

然后将方法的函数签名改成
1
2
3
4
5
6
7
8
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
// 这样就可以直接使用一行进行表达了
String oneLine = processFile(br -> br.readLine());
String twoLines = processFile(br -> br.readLine() + br.readLine());

看了上述的示例后是否觉得似曾相识ヾ(•ω•`)o
其实lambda能和匿名内部类相互替换(当然还是要遵守仅有一个抽象方法)
1
2
3
4
5
6
String twoLines = processFile(new BufferedReaderProcessor() {
@Override
public String process(BufferedReader br) throws IOException {
return br.readLine() + br.readLine();
}
});

在Lambda中使用局部变量

如果你曾使用过匿名内部类,也许会遇到这样的情况,需要引用它所在方法里的变量。这时,需要将变量声明为final

1
2
3
4
5
6
final String name = getUserName();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});

在Lambda虽然放松了这一限制,可以不使用final修饰变量,但是该变量实际上不能重复赋值,否则编译器将会报错
1
2
3
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name)); // 此处编译时将会报错

方法引用

当一个方法的签名符合一个使用了函数式接口的参数时,可以使用方法引用进行简写,使调用更加易懂和自然。
例如在上文的例子中

1
2
3
String oneLine = processFile(br -> br.readLine());
// 可以使用以下方式进行表示
String oneLine = processFile(BufferedReader::readLine);

常见使用方法引用的例子

lambda的复合使用

可以将几个Lambda组合一起形成复杂的语句进行使用

比较复合

  • 逆序
    1
    inventory.sort(comparing(Apple::getWeight).reversed());
  • 比较链
    1
    2
    3
    4
    5
    inventory.sort(
    comparing(Apple::getWeight)
    .reversed() // 重量递减排序
    .thenComparing(Apple::getCountry) //当一样重时根据国家排序
    );

谓词复合

  • 1
    Predicate<Apple> notRedApple = redApple.negate();
  • 1
    Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
  • 1
    2
    3
    4
    // 表示 (红苹果 且 重量大于150) 或 绿苹果
    Predicate<Apple> redAndHeavyAppleOrGreen = redApple
    .and(a -> a.getWeight() > 150)
    .or(a -> "green".equals(a.getColor()));

函数复合

1
2
3
4
5
6
Function<Integer, Integer> plus2 = x -> x + 2;
Function<Integer, Integer> multiply2 = x -> x * 2;
// 先执行自己 而后 传递给下一个
plus2.andThen(multiply2).apply(1); // 6
// 先让另一个方法执行 而后 自己再执行
plus2.compose(multiply2).apply(1); // 4

一些技巧

以下是除了上述的能力之外拓展的一些小技巧

缓存

1
2
3
4
5
// 其中的T可以是一些复杂对象
public Predicate<T> distinct() {
Set<String> cache = new HashSet<>();
return t -> cache.add(t.field());
}

延迟求值

1
2
3
4
5
6
7
8
9
10
11
// 假设当前有个方法a比较耗时
public String a() {}

// 这时有个b方法
public void b(String s) {
if (some condition) {
System.out.println(s);
}
}
// b方法仅在特定条件下才执行a方法,而b方法不能直接访问到a
b("some thing" + a());

此时将会导致无论b方法中的条件是否满足,都需要对a进行调用.
此时就能使用lambda的延迟满足进行优化代码

1
2
3
4
5
6
7
8
// 修改b的签名
public void b(Supplier<String> s) {
if (some condition) {
System.out.println(s.get());
}
}
// 这样 仅在b满足条件时,才会回调方法 执行a方法
b(() -> "some thing" + a());

参考书籍(顺便推荐)

  • 《Java8实战》
  • 《Java8函数式编程》
Author: Sean
Link: https://blog.whileaway.io/posts/a00bd50a/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.