The solution to the complexity brought by Lambda expression

1, Background

Lambda expressions in Java 8 are no longer a "new feature".

Once many people resisted Lambda expressions, but now they have almost become standard.

The most common thing in actual development is that many people use Stream to deal with collection classes.

However, due to the abuse of Lambda expressions, the readability of the code will become worse, so how to solve it? This paper will discuss this problem and give some solutions.

2, View

There are different views on Lambda expression or Stream.

2.1 support

(1) Using Lambda expressions can reduce the creation of classes or methods, which is relatively simple to write. For example:

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));
        }

        // Original writing
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println("k:" + entry.getKey() + " -> v:" + entry.getValue());
        }

    
}

Use Lambda

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

(2) Using Stream can enjoy the fun of chain programming.

(3) Some people see that others are using it. It seems that some are high-end, or they worry that they will be eliminated and use it in large quantities.

2.2 objection

Some people object to lambda expressions.

(1) They think that the code written with a lot of lambda expressions is not easy to understand.

(2) In addition, there are many old people in the team, who are not easy to accept new things and do not advocate the use of Lambda expressions

For example: The widespread use of Stream has brought many template methods.

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

Some people even write a lot of conversion code in the map function of Stream.

import lombok.Data;

@Data
public class DogDO {
    private String name;

    private String nickname;

    private String address;

    private String owner;

}

DogVO and DogDO have the same structure.

   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());

What's more, the whole Stream expression result is directly passed into the method as a parameter:

   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()));

When a large number of the above phenomena occur in a method, the code cannot be read.

(3) Others worry that Stream will bring some side effects.

2.3 my opinion

Lambda is a double-edged sword. It needs to grasp the degree when using it. How to crack the complexity brought by Lambda expression, please see Part 4.

3, Underlying principle

See my other article Deep understanding of Lambda expressions

4, Suggestion

Lambda can simplify the code, but we should grasp the degree. If lambda expressions are abused, the code readability will be very poor.

4.1 use method reference

 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()));

Can be optimized as:

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 complex code extraction

For some complex logic and some logic that needs to be reused, it is recommended to package them into independent classes. For example, Java. Net is commonly used in Stream parameters util. Predict, Function, Consumer class and Comparator under Function package.

   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()));

The transformation is as follows:

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;
    }
}

reform

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

Or define static methods

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;
    }
}

Just use method call directly

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

4.3 do not put stream operation in method parameters

Just like the code above

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

When writing code, many people like to put the Stream operation into the method parameters to save a local variable.

I personally object to this behavior, which greatly reduces the readability of the code.

We should define the operation of Stream as a return value with clear meaning, and then use it. For example:

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

4.4 Lambda expression should not be too long

After many people have tasted the sweetness of chain programming, they always like to write the code very long.

For example:

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

But it's hard to look at this code. When a large number of this code appears in a function, it's like vomiting blood.

For this long Lambda expression, it is recommended to split it as much as possible.

    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()))

Then further add dogs The logic of stream is encapsulated into sub functions.

        List<String> toms = getDogNamesStartWithTom(dogs)

4.5 the template method uses generic encapsulation

If you find that Lambda is widely used in your project, and the logic of many codes is very similar, you can consider using generic encapsulation tool classes to simplify the code. Here are two simple examples.

4.5.1 Stream object conversion

In the actual development, there are many codes that are similar to filtering before conversion:

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

In fact, this kind of writing is used to it, but it is very unreadable when a method appears many times.

After being encapsulated into a tool class, it is relatively concise:

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

Tools:

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());
    }
}

By encapsulating common template methods into tool classes, you can greatly simplify the code when using them.

4.5.2 Spring strategy pattern case

as Smart use of Spring automatic injection to realize the upgraded version of policy mode The following cases are mentioned in:

Define interface

public interface Handler {

    String getType();

    void someThing();
}

VIP user implementation:

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 User, follow the logic here");
    }
}

Common user implementation:

@Component
public class CommonHandler implements Handler{

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

    @Override
    public void someThing() {
        System.out.println("Ordinary users, follow the logic here");
    }
}

Used in Simulated 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);
        });
    }
}

The code in setApplicationContext is very similar.

You can write tool classes

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 {

// Construct the mapping of type to multiple bean s
  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;
    }

// Construct the mapping of type to a single 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;
    }
}

After transformation

@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);
    }
}

Many people may say that writing tools is also time-consuming. However, after writing the tool method, the code repetition rate decreases; The code is more concise and the readability is improved; Subsequent similar logic can realize code reuse, and the development efficiency is also improved; Kill many birds with one stone.

4.6 use of reinforcement package

As mentioned earlier, you can reduce the complexity of Lambda code by encapsulating tool classes. In addition, we can also consider using some enhancement packages to solve this problem.

4.6.1 StreamEx

as StreamEx

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

<dependency>
    <groupId>one.utilgroupId>
    <artifactId>streamexartifactId>
    <version>0.8.0version>
dependency>

Java 8 writing method

 Map<Role, List<User>> role2users = users.stream().collect(Collectors.groupingBy(User::getRole));

StreamEx:

Map<Role, List<User>> role2users = StreamEx.of(users).groupingBy(User::getRole);

Previous cases

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

You can change it to

 List<DogVO> vos = StreamEx.of(dogs).map(DogDO2VOConverter::toVo).toList();

4.6.2 vavr

vavr

User documentation: https://docs.vavr.io/

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

<dependency>
    <groupId>io.vavrgroupId>
    <artifactId>vavrartifactId>
    <version>1.0.0-alpha-4version>
dependency>

In Java 8:

// = ["1", "2", "3"] in Java 8
Arrays.asList(1, 2, 3)
      .stream()
      .map(Object::toString)
      .collect(Collectors.toList())

In vavr:

// = Stream("1", "2", "3") in Vavr
Stream.of(1, 2, 3).map(Object::toString)

4.7 add notes to each step

If chain programming is used, if there are many steps, it is recommended to add notes to each step to make it easier to understand.

4.8 some scenarios do not use Lambda expressions

If you find that you use too much lambda in a function (in actual work, you will find that more than half of a function is lambda expressions, which is a headache), you can consider changing some difficult lambda writing methods to ordinary writing methods, which will greatly improve the readability.

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()));

Optimized as

 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);
            }
        }

Although the code is longer, it is easier to understand.

This part of logic can also be encapsulated into a sub function and given a meaningful name.

  /**
     * Get nicknames and names
     */
    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;
    }

Call directly when using:

List<String> names = getNamesAndNickNames(users);

In this way, the outer function can clearly know the intention of this part of logic. There is no need to look at so much code at all, and the readability is greatly improved.

5, Think

Too much is better than too little. When we use Lambda expressions, we must not ignore readability.

There is nothing wrong with Lambda expressions. The mistake is that many people abuse Lambda expressions.

In the process of coding, we should pay attention to balance and master the degree.

This paper briefly discusses the advantages and disadvantages of Lambda expression and gives some solutions. I hope it will help you. Of course, there may be many solutions. Welcome to leave a message.

I hope you can strive to be a programmer with pursuit.

Added by georgen on Tue, 15 Feb 2022 02:46:53 +0200