Lambda 管中窥豹
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda表达式可以让你十分简明地传递代码。
Lambda组成结构
- 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
- 箭头——箭头->把参数列表与Lambda主体分隔开。
- Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。
Lambda的基本语法是
(parameters) -> expression
或(请注意语句的花括号)
(parameters) -> { statements; }
Java8先前:
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
Java8之后(用了Lambda表达式):
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Java 8中有效的Lambda表达式
(String s) -> s.length()
(Apple a) -> a.getWeight() > 150
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y);
}
() -> 42
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
使用案例 | Lambda示例 |
---|---|
布尔表达式 | (List list) -> list.isEmpty() |
创建对象 | () -> new Apple(10) |
消费一个对象 | (Apple a) -> {System.out.println(a.getWeight());} |
从一个对象中选择/抽取 | (String s) -> s.length() |
组合两个值 | (int a, int b) -> a * b |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
在哪里以及如何使用Lambda
你可以在函数式接口上使用Lambda表达式。
在上面的代码中, 你可以把Lambda 表达式作为第二个参数传给filter 方法, 因为它这里需要Predicate<T>,而这是一个函数式接口。
函数式接口
Predicate仅仅定义了一个抽象方法
public interface Predicate<T>{
boolean test (T t);
}
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。
Java API中的一些其他函数式接口,
//java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
//java.lang.Runnable
public interface Runnable{
void run();
}
//java.awt.event.ActionListener
public interface ActionListener extends EventListener{
void actionPerformed(ActionEvent e);
}
//java.util.concurrent.Callable
public interface Callable<V>{
V call();
}
//java.security.PrivilegedAction
public interface PrivilegedAction<V>{
V run();
}
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。
你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是一个只定义了一个抽象方法run的函数式接口
Runnable r1 = () -> System.out.println("Hello World 1");
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
process(r1);
process(r2);
process(() -> System.out.println("Hello World 3"));
函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符
为什么只有在需要函数式接口的时候才可以传递Lambda呢?语言设计者选择了现在这种方式,因为这种方式自然且能避免语言变得更复杂。
@FunctionalInterface 如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成一个函数式接口。
如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。
请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。
把Lambda 付诸实践:环绕执行模式
资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。
这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();//这就是做有用工作的那行代码
}
}
把processFile的行为参数化。需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。
第1步:记得行为参数化
从
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();//这就是做有用工作的那行代码
}
}
转化成
String result = processFile((BufferedReader br) -> br.readLine());
第2步:使用函数式接口来传递行为
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
现在你就可以把这个接口作为新的processFile方法的参数了:
public static String processFile(BufferedReaderProcessor p) throws IOException {
…
}
第3步:执行一个行为
public static String processFile(BufferedReaderProcessor p) throws
IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
第4步:传递Lambda
现在你就可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件了。
处理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
使用函数式接口
函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。
Predicate
java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。
@FunctionalInterface
public interface Consummer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c) {
for(T i : list) c.accept(i);
}
forEach(Arrays.asList(1,2,3,4,5,6,7), (Integer i)->System.out.println(i));
Function
java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public static <T,R> List<R> map(List<T> list, Function<T,R> f){
List<R> result = new ArrayList<>();
for(T s : list) {
result.add(f.apply(s));
}
return result;
}
List<Integer> list2 = map(Arrays.asList("","1234","asd"),(String s)->s.length());
System.out.println(list2);
原始类型特化
泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8 提供特殊的函数式接口,以便在输入和输出都是原始类型避免自动装箱操作
public interface IntPredicate{
boolean test(int t);
}
//无装箱
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
//装箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);
Java 8 中的常用函数式接口
//助记
//喂,消费者,生产者,我们一起玩(fun)吧
Predicate,Consumer,Supplier,Function
//数学 一元符,二元符
UnaryOperator,BinaryOperator
//(喂,消费者,生产者)* 2
BiPredicate,BiConsumer,BiFunction
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate<T> | T->boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer<T> | T->void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T,R> | T->R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T,R> | ()->T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator<T,R> | T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T,R> | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> | (L,R)->boolean | - |
BiConsumer<T,U> | (T,U)->void | ObjIntConsumer<T,R>, ObjLongConsumer<T,R>, ObjDoubleConsumer<T,R> |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
Lambda示例
使用案例 | Lambda示例 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List<String> list) -> list.isEmpty() | Predicate<List<String>> |
创建对象 | () -> new Apple(10) | Supplier<Apple> |
消费一个对象 | (Apple a) -> { System.out.println(a.getWeight()); } | Consumer<Apple> |
从一个对象中选择/抽取 | (String s) -> s.length() | Function<String, Integer>或 ToIntFunction<String> |
组合两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator或 BiFunction<Apple, Apple, Integer> 或 ToIntBiFunction<Apple, Apple> |
异常
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:
- 定义一个自己的函数式接口,并声明受检异常,
- 把Lambda包在一个try/catch块中
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
类型检查、类型推断以及限制
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
- 首先,你要找出filter方法的声明。
- 第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。
- 第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。
- 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
- 最后,filter的任何实际参数都必须匹配这个要求。
同样的Lambda,不同的函数式接口
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
类型检查——为什么下面的代码不能编译呢? 你该如何解决这个问题呢?
Object o = () -> {System.out.println("Tricky example"); };
答案:Lambda表达式的上下文是Object(目标类型)。但Object不是一个函数式接口。 为了解决这个问题,你可以把目标类型改成Runnable,它的函数描述符是() -> void:
Runnable r = () -> {System.out.println("Tricky example"); };
类型推断
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部变量
不鼓励使用外部变量
Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
错误: Lambda表达式引用的局部变量必须是最终的(final) 或事实上最终的
为什么局部变量有这些限制?
-
实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
-
这一限制不鼓励你使用改变外部变量的典型命令式编程模式。
方法引用
方法引用的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
事实上,方法引用就是让你根据已有的方法实现来创建 Lambda表达式。
但是,显式地指明方法的名称,你的代码的可读性会更好。
PS. I don't think so.
它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。
先前:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));
管中窥豹
Lambda及其等效方法引用的例子
Lambda | 等效的方法引用 |
---|---|
(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 |
你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。
如何构建方法引用
方法引用主要有三类。
-
指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)
-
指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。//(String s) -> s.toUppeCase()
-
指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)//()->expensiveTransaction.getValue()
例子
第二种
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);
第二种
Function<String, Integer> stringToInteger =
(String s) -> Integer.parseInt(s);
Function<String, Integer> stringToInteger = Integer::parseInt;
第二种
BiPredicate<List<String>, String> contains =
(list, element) -> list.contains(element);
BiPredicate<List<String>, String> contains = List::contains;
构造函数引用
无参构造函数的
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
等同
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
若构造函数是Apple(Integer weight)
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
等同
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
若构造函数是Apple(String color, Integer weight)
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);
等同
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple c3 = c3.apply("green", 110);
运用实例
List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> list,Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e));
}
return result;
}
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
// etc...
}
public static Fruit giveMeFruit(String fruit, Integer weight){
return map.get(fruit.toLowerCase()).apply(weight);
}
构造函数引用
你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如Color(int, int, int),使用构造函数引用呢?
构造函数引用的语法是ClassName::new,那么在这个例子里面就是Color::new。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身并没有提供这样的函数式接口,你可以自己创建一个:
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
现在你可以像下面这样使用构造函数引用了:
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
Lambda和方法引用实战
以排序为例
inventory.sort(comparing(Apple::getWeight));
第1步:传递代码
void sort(Comparator<? super E> c)
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
第2步:使用匿名类
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
第3步:使用Lambda表达式
inventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight()));
//进一步简化
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
//现在你可以把代码再改得紧凑一点了:
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
第4步:使用方法引用
inventory.sort(comparing(Apple::getWeight));
复合Lambda表达式的有用的方法
比较器复合
逆序
inventory.sort(comparing(Apple::getWeight).reversed());
比较器链
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
//两个苹果一样重时,进一步按国家排序
谓词复合
谓词接口包括三个方法: negate、 and和or,让你可以重用已有的Predicate来创建更复杂的谓词。
//产生现有Predicate对象redApple的非
Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getColor()));
函数复合
最后,你还可以把Function接口所代表的Lambda表达式复合起来。 Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);//x=1,结果是4
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);//x=1,结果是3
做文本转换 为例
public class Letter{
public static String addHeader(String text){
return "From Raoul, Mario and Alan: " + text;
}
public static String addFooter(String text){
return text + " Kind regards";
}
public static String checkSpelling(String text){
return text.replaceAll("labda", "lambda");
}
}
函数复合
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling).andThen(Letter::addFooter);
数学中类似的思想
积分
在这个例子里,函数f是一条直线,因此你很容易通过梯形方法(画几个三角形)来算出面积:
1/2 × ((3 + 10) + (7 + 10)) × (7 – 3) = 60
运用Lambda表达式
integrate((double x) -> x + 10, 3, 7)
public double integrate(DoubleFunction<Double> f, double a, double b) {
return (f.apply(a) + f.apply(b)) * (b-a) / 2.0;
}
小结
- Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用Lambda表达式。
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>,
- 为了避免装箱操作,对Predicate<T>和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
- 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所需要代表的类型称为目标类型。
- 方法引用让你重复使用现有的方法实现并直接传递它们。
- Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
来源:oschina
链接:https://my.oschina.net/jallenkwong/blog/4492913