0%

Java 函数式编程(一):Lambda 表达式

1. 怎样理解 Lambda 表达式

  • Lambda 这个名字来源于学术界的演算
  • 不同于接口和匿名内部类的传递代码的方式,Lambda 表达式是一种紧凑的传递代码的方式。利用 Lambda 表达式可以实现简洁灵活的函数式编程。
  • 基于 Lambda 表达式,针对常见的集合数据处理,Java 8 引入了一套新的类库,位于包 java.util.stream 下,称为 Stream API。不同于容器类 API,Stream API 是函数式的,非常简洁、灵活、易读。
  • Stream API 是对容器类的增强,它可以将对集合数据的多个操作以流水线的方式组合在一起。
  • 利用 Lambda 表达式,Java 8 还增强了日期和时间 API。

2. Lambda 表达式的语法

  • Lambda 表达式由 -> 分隔为两部分。
  • 前半部分是方法的参数,后半部分 {} 内是方法的代码

3. 使用 Lambda 表达式重写下面代码

1
2
3
4
5
6
7
8
9
10
11
//列出当前目录下的所有扩展名为 .txt 的文件
File f = new File(".");
File[] files = f.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if(name.endsWith(".txt")) {
return true;
}
return false;
}
});
  • 标准写法:

    1
    2
    3
    4
    5
    6
    7
    8
    File f = new File(".");
    //不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码
    File[] files = f.listFiles((File dir, String name) -> {
    if(name.endsWith(".txt")) {
    return true;
    }
    return false;
    });
  • 简化版本一:

    1
    2
    3
    4
    File f = new File(".");
    File[] files = f.listFiles((File dir, String name) -> {
    return name.endsWith(".txt"); //对 if 判断语句的传统优化
    });
  • 简化版本二:

    1
    2
    3
    File f = new File(".");
    //当主体代码只有一条语句的时候,大括号 {} 和 return 关键字也可以省略
    File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
  • 简化版本三:

    1
    2
    3
    4
    5
    6
    File f = new File(".");
    //方法的参数类型声明也可以省略
    File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

    //之所以可以省略方法的参数类型,是因为 Java 可以自动推断出来,它知道 listFiles() 方法接受的参数类型是 FilenameFilter。
    //这个接口只有一个方法 accept(),这个方法的两个参数类型分别是 File 和 String
  • 简化版本四:

    1
    2
    3
    4
    5
    6
    //当参数只有一个的时候,参数部分的括号可以省略
    //比如,File 还有如下方法:public File[] listFiles(FileFilter filter)
    //FileFilter 的定义为:public interface FileFilter { boolean accept(File path); }
    //使用 FileFilter 重写上面的列举文件的例子
    File f = new File(".");
    File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

4. 使用 Lambda 表达式重写下面代码

1
2
3
4
5
6
7
8
9
10
File f = new File(".");
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

//将 files 按照文件名排序
Arrays.sort(files, new Comparator<File> () {
@Override
public int compare(File f1, File f2) {
return f1.getName().compareTo(f2.getName());
}
});
1
2
3
4
File f = new File(".");
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

5. 使用 Lambda 表达式重写下面代码

1
2
3
4
5
6
7
8
//提交一个最简单的任务
ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
1
2
ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(() -> System.out.println("hello world")); //参数部分为空,写为 ()

6. Lambda 表达式和匿名内部类的区别是

  • 与匿名内部类类似,Lambda 表达式也可以访问定义在主体代码外部的变量,但对于局部变量,它也只能访问 final 类型的变量

  • 与匿名内部类的区别是,它不要求变量声明为 final,但变量事实上不能被重新赋值

  • 这个原因与匿名内部类是一样的:

    • Java 会将 msg 的值作为参数传递给 Lambda 表达式,为 Lambda 表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的 msg 变量。
    • 如果允许 msg 被修改,则程序员可能会误以为 Lambda 表达式读到修改后的值,引起更多的混淆
  • 为什么非要建立副本,直接访问外部的 msg 变量不行吗?

    • 不行,因为 msg 定义在栈中,当 Lambda 表达式被执行的时候,msg 可能早已被释放了。
    • 如果希望能够修改值,可以将变量定义为实例变量,或者将变量定义为数组。
  • Lambda 表达式与匿名内部类很像,主要就是简化了语法,但 Lambda 表达式的内部实现并不是匿名内部类

    • Java 会为每个匿名内部类生成一个类,但 Lambda 表达式不会。
    • Lambda 表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会受到影响。
  • 内部实现上,Java 利用了 Java 7 引入的为支持动态类型语言引入的 invokedynamic 指令、方法句柄 method handle 等,具体实现比较复杂,实现细节可参见 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html。需要知道的是,Lambda 表达式的 Java 实现是非常高效的,不用担心生成太多类影响性能的问题。

  • Lambda 表达式的类型是函数式接口

7. 什么是函数式接口

  • Java 8引入了函数式接口的概念。
  • 函数式接口也是接口,但只能有一个抽象方法
  • 之所以强调是抽象方法,是因为 Java 8 中还允许定义静态方法和默认方法
  • Lambda 表达式可以赋值给函数式接口

8. 怎样理解 @FunctionalInterface 注解

  • Java 8 中函数式接口都有一个注解 @FunctionalInterface。比如:

    1
    2
    3
    4
    @FunctionalInterface
    public interface Runnable {
    public abstract void run();
    }
  • 注解 @FunctionalInterface用于清晰地告知使用者这是一个函数式接口。不过,这个注解不是必需的

  • 不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java 编译器会报错,类似于 @Override 注解。

