aliases: [JAVA Lambda]
tags   : " #Java "
summary: [如何使用函数式编程写出优雅高效的JAVA代码]
author : [yaenli]
date   : [2022-11-10]

1 简介

简洁的代码就能处理大型数据集合,让复杂的集合处理算法高效的运行在多核CPU上。

面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象,能编写出更易读的代码——这种代码更多地表达了业务逻辑的意图,而不是它的实现机制。

写回调函数和事件处理程序时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,惰性代码在真正需要时才初始化变量的值。

函数式编程核心思想:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

2 Lambda 表达式

2.1 Lambda 表达式的基本形式

例2-1:使用匿名内部类将行为和按钮单击进行关联

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

这是一个代码即数据的例子,给按钮传递了代表某种行为的对象。设计匿名内部类的目的,就是为了方便 Java 程序员将代码作为数据传递。

例2-2:使用 Lambda 表达式将行为和按钮单击进行关联

button.addActionListener(event -> System.out.println("button clicked"));

Lambda 特点

  • 传入了一段代码块(无名函数),event是参数名,->将参数和主体分开。
  • 声明 event 参数的方式,匿名内部类需要显示声明参数类型,而 Lambda 表达式中无需指定类型,程序依然可以编译。这是因为 javac 根据程序的上下文( addActionListener 方法的签名)在后台推断出了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。

Java 仍然是一种静态类型语言。声明参数时也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的类型!

2.2 Lambda 表达式的变种

例2-3:Lambda 表达式的不同形式

// 1 不含参数,且返回类型为void
Runnable noArguments = () -> System.out.println("Hello World"); 
// 2 一个参数,可省略括号
ActionListener oneArgument = event -> System.out.println("button clicked"); 
// 3 主体不仅可以是表达式,也可以是代码块{},块中可以返回或抛异常
Runnable multiStatement = () -> { 
	System.out.print("Hello");
	System.out.println(" World");
};
// 4 创建了一个函数,用来计算两个数字相加的结果。变量 add 的类型是 BinaryOperator<Long> ,它不是两个数字的和,而是将两个数字相加的那行代码。
BinaryOperator<Long> add = (x, y) -> x + y; 
// 5 需要显示声明参数类型时,用()
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; 

目标类型是指 Lambda 表达式的类型,依赖于上下文环境,由编译器推断而来。比如,将 Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型。

Java 中初始化数组时,数组的类型就是根据上下文推断出来的。另一个常见的例子是 null ,只有将 null 赋值给一个变量,才能知道它的类型。

2.3 Lambda 的引用类型

Lambda 表达式引用的是值,而不是变量。

Lambda 和匿名内部类一样,在引用外部变量时,该变量必须是终态变量。如下是正确和错误示例:

例2-4:Lambda 表达式中引用既成事实上的 final 变量

// 编译通过
String name = getUserName(); // final变量
button.addActionListener(event -> System.out.println("hi " + name));
// 编译错误
String name = getUserName();
name = formatUserName(name); // 非终态变量
button.addActionListener(event -> System.out.println("hi " + name));

Java 8 无需显示声明final,但是该变量必须是既成事实上的终态变量(final),否则编译器会报错。是否显示的使用final,取决于个人喜好。

2.4 Lambda 表达式的类型

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

函数接口是只有一个抽象方法的接口.

例2-5: ActionListener 接口

public interface ActionListener extends EventListener {
	public void actionPerformed(ActionEvent event);
}

ActionListener 就是一个函数接口,actionPerformed 定义在一个接口里,因此 abstract 关键字不是必需
的。该接口也继承自一个不具有任何方法的父接口: EventListener 。

接口中单一方法的命名并不重要,只要方法签名和 Lambda 表达式的类型匹配即可。

表2-1:JDK8 中重要的函数接口

接口 参数 返回类型
Predicate<T> T boolean
Consumer<T> T void
Function<T,R> T R
Supplier<T> None T
UnaryOperator<T> T T
BinaryOperator<T> (T, T) T

2.5 Lambda 表达式的类型推断

Lambda 表达式中的类型推断,实际上是 Java 7 就引入的目标类型推断的扩展。

例2-6:使用菱形操作符,根据变量类型做推断

Map<String, Integer> diamondWordCounts = new HashMap<>();

如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。

例2-7:使用菱形操作符,根据方法签名做推断

useHashmap(new HashMap<>());
...
private void useHashmap(Map<String, String> values);

Java 8 中对类型推断系统做了提升。上面的例子将 new HashMap<>()传给 useHashmap 方法,即使编译器拥有足够的信息,也无法在 Java 7 中通过编译。

类型推断:JAVA8中,程序员可省略 Lambda 表达式中的所有参数类型。javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型。

例2-8:Predicate类型推断

// Predicate 接口的源码,接受一个对象,返回一个布尔值
public interface Predicate<T> {
	boolean test(T t);
}
// x被推断为Integer, javac 还检查Lambda 表达式的返回值是不是 boolean
Predicate<Integer> atLeast5 = x -> x > 5; 

例2-9:BinaryOperator类型推断

// BinaryOperator 接口源码,接受两个对象,返回一个对象,泛型参数既是入参类型,也是返回类型
public interface BinaryOperator<T> extends BiFunction<T,T,T> {}
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
// x,y推断为Long,并返回Long
BinaryOperator<Long> addLongs = (x, y) -> x + y;
// 以下代码编译报错:Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object. 没有给出add的泛型信息,编译器认为都是Object。
BinaryOperator add = (x, y) -> x + y; 

2.6 总结

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda 表达式的常见结构:BinaryOperator<Integer> add = (x, y) → x + y
  • Lambda 表达式的类型是函数接口,指仅具有单个抽象方法的接口。

练习:使用 ThreadLocal 创建一个线程安全的 DateFormatter 对象

ThreadLocal 作为容器保存当前线程的局部变量,JAVA8 中新增了工厂方法,可以不用再使用继承;
DateFormatter 非线程安全

public class JavaMainTest {
	private static ThreadLocal<SimpleDateFormat> threadLocal;
	private void init() {
		// jdk7
		threadLocal = new DataFormatThreadLocal() ;
		// jdk8
		threadLocal = ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {
			@Override
			public SimpleDateFormat get() {
				return new SimpleDateFormat("yyyy-MM-dd");
			}
		});
		// jdk8 lambda
		threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
	}
	class DataFormatThreadLocal extends ThreadLocal<SimpleDateFormat> {
		@Override
		protected SimpleDateFormat initialValue() {
			// TODO Auto-generated method stub
			return new SimpleDateFormat("yyyy-MM-dd");
		}
	}
}

3 参考资料

《Java 8函数式编程》- 作者:[英]沃伯顿;译者:王群锋

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。