前言
Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁。当开发者在编写Lambda表达式时,也会随之被编译成一个函数式接口。下面这个例子就是使用Lambda语法来代替匿名的内部类,代码不仅简洁,而且还可读。 没有使用Lambda的老方法:
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent actionEvent){
System.out.println("Action detected");
}
});
使用Lambda:
button.addActionListener( actionEvent -> {
System.out.println("Action detected");
});
让我们来看一个更明显的例子。 不采用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表达式不仅让代码变的简单、而且可读、最重要的是代码量也随之减少很多。然而,在某种程度上,这些功能在Scala等这些JVM语言里已经被广泛使用。
一、Lambda表达式有哪些语法?
1.1.语法一(无参数,无返回值)
() -> System.out.println("Hello Lambda!");
例:
@Test
public void test(){
Runnable a = new Runnable(){
@Override
public void run(){
System.out.println("Hello World!")
}
};
Runnable a1 = () -> System.out.println("Hello World!");
a1.run();
}
1.2.语法二(有一个参数,并且无返回值)
(x) -> System.out.println(x)
例:
@Test
public void test(){
Consumer<String> con = (x) -> System.out.println(x);
con.accept("Hello World!");
}
1.3.语法三(有两个以上的参数,有返回值,并且 Lambda 体中有多条语句)
Comparator <Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
例:
@Test
public void test(){
Comparator<Integer> com = (x,y) -> {
System.out.println("hhaha0");
return (x < y) ? -1 : ((x == y) ? 0 : 1);
};
com.compare(1,2);
}
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 表达式的正文有一条以上的语句必须包含在大括号(代码块)中,且表达式的返回值类型要与匿名函数的返回类型相同。
三、函数式接口
说起Lambda,就必须了解函数式接口,因为要使用Lambda,必须在函数式接口上使用。 函数式接口:就是一个有且仅有一个抽象方法,但是可以有多个默认方法的接口,这样的接口可以隐式转换为Lambda表达式。一般在函数式接口上都有个注解@FunctionalInterface,该注解的作用类似@Override一样告诉编译器这是一个函数式接口,用于编译期间检测该接口是否仅有一个抽象方法,如果拥有多个则编译不通过。
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);
}
IntUnaryOperator makeAccumulator(int value) {
return (x) -> { value += x; return value; };
}
如果以这种方式包含一个可改变的变量是必要的, 一个包含此变量的拷贝的合法对象应该被使用, 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.线程初始化
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello world");
}
}).start();
new Thread(
() -> System.out.println("Hello world")
).start();
5.2.事件处理
事件处理可以用 Java 8 使用 Lambda 表达式来完成。以下代码显示了将 ActionListener 添加到 UI 组件的新旧方式:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Hello world");
}
});
button.addActionListener( (e) -> {
System.out.println("Hello world");
});
5.3.遍例输出(方法引用)
输出给定数组的所有元素的简单代码。请注意,还有一种使用 Lambda 表达式的方式。
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for (Integer n : list) {
System.out.println(n);
}
list.forEach(n -> System.out.println(n));
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.接口静态方法,子类不会继承,也不能覆盖,但是可以定义一个名称相同返回值相同的普通或静态方法。
|