After using Stream, the code becomes uglier and uglier?

Posted by visionmaster on Fri, 25 Feb 2022 13:12:14 +0100

Java8 stream stream, combined with lambda expression, can make the code shorter and more beautiful, which has been widely used. We also have more choices when writing some complex code.

The code is first shown to people and then executed by machines. Whether the code is written concisely and beautifully is of great significance for subsequent bug repair and function expansion. Many times, whether you can write excellent code has nothing to do with tools. Code is the embodiment of engineers' ability and cultivation. Some people even use stream and lambda, and the code is still written like shit.

No, let's take a look at a wonderful piece of code. Good guy, there is natural and unrestrained logic in the filter.

public List<FeedItemVo> getFeeds(Query query,Page page){
    List<String> orgiList = new ArrayList<>();
    
    List<FeedItemVo> collect = page.getRecords().stream()
    .filter(this::addDetail)
    .map(FeedItemVo::convertVo)
    .filter(vo -> this.addOrgNames(query.getIsSlow(),orgiList,vo))
    .collect(Collectors.toList());
    //... Other logic
    return collect;
}

private boolean addDetail(FeedItem feed){
    vo.setItemCardConf(service.getById(feed.getId()));
    return true;
}

private boolean addOrgNames(boolean isSlow,List<String> orgiList,FeedItemVo vo){
    if(isShow && vo.getOrgIds() != null){
        orgiList.add(vo.getOrgiName());
    }
    return true;
}

If you don't like it, let's post another paragraph.

if (!CollectionUtils.isEmpty(roleNameStrList) && roleNameStrList.contains(REGULATORY_ROLE)) {
    vos = vos.stream().filter(
           vo -> !CollectionUtils.isEmpty(vo.getSpecialTaskItemVoList())
                    && vo.getTaskName() != null)
           .collect(Collectors.toList());
} else {
    vos = vos.stream().filter(vo -> vo.getIsSelect()
           && vo.getTaskName() != null)
           .collect(Collectors.toList());
    vos = vos.stream().filter(
            vo -> !CollectionUtils.isEmpty(vo.getSpecialTaskItemVoList())
                    && vo.getTaskName() != null)
           .collect(Collectors.toList());
}
result.addAll(vos.stream().collect(Collectors.toList()));

The code can run, but it's more superfluous. What should be indented is not indented, and what should be wrapped is not wrapped. Nothing is good code.

How to improve? In addition to technical problems, it is also a problem of consciousness. Always remember that excellent code is readable first, and then fully functional.

1. Reasonable line feed

In Java, with the same function and fewer lines of code, your code is not necessarily good. Due to the use of Java; As the segmentation of code lines, if you like, you can even make the whole java file into one line, just like the confused JavaScript.

Of course, we know this is wrong. In the writing of lambda, there are some routines that can make the code more regular.

Stream.of("i", "am", "xjjdog").map(toUpperCase()).map(toBase64()).collect(joining(" "));

The above code is not recommended. In addition to easily causing obstacles in reading, it will also become difficult to find problems in the exception stack when there are problems in the code, such as throwing exceptions. Therefore, we should wrap it elegantly.

Stream.of("i", "am", "xjjdog")
    .map(toUpperCase())
    .map(toBase64())
    .collect(joining(" "));

Don't think this transformation is meaningless, or take it for granted. In my usual code review, this kind of mixed code is really countless. You can't understand the intention of the person who wrote the code.

Reasonable line feed is the formula of eternal youth of code.

2. Rounding split function

Why can functions be written longer and longer? Is it because of the high level of technology that can control this change? The answer is lazy! Due to the problem of development duration or consciousness, if you encounter new requirements, you can directly add ifelse to the old code. Even if you encounter similar functions, you can directly choose to copy the original code. Over time, the code will not change.

