七的博客

Java8 Lambda表达式最佳实践

Java

Java8 Lambda表达式跟函数式接口最佳实践

之前写过一篇 【Java8的新特性】,里面有提到过 Lambda 表达式,但是并没有详细展开讲解一些项目中的实践用法。

这里单独用一篇文章总结下平时的最佳实践,同时最佳实践会提供一些正例跟范例,可以更好的理解以及运用到实际项目中。

注意: Lambda 表达式只是让代码更加简洁易读,在效率上通常没很大区别。 【正例】以及【反例】只是代码风格上的写法推荐,不存在对错。说到底都是编译器提供的语法糖,各种写法都能实现相同的功能。

1. 合理使用 Lambda 简化代码

通过使用 Lambda 表达式可以很大程度上简化代码,特别是在匿名内部类的替换上,很多匿名内部类都可以替换成 Lambda 。

当然也不是说替换成新的写法效率上更高,只是替换之后代码会更加简洁易读。

反例:

final Button button = new Button("点击这个按钮");
button.setOnAction(new EventHandler < ActionEvent > () {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("按钮被点击!");
    }
});

正例:

final Button button=new Button("点击这个按钮");
button.setOnAction(event->System.out.println("按钮被点击!"));

2. 保持 Lambda 的简洁性

Lambda 表达式应该保持足够简单以及简洁,这样可以让代码更加容易理解。如果 Lambda 表达式过长,说明应该通过函数抽取等手段,将复杂的代码剥离出去。

场景: 假设我们有一批设备需要建立档案,拿到设备信息后需要校验档案等等一系列操作。

反例:

public class DeviceProcessor {
    
    public static void processDeviceList(List<Device> devices) {
        devices.forEach(user -> {

            // 校验参数非空 n行逻辑

            // 校验字段值是否合适 n行逻辑

            // 校验是否设备在黑名单 n行逻辑

            // 校验是否已经建档过 n行逻辑

            // 校验 更多逻辑....
        });
    }
}

正例:

public class DeviceProcessor {
    
    public static void processDeviceList(List<Device> devices) {
        devices.forEach(UserProcessor::processDevice);
    }


    private static void processDevice(Device device) {
        // 校验参数非空 n行逻辑

        // 校验字段值是否合适 n行逻辑

        // 校验是否设备在黑名单 n行逻辑

        // 校验是否已经建档过 n行逻辑

        // 校验 更多逻辑....
    }

}

将复杂的逻辑被提取到了单独的 processDevice 方法中 , Lambda表达式通过方法引用来调用该方法 , 让代码更加简洁和可读。

3. 合理使用类型推断

在大部分情况下,编译器可以正确的推导出参数的类型,你不需要显式的指定参数类型以及变量名,这通常没很大必要。

反例:

final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach((Integer num) -> System.out.println(num));

正例:

final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(num -> System.out.println(num));

4. 单个参数时不必添加括号

Lambda 表达式只有在多个参数时需要括号,另一种情况就是没有参数时需要括号。

只有单个参数时直接省略括号即可,没必要加上括号。

反例:

final Consumer<String> consumer = (param) -> {
    System.out.println(param);
};

正例:

// 单个参数不需要括号
final Consumer<String> consumer = param -> {
    System.out.println(param);
};

// 多个参数时需要括号
final BiConsumer<String,String> biConsumer = (param1, param2) -> {
    System.out.println(param1 + param2);
};

// 没有参数时需要括号
final Runnable runnable = () -> {
  System.out.println("111");
};

5. 表达式避免有副作用

副作用就是指的是 Lambda 表达式不仅引用了外部的属性,还在表达式内部去更改了外部的属性值。 比如说添加删除元素,更改变量值等。

举个例子说明下一个有副作用的普通函数:

import java.util.ArrayList;
import java.util.List;

public class SideEffectDemo {

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");

        printVersions(versions);

        // [JDK5, JDK6, JDK7, JDK-XX]
        System.out.println(versions);
    }


    public static void printVersions(final List<String> versions) {
        versions.forEach(System.out::println);

        // 篡改了外部传进来的集合
        versions.add("JDK-XX");
    }
}

我们平时写代码的时候,也是经常写这种有副作用的函数,有时候就会让调试变的困难。

在 Lambda 中如果直接更改表达式外的字段值的话将会直接报错,提示 Variable used in lambda expression should be final or effectively final

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");

        boolean findJdk7 = false;
        // 为了模拟编译报错的写法
        versions.forEach(v->{
             if("JDK7".equals(v)){
              // 这里编译会报错,不能在 Lambda 内部修改外部局部变量。
               findJdk7= true;
             }
         });
    }

Lamdba 中尽量避免产生有副作用的逻辑。如果你还是想像上面这么写的话,可以换一种形式。 比如将 findJdk7 变量使用数据包起来或者使用原子类,这样你这是更改了引用内的属性而不是更改引用地址。

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");
        
        // 改成原子类
        final AtomicBoolean findJdk7 = new AtomicBoolean(false);
        versions.forEach(v -> {
            if ("JDK7".equals(v)) {
                // 这里编译不会报错
                findJdk7.set(true);
            }
        });
    }

用数组也一样可以:

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");

        // 改成数组
        final boolean[] findJdk7 = {false};
        versions.forEach(v -> {
            if ("JDK7".equals(v)) {
                // 这里编译不会报错
                findJdk7[0] = true;
            }
        });
    }

6. 优先使用方法引用

在某些场景下,使用方法引用可以比 Lambda 表达式更加清晰、更简洁。

反例:

