一、背景
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
的广泛使用带来了很多样板方法。
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
包下的 Predicate
、Function
、Consumer
类和 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()));
改造如下:
代码语言: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
中使用:
@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 表达式的利弊,给出自己的几点破解之法。
希望对大家有帮助。
当然,可能还有很多解决办法,欢迎留言补充。
希望大家能够努力做个有追求的程序员。