java学习笔记(四)
简单地讲讲Lambda表达式
Lambda管中窥豹
在前一篇文章中,已经看见了Lambda表达式的效果。那什么是Lambda表达式呢?Lambda基于数学中的λ演算得名,在 Java 中你可以把Lambda表达式理解为一种简洁地表示可传递的匿名函数的方式。它没有名称,但它有参数列表,有函数主体,又返回值,可能还可以抛出一个异常列表。
在Java8 之前的Java 传递代码十分繁琐和冗长,Java8 之后可以使用Lambda 就可以解决这个问题,不必要在为匿名类写一堆笨重的代码。使用Lambda 的结果是你的代码变得清晰、灵活。我们可以看看两个Apple比较重量的例子:
Java8 之前
Conparator byWeith = new Comparator<>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeith().compareTo(a2.getWeith());
}
}
使用Lambda之后
Conparator byWeith = (Apple a1, Apple a2) -> a1.getWeith().compareTo(a2.getWeith());
不得不承认,代码看起来更清晰了。要是你觉得Lambda表达式看起来似懂非懂也没关系,我们慢慢的来了解它。
现在,我们来看看几个Java8中有效的Lambda表达式加深对Lambda表达式的理解:
// 这个表达式具有一个String类型的参数并返回一个int,Lambda并没有return语句,因为已经隐含了return。
(String s) -> s.length()
// 这个表达式有一个Apple类型的参数并返回一个boolean(苹果重来是否大于150克)
(Apple a) -> a.getWeight() > 150
// 这个表达式具有两个int类型二的参数并且没有返回值。注意Lambda表达式可以包含多行代码。
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
}
// 这个表达式没有参数类型,返回一个int。
() -> 88
// 显式的指定为Apple类型,并对重量进行比较返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())
Java语言设计者选选择了这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。Lambda的基本语法是:
(parameters) -> expression
或者(请注意语句花括号):
(parameters) -> {statements;}
是的,Lambda表达式的语法看起来就是那么简单。
在哪里以及如何使用Lambda
现在你可能在想哪里适合使用 Lambda ,以前是在 filter 方法中使用 Lambda :
List greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));
在 filter 方法中有一个 Predicate
函数式接口
参数化的 filter 方法需要一个函数接口,而 Predicate
public interface Comparator {
int compare(T o1, T o2);
}
public interface ActionListener extends EventListener {
void actionPerformed(ActionEvent e);
}
public interface Collable {
V call();
}
public interface PrivilegedAction {
V run();
}
这只是一小部分。Lambda 表达式允许直接以内敛的形式为函数是接口的抽象方法提供实现,并把整个表达式作为函数是接口的实例。这就是 Lambda 表达式与函数式接口的关系。
函数描述符
函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。这种抽象方法叫做函数描述符。例如:Comparator 接口可以看作一个接受两个参数返回一个整数的函数签名,因为它有一个 compare 方法,这个方法接受两个参数返回一个整数。为了简单会使用形如 () -> void 的表示接受为空,返回为空的 Lambda 和函数式接口的签名。
把 Lambda 付诸实践:环绕执行
看一个简单的例子,在资源处理时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。设置和清理阶段总是类似的,并且会围绕执行处理的那些重要代码。这就是环绕执行模式,在下面的 Java7 代码中其实已经有了很大简化。
public String processFile() throws IOExceptoin {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
现在代码还是有局限的,只能读取一行。如果改变需求,如返回两行,或返回频繁词汇等等。就需要改变行为了。是不是很熟悉,没错就是行为参数化。
第一步:记得行为参数化
传递行为是 Lambda 的拿手好戏。你需要把 processFile 的行为参数化,然后需要一种方法把行为传递给 processFile 。下面是新的 processFile 方法:
String result = processFile((BufferedReader br) -> br.readLine();)
第二步:使用函数式接口传递行为
Lambda 仅可用于上下文是函数式接口的情况,你需要一个能匹配 BufferedReader -> String ,还能抛出 IOException 异常的接口。
@FunctionalInterface
public interface BufferedReaderProcess {
String process(BufferedReader br) throws IOException;
}
这样你就可以把这个接口作为 processFile 方法的参数了:
public String processFile(BufferedReaderProcess p) throws IOException{
...
}
第三步:执行一个行为
现在我们需要在 processFile 方法里执行 Lambda 所代表的代码。Lambda 表达式允许直接内联,为函数是接口的抽象方法提供实现,并将整个表达式作为一个接口的实例。因此,我们能在 processFile 方法里直接调用 process 方法执行处理:
public String processFile(BufferedReaderProcess p) throws IOException{
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.process(br);
}
}
第四步:传递 Lambda
现在我们可以通过传递 Lambda 重用 processFile 方法,并以不同的方式处理文件。
处理一行:
String oneLine = processFile((BifferedReader br) -> br.readLine());
处理两行:
String oneLine = processFile((BifferedReader br) -> br.readLine() + br.readLine());
简单的展示了如何利用函数式接口来传递 Lambda ,但是我们还需要自己定义接口。下面我们来了解一下 Java8 新加入的接口。
使用函数式接口
函数是接口定义却只定义了一个抽象方法。抽象方法的签名可以描述 Lambda 表达式的签名,也称为函数描述符。为了应用不同的 Lambda 表达式,我们需要一套能够描述常见函数描述符的函数式接口。Java API 已经有了几个函数式接口,他们在 java.util.function 包中,下面简单介绍几个常见的接口。
Predicate 接口
@FunctionInterface
public interface Predicate {
boolean test(T t);
}
这个接口有一个 test 的抽象方法,它接受一个泛型 T 对象,返回一个 boolean 。其实这个和前面创建的是一样的,现在可以直接使用这个接口。
使用 Predicate
public List filter(List list, Predicate p) {
List result = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
result.add(t);
}
}
return result;
}
List nonEmpty = filter(listOfString, (String s) -> !s.isEmpty())
Consumer 接口
@FunctionInterface
public interface Consumer {
void accept(T t);
}
这个接口有一个 accept 的抽象方法,它接受一个泛型 T 对象,没有返回(void)。如果你需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。比如,在 forEach 方法中使用它执行某些操作:
public void forEach(List list, Consumer c) {
for (T t : list) {
c.accept(t);
}
}
forEach(Array.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));
Function 接口
@FunctionInterface
public interface Function {
R apply(T t);
}
这个接口有一个 apply 的抽象方法,它接受一个泛型 T 对象,返回一个泛型 R 对象。如果你想定义一个 Lambda ,将输入对象的信息映射到输出,就可以使用这个接口。例如下面这个例子:
public List map(List list, Function f) {
List result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
List list = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
就简单介绍这三个接口,其他接口你可以去查阅 Java API 的相关说明。
Java 中有两种类型,一个是原始类型,一个是引用类型。前面三个接口都是为引用类型而设计的。原始类型可以通过自动拆装箱和引用类型转换,但是性能方面需付出代价。Java8 为此提供了原始类型的特化接口。就是在函数式接口名称上加上对应的原始类型前缀,比如 DoublePredicate、IntConsumer 等等。
类型检查、类型推断以及限制
第一次提到 Lambda 表达式时,说她可以为函数式接口生成一个实例。然而,Lambda 表达式本身并不包含它在实现哪个函数式接口的信息。为了了解 Lambda 表达式,你应该知道 Lambda 的实际类型。
类型检查
Lambda 的类型是从使用 Lambda 的上下文推断出来的,上下文中的 Lambda 表达式需要的类型指的是目标类型。通过下例代码简单的介绍一下类型检查的过程。
List heavierThan150g = filter(inventory, (Apple a) -> getWeight() > 150)
第一:找出 filter 方法的声明
filter(List inventory, Predicate p)
第二:找出要求的目标类型
Predicate
第三:确定 Predicate
boolean test(Apple a)
第四:确定 test 方法的函数描述符
Apple -> boolean
最后:filter 方法的任何实际类型参数都需要匹配,如果有异常抛出也必须匹配。
同样的 Lambda ,不同的接口
有了目标类型的概念,同一个 Lambda 就可以和不同的函数是接口联系起来了,只要抽象方法能兼容。例如:
Callable c = () -> 75;
PrivilegeAction p = () -> 75;
应为他们都是什么都不接受,返回一个泛型 T 。
菱形运算符
熟悉 Java 演变的人会记得,Java7 中引入菱形运算符(< >),利用泛型推断从上下文推断类型的思想。一个实例表达式可以出现在多个不同的上下文中,并会像下面这样推断出合适的类型。
List
ListOfString = new ArrayList<>(); List ListOfIntegers = new ArrayList<>();
现在你应该能很好理解什么时候使用 Lambda 表达式了。使用类型检查可以知道 Lambda 是否合适某个特定的上下文。其实,我们可以推断 Lambda 的参数类型。
类型推断
还可以进一步简化所写代码。Java 编译器会从上下文中推断函数的目标类型,这表明可以推断出适合的 Lambda 签名,应为函数描述符可以通过目标类型得到。这样我们可以在 Lambda 语法中省去参数注明。例如:
//参数 a 没有类型
List greenApples = filter(inventory, a -> "green".equals(a.getColor()));
Lambda 表达是由多个参数,代码可读性的好处就更为明显。例如:你可以这样创建一个 Comparator 对象:
//没有类型推断
Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
有时候显示写出类型更易读,有时候去掉更易读。没有哪种说法更好,由程序员喜好决定。
使用局部变量
Lambda 是允许使用自由变量(不是参数,是外层作用域定义的变量)的,就像匿名类一样。它们被称作捕获 Lambda 。例如:
int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);
但有时候有一点小麻烦,局部变量必须是显示声明为 final 或者事实上是 final 。如果是可变的,就不能通过编译。例如:
int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);
int portNumber = 8888;
//这是不能通过编译的
你可能会想,为什么要有这种限制呢?第一:实例变量和局部变量有一个关键不同,实例变量在堆中储存,局部变量在栈中储存。如果 Lambda 直接使用局部变量,而且是在一个线程中使用的,则使用 Lambda 的线程,可能会在分配那个变量的线程回收该变量后使用。因此,Java 访问局部变量时,实际是访问它的副本。如果局部变量只赋值一次就能没什么区别了。第二:这一限制不鼓励你使用改变外部变量的典型命令式编程模式。
方法引用
方法引用让你可以重复使用现有方法定义,并像 Lambda 一样传递它们。下面我们借助更新的 Java8 API ,用方法引用写一个排序的例子:
先前:
inventory.sort((Apple a1, Apple a2) -> a1.compareTo(a2.getWeigth()));
以后
//第一个方法引用实例
inventory.sort(comparing(Apple::getWeigth()));
管中窥豹
方法引用可以看作调用特定方法的 Lambda 的一种快捷写法。基本思想是,调用一个方法最好是直接用名称调用它,而不是去描述如何调用它。当时需要使用方法引用时,目标引用放在分隔符 :: 前,方法名称放在后面。例如:Apple::getWeigth 就是引用了 Apple 类中定义的方法 getWeigth。
方法引用主要有三类:
指向静态的方法引用(例如 Integer::parseInt)
指向任意类型实例方法的方法引用(例如 String::length )
-
指向现有对象的实例方法的方法引用
下面是上述方法引用和 Lambda 表达式之间的等价:
// Lambda (args) -> ClassName.staticMethod(args) //方法引用 ClassName::staticMethod
// Lambda (args, rest) -> args.instance(args) //方法引用 ClassName::instanceMethod
// Lambda (args) -> expr.instanceMethod(args) //方法引用 expr::instanceMethod
还有针对构造函数、数组构造函数和父类调用等一些特殊形式的方法引用。如果你感兴趣,可以查阅相关文档。
构造函数引用
对于现有构造函数,你可以利用它的名称和关键字 new 来创建一个引用:ClassName::new 。它的功能和指向静态方法的引用类似。
Supplier
c = Apple::new; Apple a = c.get(); 等价于
Supplier
c = () -> new Apple(); Apple a = c.get(); 这里就不一一举例了。
总结
- Lambda 可以理解为一种匿名函数:它没有名称,但是有参数列表、函数主体、返回类型,可能还会抛出一个异常。
- Lambda 表达式让你可以简洁的传递代码。
- 函数是接口就是仅仅声明了一个抽象方法的接口。
- 只有在接收函数式接口的地方才可以使用 Lambda 表达式。
- Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并将整个表达式作为函数式接口的一个实例。
- Java8 自带了一些常用的函数式接口,放在 java.util.function 包中,包括 Predicate
、Function 、Supplier 、Consumer 等等。 - 为了避免装箱操作,对Predicate
和 Function 等常用接口的原始类型特化:IntPredicate 、IntToLongFunction 等。 - 环绕执行模式可以配合 Lambda 提高灵活性和可用性。
- Lambda 表达式所需的代表类型成为目标类型
- 方法引用让你重复使用现有的方法实现并之间传递他们。