First, let's talk about performance. In the JVM, the JIT compiler will inline the code with large number of calls and simple logic, so as to reduce the overhead of stack frame and carry out more optimization. Therefore, short and concise functions are actually JVM friendly.

In terms of readability, it is very necessary to split a large piece of code into meaningful functions, which is also the essence of refactoring. In lambda expressions, this splitting is more necessary.

I'll take an example of entity transformation that often appears in code to illustrate. The following transformation creates an anonymous function order - > {}, which is very weak in semantic expression.

public Stream<OrderDto> getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(order-> {
            OrderDto dto = new OrderDto();
            dto.setOrderId(order.getOrderId());
            dto.setTitle(order.getTitle().split("#")[0]);
            dto.setCreateDate(order.getCreateDate().getTime());
            return dto;
    });
}

In actual business code, such assignment copy and transformation logic are usually very long. We can try to separate the creation process of dto. Because the transformation action is not the main business logic, we usually don't care what happened in it.

public Stream<OrderDto> getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(this::toOrderDto);
}
public OrderDto toOrderDto(Order order){
    OrderDto dto = new OrderDto();
            dto.setOrderId(order.getOrderId());
            dto.setTitle(order.getTitle().split("#")[0]);
            dto.setCreateDate(order.getCreateDate().getTime());
    return dto;
}

Such conversion code is still a little ugly. But if the parameter of OrderDto's constructor is Order, public OrderDto(Order order), then we can remove the real transformation logic from the main logic, and the whole code can be very refreshing.

public Stream<OrderDto> getOrderByUser(String userId){
    return orderRepo.findOrderByUser().stream()
        .map(OrderDto::new);
}

In addition to the semantic functions of map and flatMap, more filter s can be replaced by predict. For example:

Predicate<Registar> registarIsCorrect = reg -> 
    reg.getRegulationId() != null 
    && reg.getRegulationId() != 0 
    && reg.getType() == 0;

registarIsCorrect can be used as the parameter of filter.

3. Rational use of Optional

In Java code, since NullPointerException is not a mandatory exception, it will be hidden in the code and cause many unexpected bug s. Therefore, when we get a parameter, we will verify its legitimacy to see if it is null. The code is full of such code everywhere.

if(null == obj)
if(null == user.getName() || "".equals(user.getName()))
    
if (order != null) {
    Logistics logistics = order.getLogistics();
    if(logistics != null){
        Address address = logistics.getAddress();
        if (address != null) {
            Country country = address.getCountry();
            if (country != null) {
                Isocode isocode = country.getIsocode();
                if (isocode != null) {
                    return isocode.getNumber();
                }
            }
        }
    }
}

Java 8 introduces the Optional class to solve the notorious null pointer problem. In fact, it is a package class, which provides several methods to judge its own null value problem.

The more complex code example above can be replaced by the following code.

 String result = Optional.ofNullable(order)
      .flatMap(order->order.getLogistics())
      .flatMap(logistics -> logistics.getAddress())
      .flatMap(address -> address.getCountry())
      .map(country -> country.getIsocode())
      .orElse(Isocode.CHINA.getNumber());

When you are not sure whether the thing you provide is empty, a good habit is not to return null, otherwise the caller's code will be full of null judgment. We should nip null in the bud.

public Optional<String> getUserName() {
    return Optional.ofNullable(userName);
}

In addition, we should minimize the use of the Optional get method, which will also make the code ugly. For example:

Optional<String> userName = "xjjdog";
String defaultEmail = userName.get() == null ? "":userName.get() + "@xjjdog.cn";

Instead, it should be modified in this way:

Optional<String> userName = "xjjdog";
String defaultEmail = userName
    .map(e -> e + "@xjjdog.cn")
    .orElse("");

So why is our code still full of all kinds of null value judgment? Even in very popular and professional code? A very important reason is that the use of Optional needs to be consistent. When there is a fault in one of the links, most coders will write some code in an imitation way in order to keep consistent with the style of the original code.

