Lambda 表达式带来的复杂性的破解之道

Posted 明明如月学长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lambda 表达式带来的复杂性的破解之道相关的知识,希望对你有一定的参考价值。

一、背景

Java 8 的 Lambda 表达式已经不再是“新特性”。

曾经很多人抵触 Lambda 表达式,现在几乎也成了标配。

实际开发中最常见的是,很多人使用 Stream 来处理集合类。

但是由于 Lambda 表达式的滥用,代码可读性会变差,那么该如何解决?
本文会讨论一下这个问题,并给出自己的几个解决办法。

二、看法

对于 Lambda 表达式或者 Stream 的看法不尽一致。

2.1 支持

(1)使用 Lambda 表达式可以减少类或者方法的创建,写起来比较简洁。
如:

import java.util.HashMap;
import java.util.Map;

public class LambdaMapDemo 
    public static void main(String[] args) 
        Map<Integer, String> map = new HashMap<>();
        for (int i = 0; i < 10; i++) 
            map.put(i, String.valueOf(i));
        

        // 原本写法
        for (Map.Entry<Integer, String> entry : map.entrySet()) 
            System.out.println("k:" + entry.getKey() + " -> v:" + entry.getValue());
        

    


使用 Lambda 的写法

        map.forEach((k, v) -> 
            System.out.println("k:" + k + " -> v:" + v);
        );
    

(2) 使用 Stream 可以享受链式编程的乐趣。

(3)有些人看别人都在用,似乎有些高端,或者担心自己被淘汰也跟着大量使用。

2.2 反对

有些人对 lambda 表达式持反对意见。

(1)他们认为大量使用 lambda 表达式写出的代码不容易理解

(2)还有的团队老人比较多,不太容易接受新的事物,不提倡使用 Lambda 表达式。

如:
Stream 的广泛使用带来了很多样板方法。

List<String> tom = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> dog.getName().toLowerCase()).collect(Collectors.toList());

甚至经常有人会在 Stream 的 map 函数中写大量转换代码。

import lombok.Data;

@Data
public class DogDO 
    private String name;

    private String nickname;

    private String address;

    private String owner;


DogVO 和 DogDO 结构相同。

   List<DogVO> result = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> 
            DogVO dogVO = new DogVO();
            dogVO.setName(dog.getName());
            dogVO.setAddress(dog.getAddress());
            dogVO.setOwner(dog.getOwner());
            return dogVO;
        ).collect(Collectors.toList());

更有甚者直接将整个 Stream 表达结果当做参数传入方法中:

   result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> 
            DogVO dogVO = new DogVO();
            dogVO.setName(dog.getName());
            dogVO.setAddress(dog.getAddress());
            dogVO.setOwner(dog.getOwner());
            return dogVO;
        ).collect(Collectors.toList()));

当一个方法中大量出现上述现象时,代码就没法读了。

(3)还有人担心 Stream 会带来一些副作用。

三、底层原理

参见我的另外一篇文章
《深入理解 Lambda 表达式》

四、建议

Lambda 可以简化代码,但是要把握度,如果滥用 lambda 表达式,代码可读性会很差。

4.1 使用方法引用

 List<String> names = new LinkedList<>();
names.addAll(users.stream().map(user -> user.getName()).filter(userName -> userName != null).collect(Collectors.toList()));
names.addAll(users.stream().map(user -> user.getNickname()).filter(nickname -> nickname != null).collect(Collectors.toList()));

可以优化为:

List<String> names = new LinkedList<>();
        names.addAll(users.stream().map(User::getName).filter(Objects::nonNull).collect(Collectors.toList()));
        names.addAll(users.stream().map(User::getNickname).filter(Objects::nonNull).collect(Collectors.toList()));

4.2 复杂代码抽取出来

对于部分复杂逻辑、对于部分需要复用的逻辑,建议封装成独立的类。
Stream参数中常用的 java.util.function 包下的 PredicateFunctionConsumer 类和 Comparator 等。

   result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> 
            DogVO dogVO = new DogVO();
            dogVO.setName(dog.getName());
            dogVO.setAddress(dog.getAddress());
            dogVO.setOwner(dog.getOwner());
            return dogVO;
        ).collect(Collectors.toList()));

改造如下:

import java.util.function.Function;

public class DogDO2VOConverter implements Function<DogDO, DogVO> 
    @Override
    public DogVO apply(DogDO dogDO) 
        DogVO dogVO = new DogVO();
        dogVO.setName(dogDO.getName());
        dogVO.setAddress(dogDO.getAddress());
        dogVO.setOwner(dogDO.getOwner());
        return dogVO;
    

