Lambda 表达式
Java8 新特性中有几个比较主要的特性,Lambda 表达式、函数式接口、方法引用和几个新增库 Stream API、Optional 类等。
Lambda 表达式简介
什么是 Lambda ?
-
Lambda 表达式其实就是一种匿名函数,在这之前大家可能还接触过匿名内部类,虽然也是一种匿名函数,但是它并没有 Lambda 表达式那么简洁。
-
Lambda 表达式的特点就是可以传递,在使用 Lambda 表达式的时候我们可以将代码像数据一样传递。
-
使用 Lambda 表达式能够极大的减少我们代码的冗余,而且使用也相当的方便,熟练之后会大大加快我们写代码的速度。
函数式编程思想概述:
-
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”。
-
面向对象思想强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做”。
-
Lambda 表达式就是函数式思想的体现。
Lambda 表达式入门案例
下面用 Java8 的方法与老版本进行使用对比:
- 尝试找出公司员工年龄大于 35 岁的所有员工,并用不同的方法来实现一下。
- 假设有一个 Staff 类来代表员工,并使用构造函数来创建员工的集合。
Staff 类:
public class Staff {
int age;
String name;
public String getName() {
return name;
}
public Staff(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Staff{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Staff(int age) {
this.age = age;
}
}
实现方式一:常规方法
public List<Staff> getStaffs(List<Staff> staffs) {
List<Staff> stas = new ArrayList<>();
for(Staff sta: staffs){
if (sta.getAge()>=35){
stas.add(sta);
}
}
return stas;
}
@Test
public void test1(){
List<Staff> list = getStaffs(staffs);
for (Staff staf: list) {
System.out.println(staf);
}
}
实现方式二:策略设计模式
我们需要写一个接口,然后实行这个接口,最后是调用接口。这种方法看起来就比前面的那种更有层次,而且假如又需要查找员工工资大于 5000 的员工的话,就重新写一个类来实现接口就好了
import java.util.ArrayList;
import java.util.List;
// 接口
interface MyPredicate<Staff> {
boolean test(Staff staff);
}
// 实现接口
class filterStaffByAge implements MyPredicate<Staff> {
@Override
public boolean test(Staff staff) {
return staff.getAge() >= 35;
}
}
// 测试类
public class Test {
// 过滤
public List<Staff> filterStaff(List<Staff> list, MyPredicate<Staff> mp){
List<Staff> staffs = new ArrayList<>();
for (Staff staff: list) {
if (mp.test(staff)){
staffs.add(staff);
}
}
return staffs;
}
// 测试结果
public static void main(String[] args) {
ArrayList<Staff> staffs = new ArrayList<>();
staffs.add(new Staff(24, "xiaohua"));
staffs.add(new Staff(36, "xiaoming"));
staffs.add(new Staff(21, "xiaoguang"));
staffs.add(new Staff(37, "xiaoliuhua"));
List<Staff> list = new Demo().filterStaff(staffs, new filterStaffByAge());
for (Staff staff: list) {
System.out.println(staff);
// Staff{age=36, name='xiaoming'}
// Staff{age=37, name='xiaoliuhua'}
}
}
}
实现方式三:匿名内部类
package com.annotation;
import java.util.ArrayList;
import java.util.List;
// 接口
interface MyPredicate<Staff> {
boolean test(Staff staff);
}
// 测试类
public class Demo {
// 过滤
public List<Staff> filterStaff(List<Staff> list, MyPredicate<Staff> mp){
List<Staff> staffs = new ArrayList<>();
for (Staff staff: list) {
if (mp.test(staff)){
staffs.add(staff);
}
}
return staffs;
}
public static void main(String[] args) {
ArrayList<Staff> staffs = new ArrayList<>();
staffs.add(new Staff(24, "xiaohua"));
staffs.add(new Staff(36, "xiaoming"));
staffs.add(new Staff(21, "xiaoguang"));
staffs.add(new Staff(37, "xiaoliuhua"));
// 使用匿名内部类
List<Staff> list = new Demo().filterStaff(staffs, new MyPredicate<Staff>(){
@Override
public boolean test(Staff staff) {
return staff.getAge() >= 35;
}
});
for (Staff staff: list) {
System.out.println(staff);
}
}
}
实现方式四:Lambda 表达式
Lambda 表达式免去了上述 Java8 之前要么重新写方法,要么重新实现接口的繁琐。
Lambda 表达式是对匿名内部类的使用优化。
// Lambda表达式
List<Staff> list = new Demo().filterStaff(staffs, (e)->e.getAge()>=35); // 代替接口实现
// Java8的新特性:使用“::”方法引用
list.forEach(System.out::println);
实现方式五:Lambda 表达式 + Stream API
前面的示例都依赖了 filterStaff 方法,那如果现在只有员工数据,没有 filterStaff 函数,那么 Java8 可不可以实现找出年龄大于 35 的员工呢?答案是可以的,那就要用到 Java8 新特性中的 Stream API 。
staffs.stream()
.filter((e)->e.getAge()>=35)
.forEach(System.out::println);
Lambda 表达式语法
Lambda 表达式的标准格式:
格式:(形式参数) -> {代码块}
形式参数
:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可。->
:由英文中画线和大于符号组成,固定写法,代表指向动作。代码块
:是我们具体要做的事情。
Lambda 表达式的使用前提:
- 有一个接口。
- 接口中有且仅有一个抽象方法。
Lambda 语法格式:
//语法格式一:无参数,无返回值。
() -> System.out.println("Hello");
// 语法格式二:有一个参数,并且无返回值。
(e) -> System.out.println(e);
// 语法格式三:若只有一个参数,参数的小括号可以省略。
e -> System.out.println(e);
// 语法格式四:有两个以上的参数,又返回值,有多条执行语句。
(x, y) -> {
System.out.println("Lambda");
return Integer.compare(x, y);
};
// 语法格式五:如果有两个以上的参数,又返回值,只有一条执行语句,Lambda 体的大括号和return可以省略。
Comparator<Integer>con=(x, y) -> Integer.compare(x, y);
// 语法格式六:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器可以通过上下文推断出数据类型,即“类型推断”。
(Integer x, Integery) -> Integer.compare(x, y);
示例 1:无参无返回值的抽象方法
- 定义一个接口 (Eatable),里面定义一个抽象方法:void eat();
- 定义一个测试类 (EatableDemo),在测试类中提供两个方法
- 一个方法是:useEatable(Eatable e)
- 一个方法是主方法,在主方法中调用 useEatable 方法
// 接口
public interface Eatable {
void eat();
}
// 实现类
public class EatableImpl implements Eatable {
@Override
public void eat() {
System.out.println("一天一苹果,医生远离我");
}
}
//测试类
public class EatableDemo {
// 在主方法中调用 useEatable 方法
public static void main(String[] args) {
// 实现类的方式
Eatable e = new EatableImpl();
useEatable(e);
// 匿名内部类的方式
useEatable(new Eatable() {
@Override
public void eat() {
System.out.println("一天一苹果,医生远离我");
}
});
// Lambda 表达式的方式
useEatable(() -> {
System.out.println("一天一苹果,医生远离我");
});
}
private static void useEatable(Eatable e) {
e.eat();
}
}
示例 2:有参无返回值的抽象方法
interface Flyable {
void fly(String s);
}
public class Test {
public static void main(String[] args) {
// 匿名内部类
useFlyable(new Flyable() {
@Override
public void fly(String s) {
System.out.println(s);
System.out.println("飞机自驾游");
}
});
System.out.println("--------");
// Lambda
useFlyable((String s) -> {
System.out.println(s);
System.out.println("飞机自驾游");
});
}
private static void useFlyable(Flyable f) {
f.fly("风和日丽,晴空万里");
}
}
示例 3:有参有返回值的抽象方法
public interface Addable {
int add(int x, int y);
}
public class AddableDemo {
public static void main(String[] args) {
// lambda 实现 Addable
useAddable((int x, int y) -> {
return x + y;
});
}
private static void useAddable(Addable a) {
int sum = a.add(10, 20);
System.out.println(sum);
}
}
Lambda 表达式的省略模式
省略的规则:
- 参数类型可以省略。
- 如果参数有且仅有一个,那么小括号可以省略。
- 如果代码块的语句只有一条,可以省略大括号和分号,和 return 关键字。
public interface Addable {
int add(int x, int y);
}
public interface Flyable {
void fly(String s);
}
public class LambdaDemo {
public static void main(String[] args) {
// useAddable((int x, int y) -> {
// return x + y;
// });
// 参数的类型可以省略
useAddable((x, y) -> {
return x + y;
});
// useFlyable((String s) -> {
// System.out.println(s);
// });
// // 如果参数有且仅有一个,那么小括号可以省略
// useFlyable(s -> {
// System.out.println(s);
// });
// 如果代码块的语句只有一条,可以省略大括号和分号
useFlyable(s -> System.out.println(s));
// 如果代码块的语句只有一条,可以省略大括号和分号,如果有return,return也要省略掉
useAddable((x, y) -> x + y);
}
private static void useFlyable(Flyable f) {
f.fly("风和日丽,晴空万里");
}
private static void useAddable(Addable a) {
int sum = a.add(10, 20);
System.out.println(sum);
}
}
Lambda 表达式和匿名内部类的区别
所需类型不同:
- 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。
- Lambda 表达式:只能是接口。
使用限制不同:
- 如果接口中有且仅有一个抽象方法,可以使用 Lambda 表达式,也可以使用匿名内部类。
- 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用 Lambda 表达式。
实现原理不同:
- 匿名内部类:编译之后,产生一个单独的 .class 字节码文件。
- Lambda 表达式:编译之后,没有一个单独的 .class 字节码文件,对应的字节码是在运行的时候动态生成。
函数式接口
什么是函数式接口?
-
Lambda 表达式需要“函数式接口”的支持。
-
函数式接口:接口中只有一个抽象方法,称为函数式接口。可以使用注解 @FunctionalInterface 修饰(检测是否为函数式接口)。
Java8 四大内置核心函数式接口
Java8 给我们准备了现成的函数式接口,除了很特殊的场景需要自己写函数式接口外,大部分时候我们都可以使用内置的函数式接口,下面来看看四大内置接口及其使用方法:
Consumer<T>: 消费型接口
void accep(T t);
Supplier<T>: 供给型接口
T get();
Function<T, R>: 函数型接口
R apply(T t);
Predicate<T>: 断言型接口
boolean Test(T t);
使用示例:
// 取10个随机数
List<Integer> numlist = getNumList(10, ()->(int)Math.random());
for (Integer num: numlist) {
System.out.println(num);
}
}
// 取随机数
public List<Integer> getNumList(int num, Supplier<Integer> sup){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
Integer n=sup.get();
list.add(n);
}
return list;
}
//Function<T, R>:函数型接口
//用于处理字符串
public String StrHandler(String str, Function<String, String> fun){
return fun.apply(str);
}
// 测试函数型接口
@Test
public void test3(){
String str=StrHandler("\t\t\t 就是对快睡觉",(x)->x.trim());
System.out.println(str);
String uper=StrHandler("\t\t\t 就是对快睡觉",(x)->x.toUpperCase());
System.out.println(uper);
String newStr=StrHandler("\t\t\t 就是对快睡觉",(x)->x.substring(2,5));
System.out.println(newStr);
}
//Predicate<T>:断言型接口
//将满足条件的字符串放入集合中
public List<String> StrFilter(List<String> list, Predicate<String> pre){
List<String> strList = new ArrayList<>();
for (String str: list) {
if (pre.test(str)){
strList.add(str);
}
}
return strList;
}
// 测试断言型接口
@Test
public void test4(){
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "bbbb", "jahdgja");
List<String> strList = StrFilter(list, s->s.length()>3);
for (String str: strList) {
System.out.println(str);
}
}
方法引用
方法引用:指若 Lambda 体中的内容已经有方法实现了,那么我们就可以使用“方法引用”。
主要有三种语法格式:
-
对象::实例方法名
-
类::静态方法名
-
类::实例方法名
注意:
Lambda 体中调用方法的参数列表和返回值类型,要与函数式接口中抽象方法的参数列表和返回值一致。
如若 Lambda 参数列表中的第一个参数是实例方法的调用者,而第二个参数是实例方法的参数时,可以使用 ClassName::method
构造器引用:
-
格式:ClassName::new
-
注意:需要调用的构造器的参数列表,要与函数式接口中抽象方法的参数列表保持一致。
数组引用:
- 格式:Type::new