前言
Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁。当开发者在编写Lambda表达式时,也会随之被编译成一个函数式接口。下面这个例子就是使用Lambda语法来代替匿名的内部类,代码不仅简洁,而且还可读。
不采用Lambda的老方法:
Runnable runnable1=new Runnable(){
@Override
public void run(){
System.out.println("Running without Lambda");
}
};
使用Lambda:
Runnable runnable2=()->System.out.println("Running from Lambda");
正如你所看到的,使用Lambda表达式不仅让代码变的简单、而且可读、最重要的是代码量也随之减少很多。
一、Lambda表达式有哪些语法?
1.1、语法一(无参数,无返回值)
() -> System.out.println("Hello Lambda!");
1.2、语法二(有一个参数,并且无返回值)
// 此处左侧参数若是只有一个可省略小括号 即:x -> System.out.println(x)
(x) -> System.out.println(x)
1.3、语法三(有两个以上的参数,有返回值,并且 Lambda 体中有多条语句
Comparator <Integer> com = (x, y) -> {
// 这里有两条语句,故Lambda体用 {} 括起来
System.out.println("函数式接口");
return Integer.compare(x, y);
};
1.4、语法四(若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写)
Comparator <Integer> com = (x, y) -> Integer.compare(x, y);
1.5、语法五( Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”)
(Integer x, Integer y) -> Integer.compare(x, y);
注 : Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的 “类型推断”;
二、Lambda表达式结构?
Lambda 表达式可以具有零个,一个或多个参数。
可以显式声明参数的类型,也可以由编译器自动从上下文推断参数的类型。例如 (int a) 与刚才相同 (a)。
参数用小括号括起来,用逗号分隔。例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)。
空括号用于表示一组空的参数。例如 () -> 42。
当有且仅有一个参数时,如果不显式指明类型,则不必使用小括号。例如 a -> return a*a。
Lambda 表达式的正文可以包含零条,一条或多条语句。
如果 Lambda 表达式的正文只有一条语句,则大括号可不用写,且表达式的返回值类型要与匿名函数的返回类型相同。
如果 Lambda 表达式的正文有一条以上的语句必须包含在大括号(代码块)中,且表达式的返回值类型要与匿名函数的返回类型相同。
三、函数式接口
3.1、在函数式接口上使用lambda表达式
函数式接口可以被隐式转换为 lambda 表达式。
如下例子:
Thread t = new Thread(() -> System.out.println("Hello world"));
我们可以看看Thread的构造:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
其中入参为Runnable类型的接口,继续查看Runnable接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看出在jdk1.8中,Runnable就是一个函数式接口
Runnable r1 = () -> System.out.println("Hello world")
// 等价于
Runnable r2 = new Runnable() {
public void run(){
System.out.println("Hello world");
}
}
run()方法签名:参数列表为空,返回为void;lambda签名:() -> void 参数列表为空,返回为void可以看出Runnable的run方法签名与lambda的签名匹配,我们将这种对方法抽象描述叫作函数描述符
3.2、在java8中,提供了很多函数式接口,可以用于描述各种Lambda表达式的签名
函数式接口 | 函数描述符 |
Predicate | T->boolean |
Consumer | T->void |
Function<T,R> | T->R |
Supplier | ()->T |
UnaryOperator | T->T |
BiPredicate<L,R> | (L,R)->boolean |
BiConsumer<T,U> | (T,U)->void |
BiFunction<T,U,R> | (T,U)->R |
这些都是较为常用的函数式接口,还有很多都在java.util.function包下,有兴趣可以自行查看。
3.3、隐式返回
如果被放在lambda中的代码是一个java 表达式而不是一个声明,它就会被当作一个返回这个表达式值的方法,因此,下面这两个是等价的:
IntUnaryOperator addOneShort = (x) -> (x + 1);
IntUnaryOperator addOneLong = (x) -> { return (x + 1); };
3.4、访问本地变量(值闭包)
因为lambdas 是匿名内部类的简化写法,它们遵循在一个闭合的域中访问本地变量相同的规则;变量必须被当作final并且在lambda表达式中不能够被修改。
IntUnaryOperator makeAdder(int amount) {
return (x) -> (x + amount); // Legal even though amount will go out of scope
// because amount is not modified
}
IntUnaryOperator makeAccumulator(int value) {
return (x) -> { value += x; return value; }; // Will not compile
}
如果以这种方式包含一个可改变的变量是必要的, 一个包含此变量的拷贝的合法对象应该被使用, Read more in Closures with lambda expressions.
3.5、接收lambdas
因为lambda是一个接口的实现,去使一个方法接收lambda并没有什么特别的要做:任何函数只要是函数式接口都能够接收一个lambda。
public void passMeALambda(Foo1 f) {
f.bar();
}
passMeALambda(() -> System.out.println("Lambda called"));
3.6、使用lambda表达式去排序一个集合
在java 8 之前, 当排序一个集合的时候, 用一个匿名(或着 有名字)类去实现java.util.Comparator接口是必要的:
Java SE 1.2
Collections.sort(
personList,
new Comparator<Person>() {
public int compare(Person p1, Person p2){
return p1.getFirstName().compareTo(p2.getFirstName());
}
}
);
从java 8 开始, 匿名内部类能够被lambda表达式替代, 注意到p1和p2参数能够被忽略, 因为编译器能够自动的推断出它们。
Collections.sort(
personList,
(p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName())
);
这个例子能够被简化通过使用Comparator.comparing
和method references(方法引用), 用::(双冒号)符号来表达:
Collections.sort(
personList,
Comparator.comparing(Person::getFirstName)
);
静态导入允许我们更加简明的去表达它, 但是对于是否能够提高整体可读性是备受争论的。
import static java.util.Collections.sort;
import static java.util.Comparator.comparing;
//...
sort(personList, comparing(Person::getFirstName));
Comparators构建这种方式可以用来链式调用。例如, 通过名字比较之后, 如果有一些人具有相同的名字, 那么thenComparing方法将会根据性别来接着比较
sort(personList, comparing(Person::getFirstName).thenComparing(Person::getLastName));
四、方法引用
方法引用允许提前定义的静态或着实例方法去绑定到一个合适的函数式接口来当作参数传递,而不是用一个匿名的lambda表达式。
引用种类有:
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
假设我们有一个模型:
class Person {
private final String name;
private final String surname;
public Person(String name, String surname){
this.name = name;
this.surname = surname;
}
public String getName(){ return name; }
public String getSurname(){ return surname; }
}
List<Person> people = getSomePeople();
4.1、实例方法引用(对于一个任意的实例)
people.stream().map(Person::getName)
等价的lambda:
people.stream().map(person -> person.getName())
在这个例子中, 对于一个Person类的实例方法getName()的一个方法引用被传递。因为它被当作一个集合类型, 实例上的方法(之后被察觉)将会被调用 。
4.2、实例方法引用(对于一个特定类型)
people.forEach(System.out::println);
因为System.out是一个PrintStream的实例,对这个特定的实例的一个方法引用被当作一个参数传递。等价的lambda表达式:
people.forEach(person -> System.out.println(person));
4.3、静态的方法引用
对于转换流,我们能够使用静态方法引用
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().map(String::valueOf)
这个例子传递了一个String类型的valueOf()静态方法引用, 因此valueOf()
在集合中的实例对象中被当做一个参数传递。等价的lambda:
numbers.stream().map(num -> String.valueOf(num))
4.4、构造器引用
List<String> strings = Arrays.asList("1", "2", "3");
strings.stream().map(Integer::new)
读Collect Elements of a Stream into a Collection看看如何收集元素到集合中。唯一的一个Integer的String参数构造器在这里被使用, 通过一个被当作参数提供的String来构造一个整数,只要这个string代表一个数字, 流将会被转化为整数。等价的lambda:
Collect Elements of a Stream into a Collection
五、例子
5.1.线程初始化
// Old way
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello world");
}
}).start();
// New way
new Thread(
() -> System.out.println("Hello world")
).start();
5.2.事件处理
事件处理可以用 Java 8 使用 Lambda 表达式来完成。以下代码显示了将 ActionListener 添加到 UI 组件的新旧方式:
// Old way
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Hello world");
}
});
// New way
button.addActionListener( (e) -> {
System.out.println("Hello world");
});
5.3.遍例输出(方法引用)
输出给定数组的所有元素的简单代码。请注意,还有一种使用 Lambda 表达式的方式。
// old way
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for (Integer n : list) {
System.out.println(n);
}
// 使用 -> 的 Lambda 表达式
list.forEach(n -> System.out.println(n));
// 使用 :: 的 Lambda 表达式
list.forEach(System.out::println);
5.4.逻辑操作
package com.wuxianjiezh.demo.lambda;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.print("输出所有数字:");
evaluate(list, (n) -> true);
System.out.print("不输出:");
evaluate(list, (n) -> false);
System.out.print("输出偶数:");
evaluate(list, (n) -> n % 2 == 0);
System.out.print("输出奇数:");
evaluate(list, (n) -> n % 2 == 1);
System.out.print("输出大于 5 的数字:");
evaluate(list, (n) -> n > 5);
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for (Integer n : list) {
if (predicate.test(n)) {
System.out.print(n + " ");
}
}
System.out.println();
}
}
运行结果:
输出所有数字:1 2 3 4 5 6 7
不输出:
输出偶数:2 4 6
输出奇数:1 3 5 7
输出大于 5 的数字:6 7
六、总结
6.1.Lambda 表达式和匿名类之间的区别
this 关键字。对于匿名类 this 关键字解析为匿名类,而对于 Lambda 表达式,this 关键字解析为包含写入 Lambda 的类。
编译方式。Java 编译器编译 Lambda 表达式时,会将其转换为类的私有方法,再进行动态绑定。
6.2.理论汇总
1.lambda由参数列表,箭头,主体组成。
2.函数式接口只能拥有一个抽象方法,可以拥有多个默认方法,多个静态方法。
3.方法引用实际就是Lambda的快捷写法。
4.流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍。
5.并行流是采用ForkJoin实现的。
6.在并行流中,不要在peek,map中不要去修改外部数据。
7.并行流使用需要注意,不要靠猜测,请多测试。
8.接口默认方法,优先级最低,子类会继承默认方法并且可以覆盖默认方法。如果因为多继承问题引起冲突(子类实现了两个接口,两个接口都拥有相同的方法名,相同函数描述符),那么必须覆盖该方法,如果期望调用某接口中的默认方法,可以使用X.super.m(…)来显示调用哪个接口的默认方法。
9.接口静态方法,子类不会继承,也不能覆盖,但是可以定义一个名称相同返回值相同的普通或静态方法。