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

2022-02-15 08:26:02 浏览数 (1)

一、背景

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

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

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

但是由于 Lambda 表达式的滥用,代码可读性会变差,那么该如何解决?

本文会讨论一下这个问题,并给出自己的几个解决办法。

二、看法

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

2.1 支持

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

如:

代码语言:javascript复制
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 的写法

代码语言:javascript复制
        map.forEach((k, v) -> {
            System.out.println("k:"   k   " -> v:"   v);
        });
    }

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

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

2.2 反对

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

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

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

如:

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

代码语言:javascript复制
List<String> tom = dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(dog -> dog.getName().toLowerCase()).collect(Collectors.toList());

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

代码语言:javascript复制
import lombok.Data;

@Data
public class DogDO {
    private String name;

    private String nickname;

    private String address;

    private String owner;

}

DogVO 和 DogDO 结构相同。

代码语言:javascript复制
   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 表达结果当做参数传入方法中:

代码语言:javascript复制
   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 会带来一些副作用。

2.3 我的看法

Lambda 是双刃剑,使用时需要把握好度。

如何破解 Lambda 表达式带来的复杂性,请看第四部分。

三、底层原理

参见我的另外一篇文章

《深入理解 Lambda 表达式》

四、建议

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

4.1 使用方法引用

代码语言:javascript复制
 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()));

可以优化为:

代码语言:javascript复制
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 等。

代码语言:javascript复制
   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()));

改造如下:

代码语言:javascript复制
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;
    }
}

改造

代码语言:javascript复制
 result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(new DogDO2VOConverter()).collect(Collectors.toList()));

或者定义静态方法

代码语言:javascript复制
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;
    }
}

直接使用方法调用即可

代码语言:javascript复制
        result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

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

正如上面的代码一样

代码语言:javascript复制
        result.addAll(dogs.stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

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

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

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

如:

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

4.4 Lambda 表达式不宜过长

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

如:

代码语言:javascript复制
 Optional.ofNullable(dogs).orElse(new ArrayList<>()).stream().filter(dog -> dog.getName().startsWith("tom")).map(DogDO2VOConverter::toVo).collect(Collectors.toList()));

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

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

代码语言:javascript复制
    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 这部分逻辑封装为子函数。

代码语言:javascript复制
        List<String> toms = getDogNamesStartWithTom(dogs)

4.5 样板方法使用泛型封装

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

下面给出两个简单的示例。

4.5.1 Stream 对象转换

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

代码语言:javascript复制
List<DogVO> vos = dogs.stream().map(DogDO2VOConverter::toVo).collect(Collectors.toList())

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

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

代码语言:javascript复制
 List<DogVO> vos = MyCollectionUtils.convert(dogs,DogDO2VOConverter::toVo);

工具类:

代码语言:javascript复制
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 自动注入实现策略模式升级版》 中提到,如下案例:

定义接口

代码语言:javascript复制
public interface Handler {

    String getType();

    void someThing();
}

VIP 用户实现:

代码语言:javascript复制
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用户,走这里的逻辑");
    }
}

普通用户实现:

代码语言:javascript复制
@Component
public class CommonHandler implements Handler{

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

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

模拟 Service 中使用:

代码语言:javascript复制
@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 = applicationContext.getBeansOfType(Handler.class);
        beansOfType.forEach((k,v)->{
            type2HandlersMap = new HashMap<>();
            String type =v.getType();
            type2HandlersMap.putIfAbsent(type,new ArrayList<>());
            type2HandlersMap.get(type).add(v);
        });
    }
}

其中 setApplicationContext 里面的代码非常相似。

可以编写工具类

代码语言:javascript复制
import org.apache.commons.collections4.MapUtils;
import org.springframework.context.ApplicationContext;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class BeanStrategyUtils {

// 构造type 到多个 bean 的映射
  public static <K,B> Map<K, List<B>> buildTypeBeansMap(ApplicationContext applicationContext, Class<B> beanClass, Function<B,K> keyFunc) {
        Map<K, List<B>> result = new HashMap<>();

        Map<String, B> beansOfType = applicationContext.getBeansOfType(beanClass);
       if(MapUtils.isEmpty(beansOfType)){
           return result;
       }

        for(B bean : beansOfType.values()){
            K type = keyFunc.apply(bean);
            result.putIfAbsent(type,new ArrayList<>());
            result.get(type).add(bean);
        }
        return result;
    }

// 构造type 到单个 bean 的映射
    public static <K,B> Map<K, B> buildType2BeanMap(ApplicationContext applicationContext, Class<B> beanClass, Function<B,K> keyFunc) {
        Map<K, B> result = new HashMap<>();

        Map<String, B> beansOfType = applicationContext.getBeansOfType(beanClass);
        if(MapUtils.isEmpty(beansOfType)){
            return result;
        }

        for(B bean : beansOfType.values()){
            K type = keyFunc.apply(bean);
            result.put(type,bean);
        }
        return result;
    }
}