If you want to popularize the use of Optional in the project, scaffold designers or review ers need to work harder.

4. Return Stream or List?

Many people fall into a dilemma when designing interfaces. Do I return the data directly to Stream or List?

If you return a List, such as ArrayList, modifying the List will directly affect the values in it, unless you wrap it in an immutable way. Similarly, arrays have such problems.

But for a Stream, it is immutable, and it will not affect the original collection. For this scenario, we recommend returning the Stream directly instead of the collection. Another advantage of this method is that it can strongly suggest API users to use Stream related functions more, so as to unify the code style.

public Stream<User> getAuthUsers(){
    ...
    return Stream.of(users);
}

Immutable sets are a strong requirement, which can prevent external functions from making unpredictable changes to these sets. In guava, a large number of immutable classes support this package. Another example is the enumeration of Java. Its values() method can only copy one copy of data in order to prevent the enumeration from being modified by the external api.

However, if your api is for the end user and does not need to be modified, it is better to directly return to the List. For example, the function is in the Controller.


5. Use less or no parallel streams

There are many problems with Java's parallel flow. These problems are frequently trampled on by people unfamiliar with concurrent programming. It's not that parallel streaming is bad, but if you find that your team always stumbles on it, you will not hesitate to reduce the frequency of recommendation.

Thread safety is an old-fashioned problem of parallel flow. In the process of iteration, if thread unsafe classes are used, problems are easy to occur. For example, the following code runs incorrectly in most cases.

List transform(List source){
 List dst = new ArrayList<>();
 if(CollectionUtils.isEmpty()){
  return dst;
 }
 source.stream.
  .parallel()
  .map(..)
  .filter(..)
  .foreach(dst::add);
 return dst;
}

You might say, I'll just change foreach to collect. But note that many developers do not have such awareness. Since the api provides such a function and it makes logical sense, you can't stop others from using it.

Another abuse of parallel flow is that it takes a very long time to perform IO tasks in iterations. Before using parallel streams, do you have a question? Since it is parallel, how is its thread pool configured?

Unfortunately, all parallel streams share a ForkJoinPool. Its size is - 1 by default. In most cases, it is not enough.

If someone runs a time-consuming IO service on a parallel stream, you need to queue even if you perform a simple mathematical operation. The point is, you can't stop other students in the project from using parallel streams, and you can't know what he did.

What can we do? My approach is one size fits all and direct prohibition. Although it is cruel, it avoids the problem.

summary

The Stream function added by Java 8 is very good. We don't need to envy other languages any more and write code more freely. Although it looks very powerful, it's just a grammar candy. Don't expect to use it to get super power.

With the popularity of Stream, there are more and more such codes in our code. But now a lot of code. After using Stream and Lambda, the code gets worse and worse, smelly and long, so that it can't be read. No other reason, abuse!

Generally speaking, when using Stream and Lambda, we should ensure that the main process is simple and clear, the style should be unified, the line feed should be reasonable, we should be willing to add functions, use Optional and other features correctly, and do not add code logic to functions such as filter. When writing code, we should consciously follow these small tips. Simplicity and elegance is productivity.

If we think the features provided by Java are not enough, we also have an open source class library vavr, which provides more possibilities to combine with Stream and Lambda to enhance the experience of functional programming.

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.3</version>
</dependency>

However, no matter how powerful APIs and programming methods are provided, they can't withstand the abuse of small partners. These codes are completely logical, but they just look awkward and hard to maintain.

Writing a pile of garbage lambda code is the best way to abuse colleagues and the only choice to bury a hole.

Writing code is like talking and chatting. Everyone is doing the same work. Some people talk well and have high looks. Everyone likes to chat with him; Some people don't talk well and poke where they hurt. Although they exist, everyone hates them.

Code, besides the meaning of work, is just another way for us to express our ideas in the world. How to write good code is not only a technical problem, but also a problem of consciousness.


 

Topics: Back-end