final List<String> versionList = Arrays.asList("JDK5", "JDK6", "JDK7");
versionList.forEach(v -> System.out.println(v));

正例:

final List<String> versionList = Arrays.asList("JDK5", "JDK6", "JDK7");
versionList.forEach(System.out::println);

7. 避免不必要的 return 以及花括号

简单的 Lambda 语句中, return 以及返回值语句是没有必要写的。 同时花括号也是可以省略的。

反例:

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");

        final List<String> collected = versions
                .stream()
                .map(v -> {   // 这里没有必要使用花括号以及 return
                    return v + "-release";
                })
                .collect(Collectors.toList());

        //[JDK5-release, JDK6-release, JDK7-release]
        System.out.println(collected);
    }

正例:

    public static void main(String[] args) {
        final List<String> versions = new ArrayList<>();
        versions.add("JDK5");
        versions.add("JDK6");
        versions.add("JDK7");

        final List<String> collected = versions
                .stream()
                .map(v -> v + "-release")   // 直接省略 return 以及花括号即可
                .collect(Collectors.toList());

        //[JDK5-release, JDK6-release, JDK7-release]
        System.out.println(collected);
    }

8. 合理的使用以及拆分流操作

注意: 这一点在实践中需要根据团队规定来进行选择

流操作提供了一种高效且简洁的方式来处理集合数据,流操作用的合适可以使代码更加简洁、易读和易维护。但是滥用流操作,尤其是一堆的复杂逻辑堆一起或者包含副作用的操作,可能会使代码难以理解和维护。

例如下面这个例子,实际项目中可能还包含数据库查询操作等等更加复杂的逻辑:

    public static void main(String[] args) {
        List<String> names = Arrays.asList("JDK5", "JDK6", "JDK7");


        // 在流操作中做很多复杂逻辑嵌入,不易读
        final List<String> result = names.stream().filter(V -> {
            if (V.length() > 3) {
                System.out.println(V + " 的长度太长了");
                return true;
            } else {
                return false;
            }
        }).map(V -> {
            String upperName = V.toUpperCase();
            System.out.println("转换 " + V + " 到 " + upperName);
            return upperName;
        }).collect(Collectors.toList());

        System.out.println(result);
    }

还有一种就是学习 Python 的一行代码风格,表达式写在一行的。这种代码简洁是很简洁,但是对于后面维护简直就是种灾难。

可以将一些复杂的流操作剥离到小方法中,做如下改写 ( 可以参考第一小节 ):


import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ComplexStreamDemo {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("JDK5", "JDK6", "JDK7");


        // 在流操作中做很多复杂逻辑嵌入,不易读
        final List<String> result = names
                .stream()
                .filter(ComplexStreamDemo::filter)
                .map(ComplexStreamDemo::convert)
                .collect(Collectors.toList());

        System.out.println(result);
    }

    private static boolean filter(String V) {
        if (V.length() > 3) {
            System.out.println(V + " 的长度太长了");
            return true;
        } else {
            return false;
        }
    }

    private static String convert(String V) {
        String upperName = V.toUpperCase();
        System.out.println("转换 " + V + " 到 " + upperName);
        return upperName;
    }
}

9. 优先使用 JDK 内置的函数式接口

JDK 内置了很多实用的函数式接口,很多时候我们不需要自己重新去写。 这些接口都集中在 java.util.function 这个包里,可以满足场景下的 Lambda 表达式和方法引用。

在团队协作中的优点就是,同事一看你的表达式就可能直接看懂作用是什么。 可以降低很多不必要的沟通成本以及维护成本。

比如我们需要对集合中的元素进行验证是否匹配某个规则:

反例:

import java.util.Arrays;
import java.util.List;


public class NoStandardFunctionDemo {
    
    public static void main(String[] args) {
        final List<String> names = Arrays.asList("JDK5", "JDK6", "JDK7", "Node");

        // 自定义一个函数式接口来检查字符串
        final StringFilter filterByJDK = name -> name.startsWith("JDK");

        names.stream()
                .filter(filterByJDK::isValid) // 使用自定义接口
                .forEach(System.out::println);
    }

    // 自定义一个函数式接口
    @FunctionalInterface
    interface StringFilter {
        boolean isValid(String string);
    }
}

正例:

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class StandardFunctionDemo {
    public static void main(String[] args) {
        final List<String> names = Arrays.asList("JDK5", "JDK6", "JDK7", "Node");

        // 使用官方提供的 Predicate<T> 来检查字符串是否以 "JDK" 开头
        Predicate<String> startsWithJDK = name -> name.startsWith("JDK");

        names.stream()
                .filter(startsWithJDK)  // 做过滤操作
                .forEach(System.out::println);
    }
}

关于函数式接口要是展开内容也挺多的,后续打算单独用一篇文章总结下。

10. Lambda 限制以及优缺点

  • 因为 Lambda 表达式本质上是对单个方法的实现 , 所以 Lambda 表达式只能用于函数式接口。 普通的接口类是没办法使用的。

  • Lambda 表达式里面不可以修改定义在外部的局部变量。除非外面的变量声明为 final 类型, 还有一种情况就是外面变量的值在初始化后不再改变也是可以修改的。

  • 由于 Lambda 表达式是匿名的,没有函数名,在调试程序的时候,有时候会变得困难,特别是在抛异常的情况下。

  • 一些复杂场景下,类型推断可能会不太好用,需要显式指定类型以及参数名。

  • 如果在 Lambda 表达式中抛出受检异常,代码中必须显式处理或声明抛出该异常,否则编译会不通过。