Lambda是一个匿名函数,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),你可以理解为是一段可以传递的代码。使用 Lambda 表达式可以使代码变的更加简洁紧凑,是一种函数式编程方式。首先,我们先了解一下为什么要使用Lambda表达式。
假如现在有一个需求,为农民写个软件,需求是可以在苹果中挑选出所有的红苹果。
第一步,创建Apple类,具有两个属性:颜色和重量,还要get、set方法、重写toString。
public class Apple {
private String color;
private double weight;
// 构造器
// get set 方法
// 重写toString()方法
}
第二步,准备一个集合存放苹果。
List<Apple> apples = Arrays.asList(
new Apple("red",100.22),
new Apple("blue",90.42),
new Apple("red",150.90),
new Apple("blue",120.56));
第三步,实现在所有苹果中挑选出红苹果的逻辑。
private static List<Apple> filterByRedColor(List<Apple> apples){
List<Apple> result = new ArrayList<>();
for(Apple apple : apples){
if("red".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
第四步,调用并打印结果。
List<Apple> result = filterByRedColor(apples);
for(Apple apple : result){
System.out.println(apple);
}
Apple [color=red, weight=100.22]
Apple [color=red, weight=150.9]
很简单的实现了这个功能,但是现在需求有变化,还需要查询绿色的苹果,你可能需要在复制一遍方法,把颜色改成blue就行了。
private static List<Apple> filterByBlueColor(List<Apple> apples){
List<Apple> result = new ArrayList<>();
for(Apple apple : apples){
if("blue".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
如果以后再出现别的颜色的苹果呢,比如黄色,浅绿色等等,这样写明显是有问题的,它违反了DRY(不要重复你自己)的软件设计规则。你可能想到了一个优化办法,将颜色作为参数传入方法进行判断。
private static List<Apple> filterByColor(List<Apple> apples,String color){
List<Apple> result = new ArrayList<>();
for(Apple apple : apples){
if(color.equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
可是需求再次变更,想要找到重量大于150g的红色苹果,这个方法还是不能满足需求,那我们再次优化。
定义一个接口,checkApple方法用来判断苹果是否符合筛选条件。
public interface ApplePredicate {
boolean checkApple(Apple apple);
}
定义两个实现类,分别是筛选红色的苹果和重量大于150g的苹果。
public class AppleRedColorPredicate implements ApplePredicate{
@Override
public boolean checkApple(Apple apple) {
return "red".equals(apple.getColor());
}
}
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Override
public boolean checkApple(Apple apple) {
return apple.getWeight() > 150;
}
}
定义方法,实现对苹果的查询。
private static List<Apple> filter(List<Apple> apples,ApplePredicate ap){
List<Apple> result = new ArrayList<>();
for(Apple apple : apples){
if(ap.checkApple(apple)){
result.add(apple);
}
}
return result;
}
调用方法,并打印结果。
List<Apple> result = filter(apples, new AppleRedColorPredicate());
List<Apple> result2 = filter(apples, new AppleHeavyWeightPredicate());
for(Apple apple : result){
System.out.println(apple);
}
for(Apple apple : result2){
System.out.println(apple);
}
Apple [color=red, weight=100.22]
Apple [color=red, weight=150.9]
Apple [color=red, weight=150.9]
现在采用抽象的思维实现了这个需求,再有新的查询方式只要定义一个类实现ApplePredicate接口就可以了。但是,这样依然很麻烦,如果几十个查询条件岂不是要写几十个实现类,那么我们可以使用匿名内部类来实现。
List<Apple> result = filter(apples, new ApplePredicate(){
@Override
public boolean checkApple(Apple apple) {
return "red".equals(apple.getColor());
}
});
优化并没有到此为止,Java8提供了Lambda表达式来进一步优化代码。
List<Apple> result = filter(apples, (a) -> "red".equals(a.getColor()));
List<Apple> result2 = filter(apples, (a) -> a.getWeight() > 150);
使用Lambda表达式将函数当作参数传入到filter()方法中,你可以发现其实(a)就是匿名内部类重写方法的参数,而->后面的语句则是重写的方法的方法体。此时控制台打印result和result2结果依然是正确的。
Lambda语法
(parameters) -> expression
或
(parameters) ->{ statements; }
其中parameters是参数,参数名随便起,a,b,x,y都行,可以不显示指定参数类型,编译器会根据上下文的环境推断参数类型,如果参数的个数是一个可以不用写小括号。
->是Lambda操作符,也叫箭头操作符。
expression或者statements是Lambda体,如果Lambda体只有一行可以省略大括号,如果只有一行且有返回值的Lambda体也可以省略return关键字,如果是多行的话必须写大括号,且如果有返回值必须显示return。
以下几种都是正确的:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 不需要参数,无返回值
() -> System.out.print(22)
// 3. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 4. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 5. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 6. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
// 7. 接收2个int型整数,返回他们的和 与 差的乘积
(x, y) -> {
int a = x + y;
int b = x - y;
return a * b;
};
函数式接口
使用Lambda表达式有一个要求,就是需要函数式接口的支持,函数式接口指的就是只有一个抽象方法得接口。你可以使用@FunctionalInterface注解声明某个接口是函数式接口,如果接口中超过两个抽象方法就会编译错误。
我们可以通过Lambda表达式创建该接口的对象,若 Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明。
变量作用域
Lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 Lambda 内部修改定义在域外的局部变量,否则会编译错误。
发现weight没有声明为final居然没报错,这是因为编译时Java8会替weight加上final,其实weight还是final修饰的。
如果对weight做运算就会报错。
内置函数接口
此时你发现好像需要先定义一个函数接口才能使用Lambda表达式,其实Java8内置了一些函数接口供我们使用,我们直接使用即可。
四大核心内置接口
其他接口
四大核心内置接口是我们需要重点掌握的,我们可以选择适合业务需求的接口来使用,下面是简单的使用示例。
// 消费型接口 输出内容
@Test
public void test1() {
hello("你好", x -> System.out.println(x));
}
public void hello(String message, Consumer<String> consumer) {
consumer.accept(message);
}
// 供给型接口 把数字添加到集合内
@Test
public void test2() {
List<Integer> list = getList(20, () -> (int) (Math.random() * 100));
for (Integer integer : list) {
System.out.println(integer);
}
}
public List<Integer> getList(int num, Supplier<Integer> supplier) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
list.add(supplier.get());
}
return list;
}
// 函数型接口 去掉首尾空格
@Test
public void test3() {
String str = strHandler(" 你好 ", x -> x.trim());
System.out.println(str);
}
public String strHandler(String str, Function<String, String> function) {
return function.apply(str);
}
// 断言型接口 把符合条件的字符串添加到集合内
@Test
public void test4() {
List<String> strs = Arrays.asList("abc","bcd","acd","ccd");
List<String> result = strCheck(strs, x -> x.contains("a"));
for (String str : result) {
System.out.println(str);
}
}
public List<String> strCheck(List<String> strs, Predicate<String> predicate) {
List<String> list = new ArrayList<>();
for(String str : strs){
if(predicate.test(str)){
list.add(str);
}
}
return list;
}
方法引用
若 Lambda 体中的功能,已经有方法提供了实现,可以使用方法引用,可以将方法引用理解为 Lambda 表达式的另外一种表现形式。主要有以下三种:
- 对象的引用 :: 实例方法名
- 类名 :: 静态方法名
- 类名 :: 实例方法名
使用操作符 “::” 将方法名和对象或类的名字分隔开来。
使用前提:方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致。
对象的引用 :: 实例方法名
@Test
public void test1(){
// Lambda表达式
Consumer<String> con = (x) -> System.out.println(x);
// 使用方法引用
Consumer<String> con2 = System.out::println;
con2.accept("hello");
Apple apple = new Apple("red",150);
// Lambda表达式
Supplier<String> sup = () -> apple.getColor();
// 使用方法引用
Supplier<String> sup2 = apple::getColor;
System.out.println(sup2.get());
}
第一个例子Lambda体是System.out.println(x)。其中System.out.println(x)是一个已经被实现了的方法,它的参数和返回值与Consumer接口方法的参数和返回值一样,所以可以使用方法引用简写。其中System.out是对象也就是PrintStream,println是方法名。
第二个例子Lambda体是apple.getColor(),返回类型是String,所以可以使用方法引用简写Lambda表达式
类名 :: 静态方法名
@Test
public void test2() {
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
System.out.println("-------------------------------------");
Comparator<Integer> com2 = Integer::compare;
BiFunction<Double, Double, Double> fun = (x, y) -> Math.max(x, y);
System.out.println(fun.apply(1.5, 22.2));
System.out.println("--------------------------------------------------");
BiFunction<Double, Double, Double> fun2 = Math::max;
System.out.println(fun2.apply(1.2, 1.5));
}
第一个例子中Lambda体是Integer.compare(x, y),这是Integer类提供的静态方法,而且这个静态方法的参数列表和返回值与Comparator接口方法的参数列表和返回值相同,所以可以使用方法引用。
第二个例子同样,只不过参数是两个。
类名 :: 实例方法名
使用这种方式还需要一个前提:当需要引用方法的第一个参数是调用对象,并且第二个参数是实例方法的参数(或无参数)时才可以使用。
@Test
public void test3() {
BiPredicate<String, String> bp = (x, y) -> x.equals(y);
System.out.println(bp.test("abcde", "abcde"));
System.out.println("-----------------------------------------");
BiPredicate<String, String> bp2 = String::equals;
System.out.println(bp2.test("abc", "abc"));
}
例子中Lambda体是x.equals(y),其中x是第一个参数,且是调用对象,y是第二个参数且是实例方法的参数,所以可以使用方法引入简写。
构造器引用
格式: ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致!
@Test
public void test4() {
Supplier<Apple> sup = () -> new Apple();
System.out.println(sup.get());
System.out.println("------------------------------------");
Supplier<Apple> sup2 = Apple::new;
System.out.println(sup2.get());
Function<String, Apple> fun = Apple::new;
BiFunction<String, Double, Apple> fun2 = Apple::new;
}
第一个例子中Lambda体是通过Apple的无参构造器创建对象,所以可以通过Apple::new简写Lambda表达式,它调用的是Apple的无参构造,因为Supplier<Apple> sup = () -> new Apple();并没有传参数。
public Apple() {}
第二个例子传一个String类型的参数给构造器,可以通过Apple::new简写Lambda表达式,它调用的Apple的String参数的构造器来创建对象。
public Apple(String color) {
this.color = color;
}
第三个例子传入String 和Double类型的参数给构造器,可以通过Apple::new简写Lambda表达式,它调用的Apple的String参数和Double参数的构造器来创建对象。
public Apple(String color, double weight) {
this.color = color;
this.weight = weight;
}
数组引用
格式:Type[]::new
@Test
public void test5() {
Function<Integer, String[]> fun = (args) -> new String[args];
String[] strs = fun.apply(10);
System.out.println(strs.length);
System.out.println("--------------------------");
Function<Integer, Apple[]> fun2 = Apple[]::new;
Apple[] apples = fun2.apply(20);
System.out.println(apples.length);
}
异常处理
处理Unchecked异常
List<Integer> integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));
上述代码在i=0的时候会出现ArithmeticException异常,最简单的方式我们可以通过try—catch处理。
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
try {
System.out.println(50 / i);
} catch (ArithmeticException e) {
System.err.println(
"Arithmetic Exception occured : " + e.getMessage());
}
});
但是此时代码可读性差,不简洁,可以通过编写包装方法consumerWrapper并接收一个lambda表达式,在该方法内会进行异常处理。
static <T, E extends Exception> Consumer<T>
consumerWrapper(Consumer<T> consumer, Class<E> clazz) {
return i -> {
try {
consumer.accept(i);
} catch (Exception ex) {
try {
E exCast = clazz.cast(ex);
System.err.println(
"Exception occured : " + exCast.getMessage());
} catch (ClassCastException ccEx) {
throw ex;
}
}
};
}
integers.forEach(consumerWrapper(x -> System.out.println(50 / x),ArithmeticException.class));
处理Checked异常
本部分内容翻译自:Exceptions in Java 8 Lambda Expressions
这个例子中writeToFile()方法会抛出IOException。IOException是一个Checked异常,所以必须处理。因此要么抛出要么捕获。
static void writeToFile(Integer integer) throws IOException {
// logic to write to file which throws IOException
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));
抛出Checked异常
throws抛出异常仍然会提示错误unhandled IOException。
public static void main(String[] args) throws IOException {
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));
}
尝试实现Consumer,并指定accept抛出Exception,仍然错误。因为accept定义中并未 抛出任何异常。
Consumer<Integer> consumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {
writeToFile(integer);
}
};
通过自定义函数式接口并生命方法抛出异常。
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
void accept(T t) throws E;
}
static <T> Consumer<T> throwingConsumerWrapper(
ThrowingConsumer<T, Exception> throwingConsumer) {
return i -> {
try {
throwingConsumer.accept(i);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));
处理Checked异常
接着对throwingConsumerWrapper进行改造,使其能接收一个异常类型参数。
static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(
ThrowingConsumer<T, E> throwingConsumer, Class<E> exceptionClass) {
return i -> {
try {
throwingConsumer.accept(i);
} catch (Exception ex) {
try {
E exCast = exceptionClass.cast(ex);
System.err.println(
"Exception occured : " + exCast.getMessage());
} catch (ClassCastException ccEx) {
throw new RuntimeException(ex);
}
}
};
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
i -> writeToFile(i), IOException.class));
类似的,可以根据实际需求编写ThowingFunction, ThrowingBiFunction, ThrowingBiConsumer。如果希望使用现成的工具可以考虑Vavr或ThrowingFunction,这两个工具值得一看。vavr的功能简介可参考https://blog.csdn.net/revivedsun/article/details/80088080。