您的位置:首页 > 编程语言 > Java开发

Java 8 Lambda表达式详细解析(一)

2016-10-31 20:43 531 查看
Java 8开始引入Lambda表达式。官网介绍:

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

匿名内部类的介绍:

https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html

嵌套类的介绍:

https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html#shadowing

以下为对官网介绍的理解翻译。

以前可以使用匿名类的方式让语法变得更加简洁,不用重新定义一个新的类,比如接口回调,很多情况下,接口中就定义了一个方法,那么这样会使得匿名内部类的语法显得笨拙。

官方建议,如果接口中有一个以上方法时,使用匿名内部类,如果接口中就只有一个方法,那么就使用Lambda表达式,这样使得语法更加简洁。

官方用一个例子介绍了使用lambda表达式的一个理想场景,假设要查找符合指定条件的人群,通常用以下方法:

public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}


方法1: 根据一个指定的条件创建一个搜索方法。

传入要查找的List和条件,看似简单,但该方法比较脆弱,如果Person类型修改了,比如age不再是一个int型的,那么所有有关的搜索API都要做出相应的修改,并且该方法增加了不必要的限制性,比如要搜索年龄小于指定值的人,那岂不是又要写一个相似的方法了。

方法2: 创建一个更加通用的搜索方法。

public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}


虽然相比方法一更加通用,可以根据一个范围进行搜索,但是如果想指定性别,或者是指定性别和年龄段的混合搜索,当然可以增加多个搜索方法,但是仍然会导致代码的脆弱性,因为Person类结构一改变,又要做很多修改。

方法3: 在一个内部类中写明搜索方法

public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}


该方法在一个新的类中指明具体的搜索条件,即具体实现交给一个具体的实现类来做。首先定义一个接口,然后定义一个实现类,其中可以指明具体的搜索条件,比如以下代码查找符合美国服兵役的人员。

interface CheckPerson {
boolean test(Person p);
}


class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}


使用该类的时候,通过以下声明内部类的形式调用。

printPersons(
roster, new CheckPersonEligibleForSelectiveService());


虽然该方法减少了代码的脆弱性(不可扩展性),即改变Person结构的时候不影响现有代码,但是该方法增加了一个接口,每多一个搜索需求,就会多创建一个新的类。因此可以考虑使用匿名内部类替代内部类的方式。

方法4: 在一个匿名内部类中写明搜索方法。

printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);


使用该方法可以减少创建很多新的类,但是CheckPerson接口中只有一个方法,这使得该方法仍显得笨拙,通过使用lambda表达式替换匿名内部类的方式更加简洁。

方法5: 通过lambda表达式指明搜索方法。

CheckPerson接口是一个函数式接口( functional interface),函数式接口即接口中只有一个abstract方法的接口,当然从Java8开始函数式接口可以有其他的default或者static的方法。由于函数式接口中只有一个abstract方法,通过使用lambda表达式可以省略该方法名,代码如下所示:

printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);


通过使用标准的函数式接口standard functional interface,可以进一步减少代码

方法6: 通过使用标准函数式接口和lambda表达式指明搜索方法。

由于CheckPerson接口中只有一个方法,其太过于简单以至于根本没有必要单独写一个接口类,在jdk8的java.util.function包中预定义了许多标准的函数式接口,比如可以用以下预定义的Predicate接口替换CheckPerson接口:

interface Predicate<T> {
boolean test(T t);
}


因此修改后的方法定义为:

public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}


方法使用时用以下代码:

printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);


当然这不是使用lambda表达式唯一的场景,以下方法7至9介绍了其他的一些使用场景,可以使得lambda表达式广泛应用于整个项目中各个地方。

方法7: 在整个项目中普遍应用lambda表达式。

重新思考以下代码:

public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}


如果针对符合条件的这些结果,除了打印出来之外,还有一些其他的操作需求,比如查找出他们的Email,那么可以使用lambda表达式来明确这些操作。

注:使用lambda表达式,必须要实现一个相应的函数式接口,在这个例子中,需要一个这样的函数式接口,持有一个person的引用,并且返回void值, Consumer <T>  接口中有一个方法void accept(T t),符合上述特性需求,所以可以将对搜索结果的操作通过block来指定,对printPerson()方法替换后,代码如下:


public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}


改动后,原来打印的操作,可以通过以下代码调用来完成:

processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);


如果要对搜索结果进行其他操作,比如获取搜索结果中的人的联系信息,Function<T,R> 接口中有一个方法R apply(T t),可以有返回值,如下代码,通过mapper可以取得数据,然后通过block指定对数据的操作:


public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}


通过下面的调用可以取得搜索结果的Email数据:

processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);


方法8: 将泛型更加广泛的应用于lambda表达式。

上述方法7中指定了操作Person类型的集合,并且对搜索结果操作的数据类型为String,通过使用泛型,可以使得上述代码更加具有通用性,如下:

public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}


修改后的代码可以传入任何数据类型的集合,并对结果集中的任何类型数据进行操作,调用如下:

processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);


方法9: 使用接收lambda表达式作为参数的聚合操作。

上述方法8中的调用形式可以修改为使用聚合操作的方式,如下:

roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));


其中stream()获得操作对象的数据源,filter()是对符合Predicate条件的对象进行过滤,map()是根据Function的描述将过滤后的结果取出需要的值,forEach()是对Consumer对象指定的操作进行处理。

filter、map、forEach都是聚合操作,聚合操作处理stream流中的数据,并不是处理collection中的数据,一个stream流是一系列元素的集合,但其中存储的并不是某一数据结构,stream通过一个管道从一个collection中运输数据,在管道中的是一系列数据操作,比如在本例中为filter-> map-> forEach,其中第一个stream()方法的目的就是做这个工作。

聚合操作接受lambda表达式作为参数的特性,使得你可以定制对collection数据的各类操作。

在GUI程序中使用lambda表达式

例如在android开发中,会有如下的写法:

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
System.out.println("Hello World!")
}
}


使用lambda表达式替换上述匿名内部类的形式,简化后的代码为:

btn.setOnAction(
event -> System.out.println("Hello World!")
);


Lambda表达式的语法

1 一个在圆括号里的,以逗号分隔开的传入形参,在此例中,CheckPerson.test方法有一个参数p,代表Person类型的一个实例,

注:可以在lambda表达式中省略参数的类型,此外如果只有一个参数,还可以省略圆括号,如下形式的代码也是合法的(其实我没看出怎么省略的…)

p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25


2 - >箭头符号,

3 方法体body部分,可以由一个简单的表达式或者一个代码段表示,在这个例子中为:

p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25


如果body部分为一个简单的表达式,那么Java运行时就会求出表达式的值并返回,或者可以直接使用return声明:

p -> {
return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25;
}


return声明不是语句,在lambda表达式中,必须加{},如果方法返回值为void,那么也可以不用加{},如下所示:

email -> System.out.println(email)


lambda表达式看起来就像是方法声明,我们可以把lambda表达式看成是匿名方法,即没有方法名的方法。

以下Calculator示例中的lambda表达式含有两个形参,

public class Calculator {

interface IntegerMath {
int operation(int a, int b);
}

public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}

public static void main(String... args) {

Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}


关于进一步介绍Lambda语法和方法引用的内容参考以下大神的博客:

https://my.oschina.net/benhaile/blog/175012

https://my.oschina.net/benhaile/blog/177148
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java8 lambda表达式