文章目录
初始化
因为是java8的新属性,所以需要在gradle中加入:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
注意,不要加入
jackOptions {
enabled true
}
https://developer.android.com/studio/write/java8-support?utm_source=android-studio 这里提到Jack已不被支持。
示例说明
比如说传统情况下开启一个子线程的写法如下:
new Thread(new Runnable() {
@Override
public void run() {
// 处理具体的逻辑
}
}).start();
而使用Lambda
表达式则可以这样写:
new Thread(() -> {
// 处理具体的逻辑
}).start();
么为什么我们可以这样写呢?这是因为Thread类的构造函数接收的参数是一个Runnable
接口,
并且该接口中只有一个待实现方法。我们查看一下Runnable
接口的源码,如下所示:
public interface Runnable {
public void run();
}
凡是这种只有一个待实现方法的接口,都可以使用Lambda
表达式的写法。比如说,通常创建一个类似于上述接
口的匿名类实现需要这样写:
Runnable runnable = new Runnable() {
@Override
public void run() {
// 添加具体的实现
}
};
而有了Lambda
表达式之后我们就可以这样写了:
Runnable runnable1 = () -> {
// 添加具体的实现
};
了解了Lambda
表达式的基本写法,接下来我们尝试自定义一个接口,然后再使用Lambda
表达式的方式进行实
现。
新建一个MyListener
接口,代码如下所示:
public interface MyListener {
String doSomething(String a, int b);
}
MyListener
接口中也只有一个待实现方法,这和Runnable接口的结构是基本一致的。唯一不同的是,MyListener
中
的doSomething
方法是有参数并且有返回值的,那么我们就来看一看这种情况下该如何使用Lambda
表达式进行
实现。
MyListener listener = (String a, int b) -> {
String result = a + b;
return result;
};
可以看到,doSomething()
方法的参数直接写在括号里面就可以了,而返回值则仍然像往常一样,写在具体实现
的最后一行即可。
另外,Java还可以根据上下文自动推断出Lambda
表达式中的参数类型,因此上面的代码也可以简化成如下写
法:
MyListener listener = (a, b) -> {
String result = a + b;
return result;
};
Java将会自动推断出参数a是String类型,参数b是int类型,从而使得我们的代码变得更加精简了。
接下来举个具体的例子,比如说现在有一个方法是接收MyListener
参数的,如下所示:
public void hello(MyListener listener) {
String a = "Hello Lambda";
int b = 1024;
String result = listener.doSomething(a, b);
Log.d("TAG", result);
}
我们在调用hello()
这个方法的时候就可以这样写:
hello((a, b) -> {
String result = a + b;
return result;
});
那么doSomething()
方法就会将a和b两个参数进行相加,从而最终的打印结果就会是“ Hello Lambda1024” 。
现在你已经将Lambda
表达式的写法基本都掌握了,接下来我们看一看在Android当中有哪些常用的功能是可以使
用Lambda
表达式进行替换的。
其实只要是符合接口中只有一个待实现方法这个规则的功能,都是可以使用Lambda
表达式来编写的。除了刚才
举例说明的开启子线程之外,还有像设置点击事件之类的功能也是非常适合使用Lambda
表达式的。
传统情况下,我们给一个按钮设置点击事件需要这样写:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击事件
}
});
而使用Lambda
表达式之后,就可以将代码简化成这个样子了:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener((v) -> {
// 处理点击事件
});
另外,当接口的待实现方法有且只有一个参数的时候,我们还可以进一步简化,将参数外面的括号去掉,如下
所示:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
// 处理点击事件
});
官方使用说明
Lambda表达式的理想用例
假设你在创建一个社交软件。你想要实现这样一个功能,管理员可以做任何操作,比如发送消息给社交网络上满足一定条件的所有用户。下面的表格详细描述了这个用例。
字段 | 描述 |
---|---|
名字 | 对选中的用户做操作 |
主要实施者 | 管理员 |
前提条件 | 管理员已经登录 |
确认项目 | 目 对满足条件的用户做了操作 |
方案 | 1.管理员指定要对哪些用户做操作 2.管理员指定要对那些选中的用户做的操作 3.管理员点击提交按钮 4.系统找到所有符合要求的用户 5.系统对所有符合的用户执行操作 |
扩展 | 管理员有预览哪些用户符合条件的选项,在他确定要做什么操作或者选择提交之前 |
发生频率 | 一天很多次 |
假设社交软件上的用户用下面的Person类来实现:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假设社交软件上的用户被保存在一个List<Person>
对象中。
这个章节从一个比较傻的方法开始实现这个用例。首先用内部类和匿名类来优化,最后用一个效率且简洁的Lambda表达式来实现。查看这个章节中叫RosterTest的例子的代码。
方法1 创建方法,寻找一个特点相匹配的成员
一个简单的方法是创建多个方法;每个方法搜索匹配一个成员特征,如性别或年龄。下面的方法打印超过指定的年龄的成员:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注意:一个List是一个有序的Collection。是一个对象集合,组织多个元素,成为一个单一的单位。集合是用于存储、检索、操作和综合数据通信。关于集合的更多信息,请参阅集合线索。
这个方法可能让你的软件很难用,软件可能不能正常工作当引入了更新(比如新的数据类型),想象这种情况,你更新了你的软件,更改了Person类的结构比如让它包含不同的成员变量,也可能是一个类使用不一样的数据类型或者算法来记录和计算年龄。你不得不重写很多你的API来适应这些变化。另外,这个方法还有不必要的限制,比如,你要是想打印比这个年龄小的成员该怎么办。
方法2 创建更通用的搜索方法
下面的方法比printPersonsOlderThan
更通用,它打印一个年龄区间内的成员。
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类,加入其他的属性,比如关系状态或者地理位置呢?
尽管这个方法比printPersonsOlderThan
更通用,但是为每个可能的检索去新建特定的方法还是会使代码变得容易失效。你可以将指定对象的检索逻辑代码放到不同的类中。
方法3:指定搜索条件代码在本地类
下面的方法打印那些符合你指定的检索逻辑的成员信息。
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
这个方法调用tester.test方法检查每个参数List roster中的Person
对象是否满足检索条件,如果tester.test返回true,则Person的printPersons
方法会被调用。
你需要实现CheckPerson
接口来指定检索逻辑。
interface CheckPerson {
boolean test(Person p);
}
下面的类实现了CheckPerson
接口,实现了方法test的逻辑。它的参数Person是男性并且年龄在18到25之间则返回true。
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
要使用这个类的时候,你新建一个它的实例,然后调用它的printPerson
方法。
printPersons(roster, new CheckPersonEligibleForSelectiveService());
虽然这个方法比前面的好一点——你不需要重写方法当你改变了Person
的结构——你仍然有多余的代码:对于软件中每个你想要做的检索都需要一个新的接口和一个内部类。CheckPersonEligibleForSelectiveService
实现了接口,你可以使用一个匿名类代替内部类,在每个检索需要声明一个新类的地方。
方法4:指定搜索条件的代码在一个匿名类
下面调用printPersons
方法时的参数之一是一个匿名类,它过滤满足美国选择性服务条件的成员:男性并且年龄在18到25之间。
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
接口是一个函数式接口。一个函数式接口指那些只包含一个抽象方法的接口。(一个函数式接口可以包含一个或多个默认方法或静态方法)因为函数式接口只包含一个抽象方法,所以你可以忽略方法名当你实现它的时候。代替匿名类表达式,你使用Lambda
表达式来做这件事。在下面的方法调用中它被高亮了。
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
方法6:用Lambda表达式来实现标准的函数式接口
重新考虑CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。它是一个函数式接口因为它只包含一个抽象方法。这个方法有一个参数并返回一个boolean值。这个方法是如此简单,可能都不值得去为它定义一个方法。所以,JDK定义了一些标准的函数接口,你可以在java.util.function
包中找到。
比如,你可以使用Predicate<T>
接口来代替CheckPerson
。这个接口包含了方法boolean test(T t)
:
interface Predicate<T> {
boolean test(T t);
}
Predicate<T>
接口是一个泛型接口的例子。泛型(比如泛型接口)用三角括号(<>)来指定一个或多个类型参数。这个接口只有一个类型参数,T。当你使用一个具体的类型声明或者实例化泛型的时候,你将一个类型作为了参数。比如下面的例子,参数化类型Predicate<Person>
。
interface Predicate<Person> {
boolean test(Person t);
}
参数类型包含一个有着相同的返回值和参数的方法比如CheckPerson.boolean test(Person p)
。
所以你可以像下面的方法演示的那样用Predicate<T>
代替CheckPerson
:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
结果是,和步骤3中一样,可以用下面的方法调用来获得可以有选择性服务的成员:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
在方法中使用Lambda
表达式不是唯一可以使用它的地方。下面的步骤中有其他使用Lambda
表达式的建议。
方法7:使用Lambda表达式在您的应用程序
重新考虑这个方法 printPersonsWithPredicate
,思考还有哪里可以使用Lambda
表达式。
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
这个方法检查每个存在于List参数roster中的Person实例,确认是否符合Predicate型参数tester所定义的条件。如果Person实例满足条件,这个Person实例的printPersons
方法会被调用。
不用去调用printPerson
方法,你可以对满足tester所定义的逻辑的成员执行其他的操作。你可以用Lambda
表达式来指定操作。假设你需要一个Lambda
表达式,跟printPerson
类似,有一个参数(Person类型)并返回void
。
记住,要使用lambda
表达式,你需要去实现一个函数式接口。在这个例子中,你需要一个函数式接口,它包含一个有一个Person
参数并返回void的抽象方法。
Consumer<T>
接口包含方法void accept(T t)
,满足这些特性。下面的方法用一个调用accept Consumer<Person>
实例来代替调用p.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);
}
}
}
所以像步骤3一样,可以用lambda
表达式改写为下面的方法调用。
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);
}
}
}
下面的方法获取每个roster中满足选择性服务的成员的email地址并打印它。
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法8:使用泛型更可扩展
再思考下方法processPersonsWithFunction
。下面是一个泛型版本,接收一个包含任何数据类型的集合作为参数。
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);
}
}
}
要打印满足选择性服务的成员的email地址,像这样调用processElements
方法。
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
这个方法执行了以下操作。
1.从集合source中获得对象资源。在例子中,从集合roster中获得Person类型的资源。注意集合roster,它的类型是List,同时也是Iterable的对象。
2.过滤满足Predicate 类型 tester的对象。例子中,Predicate对象是一个lambda
表达式指定哪些成员符合选择性服务。
3.用Function类型mapper将过滤后的对象映射到一个值。例子中,Function对象是一个lambda
表达式返回每个成员的email地址。
4.对映射后的结果值执行Consumer类型block中定义的操作。例子中,Consumer对象是一个lambda
表达式打印Function对象返回的email地址。
你可以将这些操作中的任意一个换成聚合操作。
方法9:使用聚合操作,接受Lambda表达式作为参数
下面的例子用聚合操作来打印roster中满足选择性服务的成员的email地址。
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));
下面的表格展示了processElements
中每个操作与其对应的聚合操作。
processElements操作 | 聚合操作 |
---|---|
获取数据 | Stream stream() |
过滤符合Predicate 的对象 | Stream filter(Predicate<? super T> predicate) |
根据Function映射对象到另一个值 | Stream map(Function<? super T,? extends R> mapper) |
执行Consumer中的操作 | void forEach(Consumer<? super T> action)) |
GUI程序中的Lambda表达式
要处理一个图形用户接口软件的事件,比如键盘操作,鼠标操作和滚动栏操作,你一般会创建事件handler,handler通常会调用一个特定接口的实现。事件handler经常是一个函数式接口,它们应该只有一个方法。
在JavaFX例子中,你可以将匿名类替换成lambda
表达式。
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
方法btn.setOnActio
n定义了当你按了btn代表的按钮时会发生什么。这个方法需要一个EventHandler<ActionEvent>.
类型的对象作为参数。EventHandler<ActionEvent>
接口只有一个方法void handle(T event)
。这是个函数式接口。所以你可以用下面的lambda
表达式代替它。
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda表达式的语法
一个lambda
表达式由以下组成:
- 一个用括号包围并用逗号隔开的参数列表。CheckPerson.test方法包含一个参数,p代表一个Person类型的实例。
注意:在lambda
表达式中,你可以省略数据类型,另外,你可以省略括号当你只有一个参数的时候。比如下面 是lambda
表达式也是合法的:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
- 箭头符号 ->
- 单个表达式或者一个语句块组成的body。例子中是以下表达式:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
如果你定义一个单句表达式,那么Java运行环境会执行这个表达式,然后返回它的值。
或者你可以使用一个return语句。
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
return语句不是表达式,在lambda
表达式中,你必须将语句用花括号({})括起来。然而你不需要将一个void方法括起来。比如下面的lambda
表达式是合法的。
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));
}
}
方法operateBinary
对两个整数做数学运算。操作本身是一个IntegerMath
的实例。例子用lambda
表达式定义了两个操作,addition
和 subtraction
。例子的输出如下:
40 + 2 = 42
20 - 10 = 10
在封闭的作用域中访问本地变量
与内部类和匿名类一样,lambda
表达式可以获取变量,它们对封闭作用域中的本地变量有着相同的权限。
然而,与内部类和匿名类不同,lambda
表达式没有Shadowing
特性(不同作用域,变量重名,ShadowTest.this.x
代表上层ShadowTest
类的scope
中的x
)。
ambda
表达式是单词作用域的。这表示它不从父类中继承名字,也不会引入新的作用域。Lambda
表达式中的声明,被解读为它们在封闭环境中。下面的例子LambdaScopeTest
演示了这个特性。
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// The following statement causes the compiler to generate
// the error "local variables referenced from a lambda expression
// must be final or effectively final" in statement A:
//
// x = 99;
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x); // Statement A
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
例子生成以下输出:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
如果你用x代替y来声明lambda
表达式myConsumer
, 那么编译器会产生错误
Consumer<Integer> myConsumer = (x) -> {
// ...
}
编译器会生成 "variable x is already defined in method methodInFirstLevel(int)"
的错误,因为lambda
表达式不会引入新的作用域。所以,你可以直接访问作用域中的字段,方法,本地变量。
比如,直接访问方法methodInFirstLevel
的参数x
。访问当前类中的变量,使用关键字this
。
例子中,this.x
代表FirstLevel.x
这个成员变量。
然而,与内部类和匿名类一样,lambda
表达式只能访问闭合块中的final
或者effectively final
。
(一个变量在初始化之后,值再也没有被改变,那么它是effectively final
的)
比如,假如你在methodInFirstLevel
声明语句后面紧跟着加上下面的赋值语句。
void methodInFirstLevel(int x) {
x = 99;
// ...
}
由于这个赋值语句,变量FirstLevel.x
不再effectively final
。所以,Java编译器在lambda
表达式myCosumer
想要访问FirstLevel.x
变量时发出类似于 "local variables referenced from a lambda expression must be final or effectively final"
的错误。
目标类型
如何确定lambda
表达式的类型。回顾选择18-25之间男性的lambda
表达式。
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
这个lambda
表达式被使用在两个方法中。
public static void printPersons(List<Person> roster, CheckPerson tester)
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
当Java运行环境调用printPersons
方法,期待一个CheckPerson
类型的数据,所以,lambda
表达式是CheckPerson
类型。
然而当调用printPersonWithPredicate
时,期待Predicate<Person>
类型的数据,所以,lambda
表达式是Predicate<Person>
类型的。这些方法期待的类型叫做目标类型。
要确定一个lambda
表达式的类型,java编译器用上下文的目标类型或者找到的lambda
表达式的内容。
你只能在java编译器可以确定目标类型的情况下使用lambda
表达式。
- Variable declarations 变量定义
- Assignments 赋值
- Return statements 返回语句
- Array initializers 数组初始化
- Method or constructor arguments 方法或构造函数参数
- Lambda expression bodies lambda表达式主体
- Conditional expressions, ?: 三元演算式
- Cast expressions 类型转换表达式
目标类型与方法参数
对于方法参数,java编译器根据两个语言特性来确定目标类型:重载解析与参数类型推断。思考下面函数接口(java.lang.Runable
和java.util.concurrent.Callable<V>
)
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
Runnable
方法没有返回值而Callable<V>
有。假设你像下面这样重载了方法。
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
下面的语句中哪个方法会被调用?
String s = invoke(() -> "done");
方法invoke(Callable<V>)
会被调用,因为它有返回值,而Runnable
没有。在这个例子中,lambda
表达式() -> "done"
的类型是Callable<V>
。
序列化
lambda
表达式的返回值和参数都是可序列化的,它本身也可以序列化。但是和内部类一样,强烈建议不要序列化lambda
表达式。
参考:
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html (官方)
https://www.cnblogs.com/xuemanjiangnan/p/7412734.html
http://www.cnblogs.com/WJ5888/p/4618465.html