Lambda管中窥豹
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数、函数主体和返回类型,可能还有一个可以抛出的异常列表。
何为更简介,做一个例子,使用匿名类和Lambda定义一个Comparator对象:
package cn.net.bysoft.chapter3;
import java.util.Comparator;
public class Example1 {
public static void main(String[] args) {
// 使用匿名类定义Comparator对象
Comparator<Apple> byWeight = new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWidth().compareTo(a2.getWidth());
}
};
// 使用Lambda定义Comparator对象
// 以 -> 为界限,-> 左侧部分是方法参数, -> 右侧部分是方法主体
Comparator<Apple> byWeight_lambda = (Apple a1, Apple a2) -> a1.getWidth().compareTo(a2.getWidth());
}
}
在进一步,下面给出了Java8中五个有效的Lambda表达式:
// 具有一个String类型的参数并返回一个int,没有return语句,因为已经隐含了return
(String s) -> s.length()
// 具有一个Apple类型的参数并返回一个boolean
(Apple a) -> a.getWeight() > 150
// 具有两个int类型的参数而没有返回值
(int x, int y) -> {
System.out.println(x + y);
}
// 没有参数并返回一个int
() -> 42
// 有两个Apple类型的参数并返回一个int
(Apple a1, Apple a2) -> a1.getWeight().comparaTo(a2.getWeight())
在哪里以及如何使用Lambda
函数式接口就是只定义一个抽象方法的接口,例如Comparator和Runnable。
Lambda表达式允许你以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。用匿名类也可以完成同样的事情,但比较笨拙。
让我们通过一个例子来看看在哪里以及如何使用Lambda表达式。
打开一个资源,做一些处理,关闭资源:
package cn.net.bysoft.chapter3;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Example2 {
public static void main(String[] args) {
try {
String s = processFile();
System.out.println(s);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 打开一个资源,读取一行,关闭资源
// 使用了Java7中的带资源的try语句,不需要显示关闭资源
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("c://data.txt"))) {
return br.readLine();
}
}
}
上面这段代码是有局限性的,只能读文件的第一行。如果想要返回前两行,甚至更多该怎么办?
一般来说,通过4个步骤就可以将行为抽象出来:
- 使用函数式接口;
- 行为参数化;
- 执行一个行为;
- 传递Lambda表达式;
具体来看看这4步,首先,创建一个函数式接口:
package cn.net.bysoft.chapter3;
import java.io.BufferedReader;
import java.io.IOException;
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
接下来,行为参数化:
public static String processFile(BufferedReaderProcessor p) throws IOException {
...
}
在来,执行行为:
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("c://data.txt"))) {
return p.process(br);
}
}
最后,传递lambda:
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
测试:
package cn.net.bysoft.chapter3;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Example2 {
public static void main(String[] args) {
// 1.行为参数化
try {
// 4.传递Lambda
String oneLine = processFile((BufferedReader br) -> br.readLine());
System.out.println(oneLine);
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
System.out.println(twoLine);
/**
* output:
* aaa
* aaabbb
* */
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 2.传递行为
public static String processFile(BufferedReaderProcessor p) throws IOException {
// 3.调用行为
try(BufferedReader br = new BufferedReader(new FileReader("c://data.txt"))) {
return p.process(br);
}
}
}
使用Java8中的函数式接口
Java API中已经有了一些函数式接口,比如Comparable、Runnable和Callable等。
Java8的设计师帮你在java.util.function包中引入了几个新的函数式接口:
函数式接口 | 函数描述符 |
Predicate<T> | T -> boolean |
Consumer<T> | T -> void |
Function<T,R> | T -> R |
Supplier<T> | () -> T |
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 |
写个例子使用几个常用的函数式接口:
package cn.net.bysoft.chapter3;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class Example3 {
public static void main(String[] args) {
// 使用Predicate<T>
Apples apples = new Apples();
List<Apple> green_apples = filter(apples.getApples(), (Apple apple) -> "green".equals(apple.getColor()));
// 使用Consumer<T>
System.out.println("绿苹果有:");
forEach(green_apples, (Apple apple) -> System.out.println(apple.getId()));
// 使用Function<T>
// 将字符串的长度放到一个List<Integer>中
System.out.println("数组中的字符长度:");
List<Integer> list = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
forEach(list, (Integer i) -> System.out.println(i));
}
private static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T t : list)
if (p.test(t))
result.add(t);
return result;
}
private static <T> void forEach(List<T> list, Consumer<T> c) {
for (T t : list)
c.accept(t);
}
private static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
}
Lambda的类型检查、类型推断以及限制
类型检查
Lambda的类型检查是从使用Lambda的类型上下文推断出来的。上下文中需要的类型称为目标类型。例如:
filter(apples.getApples(), (Apple apple) -> "green".equals(apple.getColor()));
- 检查filter的方法声明;
- 发现目标类型是Predicate<Apple>
- 发现Predicate<Apple>是一个函数式接口,定义了一个test()方法;
- test方法描述了一个函数描述符,接收一个Apple,返回一个boolean;
- filter的任何实际参数都必须匹配这个要求;
类型推断
Java编译器会从上下文推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适用Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型,例如:
Comparator<Apple> byWeight_lambda = (a1, a2) -> a1.getWidth().compareTo(a2.getWidth());
有时候显示写出类型更容易读,有时候去掉他们更容易读。
使用局部变量
Lambda允许使用自由变量,它们被称作捕获Lambda,例如:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
r.run();
Lambda可以没有限制地捕获实例变量和敬爱变量。但局部变量必须显示声明为final,或事实上是final。例如,下面这段代码就无法编译:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber); // error
portNumber = 1327;
r.run();
方法引用
简单描述
方法引用让你可以重复使用现有的方法,并像Lambda一样传递它们,例如:
inventory.sort(comparing(Apple::getWidth));
List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareToIgnoreCase);
for(String s : str) System.out.println(s);
可以把方法引用看作针对仅仅涉及单一方法的语法糖,因为表达同样的事情时要写的代码更少了,例如:
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
方法引用主要有三类:
- 指向静态方法的方法引用,Integer的parseInt写作Integer::parseInt;
- 指向任意类型实力方法的引用,String的length写作String::length;
- 指向现有对象的实例方法的引用,局部变量evn的实例方法getValue写作evn::getValue;
构造函数引用
可以利用类的名称和关键字new来创建一个类,写作ClassName::new。
加入一个构造函数没有参数,它适合Supplier的签名,() -> Apple,可以这样做:
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
如果有带参数的构造函数,则可以使用BiFunction:
BiFunction<Integer, String, Apple> c2 = Apple::new;
Apple a2 = c2.apply(1, "greed");