9. Java 8 中预定义的函数式接口有哪些

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

  • 主要的预定义函数式接口

    函数接口 方法定义 说明
    Predicate boolean test(T t) 谓词,测试输入是否满足条件
    Function R apply(T t) 函数转换,输入类型 T,输出类型 R
    Consumer void accept(T t) 消费者,输入类型 T
    Supplier T get() 工厂方法
    UnaryOperator T apply(T t) 函数转换的特例,输入和输出类型一样
    BiFunction R apply(T t, U u) 函数转换,接受两个参数,输出 R
    BinaryOperator T apply(T t, T u) BiFunction 的特例,输入和输出类型一样
    BiConsumer void accept(T t, U u) 消费者,接受两个参数
    BiPredicate boolean test(T t, U u) 谓词,接受两个参数
  • int 类型的函数式接口:除 int 之外,对于基本类型 booleanlongdouble,为避免装箱/拆箱,Java 8 也提供了一些专门的函数:

    函数接口 方法定义 说明
    IntPredicate boolean test(int value) 谓词,测试输入是否满足条件
    IntFunction R apply(int value) 函数转换,输入类型 int,输出类型 R
    IntConsumer void accept(int value) 消费者,输入类型 int
    IntSupplier int getAsInt() 工厂方法
  • 这些函数的作用是:它们被大量用于 Java 8 的函数式数据处理 Stream 相关的类中,即使不使用 Stream,也可以在自己的代码中直接使用这些预定义的函数。

10. 使用预定义的 Predicate 过滤学生列表,筛选 90 分以上的学生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//学生类
static class Student {
String name;
double score;
//省略了构造方法和 getter/setter 方法
}

//学生列表
List<Student> students = Arrays.asList(new Student[] {
new Student("zhangsan", 89d), new Student("lisi", 89d), new Student("wangwu", 98d)
});

//在日常开发中,列表处理的一个常见需求是过滤,列表的类型经常不一样,过滤的条件也经常变化,但主体逻辑都是类似的,可以借助 Predicate 写一个通用的方法
public static <E> List<E> filter(List<E> list, Predicate<E> pred) {
List<E> retList = new ArrayList<> ();
for(E e : list) {
if(pred.test(e)) {
retList.add(e);
}
}
return retList;
}

//过滤 90 分以上的学生
students = filter(students, t -> t.getScore() > 90);

11. 使用预定义的 Function 转换学生列表,将学生名称转换为大写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//借助 Function 写一个通用的方法
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
List<R> retList = new ArrayList<> (list.size());
for(T e : list) {
retList.add(mapper.apply(e));
}
return retList;
}

//根据学生列表返回名称列表
List<String> names = map(students, t -> t.getName());

//将学生名称转换为大写
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));

12. 使用预定义的 Consumer 转换学生列表,将学生名称转换为大写(直接修改原对象,而不是为每个学生创建新对象)

1
2
3
4
5
6
7
8
9
//用 Consumer 写一个通用的方法
public static <E> void foreach(List<E> list, Consumer<E> consumer) {
for(E e : list) {
consumer.accept(e);
}
}

//将学生名称转换为大写
foreach(students, t -> t.setName(t.getName().toUpperCase()));

13. 怎样理解 Java 8 的方法引用语法

  • 下面两种写法是等价的:

    • List<String> names = map(students, t -> t.getName());
    • List<String> names = map(students, Studetn::getName);
  • Student::getName 这种写法是 Java 8 引入的一种新语法,称为:方法引用

  • 方法引用是 Lambda 表达式的一种简写方法,由 :: 分隔为两部分,前面是类名或变量名,后面是方法名(不带小括号 ())

  • 方法可以是实例方法,也可以是静态方法,但含义不同:举例,以 Student 为例,先增加一个静态方法:public static String getCollegeName() {return "Laoma School"}

    • 对于静态方法,下面两条语句是等价的:(参数都是空,返回类型为 String

      • Supplier<String> s = Student::getCollegeName;
      • Supplier<String> s = () -> Student.getCollegeName();
    • 对于实例方法,下面两条语句是等价的:(它的第一个参数就是该类型的实例)

      • Function<Student, String> f = Student::getName;
      • Function<Student, String> f = (Student t) -> t.getName();
    • 对于 Student::setName,它是一个 BiConsumer,即下面两条语句是等价的:

      • BiConsumer<Student, String> c = Student::seName;
      • BiConsumer<Student, String> c = (t, name) -> t.setName(name);
    • 如果方法引用的第一部分是变量名,则相当于调用那个对象的方法。比如,假定 t 是一个 Student 类型的变量,则下面两条语句是等价的:

      • Supplier<String> s = t::getName;
      • Supplier<String> s = () -> t.getName();
    • 下面两条语句也是等价的:

      • Consumer<String> consumer = t::setName;
      • Consumer<String> consumer = (name) -> t.setName(name);
    • 对于构造方法,方法引用的语法是 类名::new,如 Student::new,即下面两条语句是等价的:

      • BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
      • BiFunction<String, Double, Student> s = Student::new;

14. Java 8 函数式编程中函数的复合是什么意思

  • 函数式接口和 Lambda 表达式除了用作方法的参数,还可以用作方法的返回值,传递代码回调用者。将这两种用法结合起来,可以构造复合的函数,使程序简洁易读。
  • 复合函数经常会用到 Java 8 对接口的增强,即静态方法和默认方法。
  • ComparatorConsumerPredicate 等都有一些复合方法,被大量用于函数式数据处理 API 中。
-------------------- 本文结束感谢您的阅读 --------------------