java8 系列:十分钟入门Lambda表达式
hello world
先来个没有使用lambda表达式的例子
1.
Runnable old = new Runnable() {
public void run() {
System.out.println("old hello world!!!");
}
};
new Thread(old).start();
使用第一个lambda表达式来实现
2.
Runnable helloWorld = () -> System.out.println("hello world 1");
new Thread(helloWorld).start();
是不是精简了许多,当然你可能会说直接匿名类的方式也很精简
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("old hello world!!!");
}
}).start();
然而,看看下面的lambda表达式后,你会不会改观呢?嘿嘿
new Thread(()->System.out.println("hello world3")).start();
本来为了创建实际只打印hello world的线程,你必须去实例化接口对象或创建匿名类覆盖方法的方式去实现功能,代码要3、4行,而有了lambda表达式后,一行代码就能搞定。
lambda表达式特点
- 匿名——不像普通方法一样非得有个明确的名称
- 函数—— lambda函数不像方法那样属于特定的类。但lambda可以和方法一样有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递性——lambda表达式可以作为参数传递给方法或存储在变量中
- 简洁性——无需像匿名类那样写很多模板代码。
- 参数类型、返回结果可推导
我们来看看如何简洁地定义一个Comparator对象
原先的方式:
Comparator<Apple> sortByWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
用了lambda表达式后
Comparator<Apple> sortByWeight = (a1,a2)->a1.getWeight().compareTo(a2.getWeight());
这个lambda表达式有三个部分
- 参数列表~这里采用了Comparator中compare方法的参数,两个Apple,因为参数类型可推导,可以省略Apple类型。
- 箭头——箭头->把参数列表与Lambda主体分隔开,
- Lambda主体——比较两个苹果重量的逻辑,即代码a1.getWeight().compareTo(a2.getWeight();
lambda表达式例子
(1)
(String a) -> a.length()
表示具有一个String类型的参数并返回一个int.lambda没有return语句,因为已经隐含了return
(2)
(Apple a) -> a.getWeight() > 150
表示具有一个Apple类型的参数并返回一个boolean
(3)
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y);
}
表示具有两个int类型的参数而没有返回值(void返回)。表明Lambda表达式可以包含多行语句,这里是两行。
(4)
() -> 10
表示表达式没有参数,返回一个int
(5)
() -> new Apple(10)
表示返回一个Apple对象
(6)
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
表示具有两个Apple类型的参数,返回一个int:比较两个Apple的重量
小结下:Lambda表达式的基本语法是
(parameters) -> expression
或
(parameters) -> {expression;}
错误的Lambda表达式例子
(1)
(Integer i) -> return "xxx" + i;
return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:
(Integer i) -> {return “xxx” + i;}
(2)
(String s) -> {"xxx";}
“xxx”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号
和分号,如下所示:
(String s) -> “xxx”。
或者可以使用显式返回语句,如下所示:
(String s)->{return “xxx”;}。
哪里可以使用Lambda表达式?
函数式接口
函数式接口就是只定义一个抽象方法的接口。如之前提到的Comparator和Runnable等等。
public interface Runnable {
public abstract void run();
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface ActionListener extends EventListener{
void actionPerformed(ActionEvent e);
}
public interface Callable<V>{
V call();
}
public interface PrivilegedAction<V>{
V run();
}
接口现在还可以拥有默认方法(即在类没有对方法进行实现时,
其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
思考下:用函数式接口可以干什么呢?
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
函数描述符是什么?
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。这种抽象方法称为函数描述符。Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。Runnable接口的签名为 ()->void。
一句话概括:函数描述符就是描述一种简单的行为 ,有什么参数(可能没有参数),以及有没有返回结果,结果是什么。
来些例子,看看哪些可以使用Lambda表达式:
(1)
execute(() -> {});
public void execute(Runnable r){
r.run();
}
(2)
public Callable<String> fetch() {
return () -> "Tricky example ;-)";
}
(3)
Predicate<Apple> p = (Apple a) -> a.getWeight();
答案:只有1和2是有效的。
<br>第一个例子有效,是因为Lambda() -> {}具有签名() -> void,这和Runnable中的抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的!<br/>
第二个例子也是有效的。事实上,fetch方法的返回类型是Callable<String>。Callable<String>基本上就定义了一个方法,签名是()->String,其中T被String代替
了。因为Lambda() -> “Trickyexample;-)”的签名是() -> String,所以在这个上下文
中可以使用Lambda。<br/>
第三个例子无效,因为Lambda表达式(Apple a) -> a.getWeight()的签名是(Apple) ->
Integer,这和Predicate<Apple>:(Apple) -> boolean中定义的test方法的签名不同。
java8中的函数式接口
java8在java.util.function包中新加入了许多通用的函数式接口,这允许我们在大部分场景可以直接使用,而不用自己去定义函数式接口,减少自己开发的工作量。
几个常见的函数式接口
Predicate
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
描述了 接收泛型T对象,并返回一个boolean
使用场景:对传入的对象做判断,判断逻辑可自行传入。示例代码如下:
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
@FunctionalInterface public interface Consumer<T> { /** * Performs this operation on the given argument. * * @param t the input argument */ void accept(T 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),
(Integer i) -> System.out.println(i) );
- Function
@FunctionalInterface public interface Function<T, R>{ R apply(T t); }
接口定义了一个叫作apply的方法,它接受一个
泛型T的对象,并返回一个泛型R的对象。
使用场景:如果你需要定义一个Lambda,将输入对象的信息映射
到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。示例代码:
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> l = map(
Arrays.asList("who","am","i"),
(String s) -> s.length()
);// 将返回 [3,2,1]
注意:这些接口用的都是泛型,接收的都是引用类型,如果要对原始类型进行操作的话,这些接口要用相应的带有原始类型的前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。这样能避免拆装箱操作,减少性能损耗和内存占用
更多函数式接口详见java.util.function包
lambda表达式异常处理
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda
表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
总结:
Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
Lambda表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用Lambda表达式。
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>,