改造

 result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(new DogDO2VOConverter()).collect(Collectors.toList()));

或者定义静态方法

public class DogDO2VOConverter 
    
    public static DogVO toVo(DogDO dogDO) 
        DogVO dogVO = new DogVO();
        dogVO.setName(dogDO.getName());
        dogVO.setAddress(dogDO.getAddress());
        dogVO.setOwner(dogDO.getOwner());
        return dogVO;
    


直接使用方法调用即可

        result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

4.3 不要将stream 操作放在方法参数中

正如上面的代码一样

        result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

很多人写代码时,喜欢将 Stream 操作放到方法参数中来节省一个局部变量。

我个人非常反对这种行为,这样做极大降低了代码的可读性。

我们应该将 Stream 的操作定义具有明确含义的返回值,然后再使用。
如:

   List<DogVO> toms = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList());
  result.addAll(toms);

4.4 Lambda 表达式不宜过长

很多人尝到了链式编程的甜头以后,总喜欢把代码写的很长。
如:

 Optional.ofNullable(dogs).orElse(new ArrayList<>()).stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

但看这一句代码都很费劲,当一个函数中出现大量这种代码时,简直要吐血。

对于这种长的 Lambda 表达式写法,建议尽可能拆分出来。

    List<Dog> dogs = Optional.ofNullable(dogs).orElse(new ArrayList<>());
        List<String> toms = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()))

然后进一步将 dogs.stream 这部分逻辑封装为子函数。

    List<Dog> dogs = Optional.ofNullable(dogs).orElse(new ArrayList<>());
        List<String> toms = getDogNamesStartWithTom(dogs)

这样就比较清晰易懂。

4.5 样板方法使用泛型封装

如果你发现你的项目中大量使用 Lambda,而且有很多代码的逻辑非常相似,可以考虑使用泛型封装工具类来简化代码。
下面给出两个简单的示例。

4.5.1 Stream 对象转换

实际开发中类似先过滤后转换的代码非常多:

List<DogVO> vos = dogs.stream().map(DogDO2VOConverter::toVo).collect(Collectors.toList())

实际上这种写法习以为常,但一个方法出现多次这种写法就非常不宜读。

封装成工具类之后就比较简洁:

 List<DogVO> vos = MyCollectionUtils.convert(dogs,DogDO2VOConverter::toVo);
       

工具类:

public class MyCollectionUtils 

    public static <S, T> List<T> convert(List<S> source, Function<S, T> function) 

        if (CollectionUtils.isEmpty(source)) 
            return new ArrayList<>();
        

        return source.stream().map(function).collect(Collectors.toList());
    

    public static <S, T> List<T> convert(List<S> source, Predicate<S> predicate, Function<S, T> function) 

        if (CollectionUtils.isEmpty(source)) 
            return new ArrayList<>();
        

        return source.stream().filter(predicate).map(function).collect(Collectors.toList());
    


通过将常见的样板方法封装成工具类,可以极大简化使用时的代码。

4.5.2 Spring 策略模式案例

《巧用 Spring 自动注入实现策略模式升级版》 中提到,如下案例:

定义接口

public interface Handler 

    String getType();

    void someThing();



VIP 用户实现:

import org.springframework.stereotype.Component;

@Component
public class VipHandler implements Handler
    @Override
    public String getType() 
        return "Vip";
    

    @Override
    public void someThing() 
        System.out.println("Vip用户,走这里的逻辑");
    



普通用户实现:

@Component
public class CommonHandler implements Handler

    @Override
    public String getType() 
        return "Common";
    

    @Override
    public void someThing() 
        System.out.println("普通用户,走这里的逻辑");
    


模拟 Service 中使用:


@Service
public class DemoService implements ApplicationContextAware 


    private Map<String, List<Handler>> type2HandlersMap;

    public void test()
      String type ="Vip";
      for(Handler handler : type2HandlersMap.get(type))
          handler.someThing();;
      
    


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 

        Map<String, Handler> beansOfType 以上是关于Lambda 表达式带来的复杂性的破解之道的主要内容,如果未能解决你的问题,请参考以下文章

C11简洁之道:lambda表达式

Java8新特性之一:Lambda表达式

C#代码整洁之道读后习题

java编程之LambdaStreamOptional

IntelliJ:求值lambda表达式在调试时引发编译错误

python lambda表达式