李文轩 2019-04-21
3.1 Overview
-
Lambda表达式可以简单得理解为简洁地表示可以传递的匿名函数的一种方式:没有名称,但是有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 简洁:代码一般比匿名类少,主要少在模版代码中。
- 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
- 匿名:像匿名类一样没有名称
- 函数:像函数一样有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表;但不像方法那样属于某个特定的类。
-
主要表现形式为:
(parameters) -> expression
或者
(parameters) -> { statement; }
其实和普通的方法一样,有一个参数列表和方法主体;不同点在于 Lambda表达式用箭头(→) 区分这两者。
-
例子:
//参数是一个字符串,返回此字符串的长度 //Lamdba 没有return语句,因为已经隐含了return (String s) -> s.length() //参数是一个Apple的对象,返回一个布尔值代表Apple的重量是否大于150 (Apple apple) -> apple.getWeight() > 150 //参数是两个int类型,而没有返回值(void返回)。 //Lambda是可以包含多行语句 (int x, int y) -> { System.out.println("result: "); System.out.println("x+y); } //没有参数,返回一个int值 () -> 42
-
用Lambda表达式的前后对比
//这里用第二章的Apple类做例子,创建一个Apple类型的Comparator //前 Comparator<Apple> byweight = new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } } //后 Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
3.2 Lambda表达式的具体用法
-
函数式接口
即一个定义且只定义了一个抽象方法的接口
-
一些Java自带的函数式接口
//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 EvenListener{ void actionPerformed(ActionEvent e); } //java.util.concurrent.Callable public interface Callable<V>{ V call(); } //java.security.PrivilegedAction public interface PrivilegedAction<V> { V run(); }
-
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。虽然匿名类也可以达成目的,但是用Lambda表达式比较简洁和更易读。
//用3种方法打印“Hello World” 来对比匿名类和Lambda表达式 //使用Lambda 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(); } public static void main(String[] args){ //打印"Hello world 1" process(r1); //打印"Hello world 2" process(r2); //利用直接传递的Lambda,打印"Hello world 3" process( () -> System.out.println("Hello World 3")); }
-
函数描述符
- 函数式接口的抽象方法的签名就是Lambda表达式的签名,函数描述符是这个签名的抽象。
- 以
Runnable
为例,它的函数描述符为() -> void
,因为它不接受任何参数,也不返回(void) - 上面
Comparator<Apple>
的描述符为(Apple, Apple) -> int
,即接受两个Apple对象,返回一个int值。 -
@FunctionalInterface
,这个标注用于表示该接口会设计成一个函数式接口。
3.3 用Lambda简化环绕执行
环绕执行主要出现在需要某些资源处理的行为中,处理文件或者数据库。
环绕执行模式基本上是打开一个资源,做一些处理,关闭资源。这些开头和结尾关闭的阶段总是很类似,而且是围绕这执行处理的重要代码。
-
简化的目标就是重用这些环绕的代码
//环绕执行的一个例子 //try-with-resource 已经简化了显式地关闭资源的行为 public static String processFile() throws IOException { //用try语句开启资源,并在它结束时隐式地关闭资源 try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))){ //重要的资源处理代码 return br.readline(); } }
-
用Lambda优化环绕执行模式并使其行为参数化
-
行为参数化
在上面的代码中,处理资源的代码有局限性。为了更灵活地设计环绕执行模式,我们可以将处理资源的行为参数化。
-
理想方式是让
processFile
方法接受一个函数,(BufferedReader) -> String
//理想的processFile方法的运用方式 String result = processFile( (BufferedReader br) -> br.readLine());
-
函数式接口
-
因为Lambda只能用在接受函数式接口的方法里,我们需要一个签名是
(BufferedReader) -> String
的函数式接口,并且要抛出IOException
异常@FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader br) throws IOException; }
-
我们也同时需要改变
processFile
的方法参数,以至于它能接受这个接口public static String processFile(BufferedReaderProcessor) throws IOException{ ... }
-
-
执行一个行为
在
processFile
方法里,将重要的处理资源的代表,替换成BufferedReaderProcessor
的process,就可以把处理资源的行为和整个框架分离。-
处理资源的行为将取决于我们传递给
processFile
的BufferedReaderProcessor
的实例public static String processFile(BufferedReaderProcessor) throws IOException{ try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return p.process(br); } }
-
传递Lambda
-
用Lambda表达式定义行为
//从文件里取第一行 String oneLine = processFile((BufferedReader br -> br.readLine()); //从文件里取前两行 String twoLine = processFile((BufferedReader br -> br.readLine() + br.readLine());
-
-
3.4 Java API的常见函数式接口
-
Predicate
java.util.function.Predicate<T>
接口定义了一个test
的抽象方法签名为
(T) -> boolean
-
需要表达一个涉及类型T的布尔表达式的时候可以使用
@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<>(); //遍历list,每个元素都通过p的test方法,每次调用都会获得一个布尔值,它决定会不会将元素放进返回的list里。 for(T s: list){ if(p.test(s)){ result.add(s); } } return results; }
-
使用:
//用lambda表达式赋予函数给一个Predicate的对象 Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); //通过变量传递行为 List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate)
Predicate
有诸如and和or的方法,下一节会谈到
-
Consumer
java.util.function.Consumer<T>
接口定义了一个accept
的抽象方法签名为
(T) -> void
-
需要访问一个类型T的对象,并对其执行操作(不返回)的时候可以使用
@FunctionalInterface public interface Consumer<T>{ void accpet(T t); } public static <T> void forEach(List<T> list, Consumer<T> c){ for(T i: list){ //遍历list,每个元素都通过c的accept方法,accept方法会有运用这些元素的行为 c.accept(i); } }
-
使用:
//遍历list,并打印每个数字 //通过Lambda传递 Consumer的实例(行为) forEach( Array.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
-
Function
java.util.function.Function<T,R>
接口定义了一个apply
的抽象方法签名为
(T) -> R
-
需要将输入对象的信息映射到输出的时候可以使用(比如说,把字符串映射为它的长度)
@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){ //遍历list,每个元素都通过f的apply方法,apply方法会有映射这些元素的行为 result.add(f.apply(s)); } return result; }
-
使用:
//遍历list,并将每个字符串输入到f的apply //这里的apply实例为,输入字符串返回其长度。 List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length() );
原始类型特化
上述的函数式接口都是泛性的;若是要输入原始类型的数据时,我们不能避免得用到Java的自动装箱和自动拆箱的机制。自动将引用类型(Byte、Integer等)换成对应的原始类型(byte、int)为自动拆箱,自动从原始类型到引用类型为自动装箱。
如果要实现函数式接口是没有问题的,但是会有性能上的缺陷。详情请查询相关阅读,这里不展开了。
-
Java 8 带来了一些其函数式接口的专门的版本,以便在输入和输出时,都是原始类型,避免自动装箱和拆箱。
//这里用Predicate的专门应对输入是integer原始类型的一个版本 public interface IntPredicate{ boolean test(int t); } //无装箱 IntPredicate evenNumbers = (int i) -> i % 2 == 0; evenNumbers.test(1000); //返回true //有装箱 Predicate<T> oddNumbers = (Integer i) -> i % 2 == 1; oddNumbers.test(1000); //返回false
这个链接有这些版本的详细的表格
java.util.function (Java Platform SE 8 )
Reference:
[1] Fusco, Mario, and Alan Mycroft. Java 8 in Action: Lambdas, Streams, and Functional-Style Programming. Manning, 2015.