改造后

代码语言:javascript复制
@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 {
        type2HandlersMap = BeanStrategyUtils.buildTypeBeansMap(applicationContext,Handler.class, Handler::getType);
    }
}

很多人可能会说,写工具方法也很花时间呢。

但是,写工具方法之后,代码重复率降低;代码更加简洁,可读性提高;后续类似逻辑都可以实现代码复用,开发效率也提高了;一举多得。

4.6 使用加强包

前面讲到了,可以通过封装工具类来减少 Lambda 代码的复杂性。

此外,我们还可以考虑使用一些加强包来解决这个问题。

4.6.1 StreamEx

如 StreamEx

Maven 依赖

https://mvnrepository.com/artifact/one.util/streamex

代码语言:javascript复制
<dependency>
    <groupId>one.utilgroupId>
    <artifactId>streamexartifactId>
    <version>0.8.0version>
dependency>

Java 8 写法

代码语言:javascript复制
 Map<Role, List<User>> role2users = users.stream().collect(Collectors.groupingBy(User::getRole));

StreamEx 写法:

代码语言:javascript复制
Map<Role, List<User>> role2users = StreamEx.of(users).groupingBy(User::getRole);

前面的案例

代码语言:javascript复制
List<DogVO> vos = dogs.stream().map(DogDO2VOConverter::toVo).collect(Collectors.toList())

就可以改为

代码语言:javascript复制
 List<DogVO> vos = StreamEx.of(dogs).map(DogDO2VOConverter::toVo).toList();

4.6.2 vavr

vavr

用户文档:https://docs.vavr.io/

Maven 依赖

https://mvnrepository.com/artifact/io.vavr/vavr

代码语言:javascript复制
<dependency>
    <groupId>io.vavrgroupId>
    <artifactId>vavrartifactId>
    <version>1.0.0-alpha-4version>
dependency>

Java 8 中的写法:

代码语言:javascript复制
// = ["1", "2", "3"] in Java 8
Arrays.asList(1, 2, 3)
      .stream()
      .map(Object::toString)
      .collect(Collectors.toList())

vavr 中的写法:

代码语言:javascript复制
// = Stream("1", "2", "3") in Vavr
Stream.of(1, 2, 3).map(Object::toString)

4.7 每个步骤加上注释

如果使用链式编程,如果步骤较多时,建议每个步骤都加上注释,这样更容易理解。

4.8 有些场景不用 Lambda 表达式

如果你发现某个函数里使用 Lambda 过多时(实际工作中,发现会有人一个函数里一半以上都是 lambda 表达式,非常头疼),可以考虑将部分不容易懂的 Lambda 写法改为普通写法,通常可读性会大大提高。

代码语言:javascript复制
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()));

优化为

代码语言:javascript复制
 List<String> names = new LinkedList<>();
        for(User user : users) {
            String name = user.getName();
            if(name!= null ){
                names.add(name);
            }
            
            String nickname = user.getNickname();
            if(nickname != null){
                names.add(nickname);
            }
        }

虽然代码更长,但是更容易看懂。

还可将该部分逻辑封装为一个子函数并给一个有意义的命名。

代码语言:javascript复制
  /**
     * 获取名称和昵称
     */
    private  List<String> getNamesAndNickNames(List<User> users) {
        List<String> names = new LinkedList<>();
        for (User user : users) {
            String name = user.getName();
            if (name != null) {
                names.add(name);
            }

            String nickname = user.getNickname();
            if (nickname != null) {
                names.add(nickname);
            }
        }
        return names;
    }

使用时直接调用即可:

代码语言:javascript复制
List<String> names = getNamesAndNickNames(users);

这样外层函数可以明确知道这部分逻辑的意图,压根不需要看这么多代码,可读性大大提高。

五、思考

过犹不及,我们使用 Lambda 表达式时,一定不要忽略可读性。

Lambda 表达式没有错,错在很多人滥用 Lambda 表达式。

我们在编码过程中要注意做好权衡,掌握好度。

本文简单谈了 Lambda 表达式的利弊,给出自己的几点破解之法。

希望对大家有帮助。

当然,可能还有很多解决办法,欢迎留言补充。

希望大家能够努力做个有追求的程序员。

0 人点赞