The way to simplify Java code

Preface

There is a cloud in the old saying:

Tao is the spirit of art, and art is the body of Tao; Tao is the rule of art, and art leads to Tao.

Among them, "Tao" refers to "law, truth and theory", and "technique" refers to "method, skill and technology". It means that "Tao" is the soul of "Shu" and "Shu" is the body of "Tao". You can use "Tao" to govern "Shu" or get "Tao" from "Shu".

Reading the article of the big guy "Gu Du"< Code Review is a bitter but interesting practice >At that time, the deepest feeling is: "good code must be the principle of less is more elite", which is the "way" of big guy's code simplification.

Craftsman's pursuit of "skill" to the extreme is actually seeking for "Tao", and it is not far away from the realization of "Tao", or has already got the way, which is "craftsman spirit" - a spirit of pursuing "skill to get the way". If a craftsman is only satisfied with "skill" and cannot pursue "skill" to the extreme to realize "Tao", it is just a craftsman who relies on "skill" to support his family. Based on years of practice and exploration, the author summarizes a large number of "techniques" of Java code reduction, trying to elaborate the "way" of Java code reduction in mind.

1. Using grammar

1.1. Using ternary expression

Normal:

String title;
if (isMember(phone)) {
    title = "Member";
} else {
    title = "Tourist";
}

Streamlining:

String title = isMember(phone) ? "Member" : "Tourist";

Note: for arithmetic calculation of package type, it is necessary to avoid the problem of null pointer when unpacking.

1.2. Using for each statement

From Java 5, for each loop is provided to simplify the loop traversal of arrays and collections. The for-each loop allows you to traverse the array without having to maintain the index in the traditional for loop, or you can traverse the collection without calling hasNext and next methods in the while loop when using iterators.

Normal:

double[] values = ...;
for(int i = 0; i < values.length; i++) {
    double value = values[i];
    // TODO: process value
}

List<Double> valueList = ...;
Iterator<Double> iterator = valueList.iterator();
while (iterator.hasNext()) {
    Double value = iterator.next();
    // TODO: process value
}

Streamlining:

double[] values = ...;
for(double value : values) {
    // TODO: process value
}

List<Double> valueList = ...;
for(Double value : valueList) {
    // TODO: process value
}

1.3. Using try with resource statement

All "resources" that implement the Closeable interface can be simplified by using try with resource.

Normal:

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("cities.csv"));
    String line;
    while ((line = reader.readLine()) != null) {
        // TODO: processing line
    }
} catch (IOException e) {
    log.error("Reading file exception", e);
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            log.error("Close file exception", e);
        }
    }
}

Streamlining:

try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // TODO: processing line
    }
} catch (IOException e) {
    log.error("Reading file exception", e);
}

1.4. Use return keyword

Using the return keyword, you can return functions in advance to avoid defining intermediate variables.

Normal:

public static boolean hasSuper(@NonNull List<UserDO> userList) {
    boolean hasSuper = false;
    for (UserDO user : userList) {
        if (Boolean.TRUE.equals(user.getIsSuper())) {
            hasSuper = true;
            break;
        }
    }
    return hasSuper;
}

Streamlining:

public static boolean hasSuper(@NonNull List<UserDO> userList) {
    for (UserDO user : userList) {
        if (Boolean.TRUE.equals(user.getIsSuper())) {
            return true;
        }
    }
    return false;
}

1.5. Using static keyword

With the static keyword, you can change a field into a static field, or a function into a static function. When you call it, you do not need to initialize the class object.

Normal:

public final class GisHelper {
    public double distance(double lng1, double lat1, double lng2, double lat2) {
        // Method implementation code
    }
}

GisHelper gisHelper = new GisHelper();
double distance = gisHelper.distance(116.178692D, 39.967115D, 116.410778D, 39.899721D);

Streamlining:

public final class GisHelper {
    public static double distance(double lng1, double lat1, double lng2, double lat2) {
        // Method implementation code
    }
}

double distance = GisHelper.distance(116.178692D, 39.967115D, 116.410778D, 39.899721D);

1.6. Using lambda expression

After Java 8 was released, lambda expression replaced the use of anonymous inner class in a large number, which not only simplified the code, but also highlighted the really useful part of the original anonymous inner class.

Normal:

new Thread(new Runnable() {
    public void run() {
        // Thread processing code
    }
}).start();

Streamlining:

new Thread(() -> {
    // Thread processing code
}).start();

1.7. Use method reference

Method reference (::), which can simplify lambda expression and omit variable declaration and function call.

Normal:

Arrays.sort(nameArray, (a, b) -> a.compareToIgnoreCase(b));
List<Long> userIdList = userList.stream()
    .map(user -> user.getId())
    .collect(Collectors.toList());

Streamlining:

Arrays.sort(nameArray, String::compareToIgnoreCase);
List<Long> userIdList = userList.stream()
    .map(UserDO::getId)
    .collect(Collectors.toList());

1.8. Using static import

Static import can simplify the reference of static constants and functions when the same static constants and functions are widely used in programs.

Normal:

List<Double> areaList = radiusList.stream().map(r -> Math.PI * Math.pow(r, 2)).collect(Collectors.toList());
...

Streamlining:

import static java.lang.Math.PI;
import static java.lang.Math.pow;
import static java.util.stream.Collectors.toList;

List<Double> areaList = radiusList.stream().map(r -> PI * pow(r, 2)).collect(toList());
...

Note: static introduction is easy to cause code reading difficulties, so it should be used with caution in actual projects.

1.9. Use unchecked exception

Java exceptions are divided into two types: Checked and unchecked. Unchecked exceptions inherit the RuntimeException, which is characterized by that the code can compile without handling them, so they are called unchecked exceptions. With unchecked exception, unnecessary try catch and throws exception handling can be avoided.

Normal:

@Service
public class UserService {
    public void createUser(UserCreateVO create, OpUserVO user) throws BusinessException {
        checkOperatorUser(user);
        ...
    }
    private void checkOperatorUser(OpUserVO user) throws BusinessException {
        if (!hasPermission(user)) {
            throw new BusinessException("User has no operation permission");
        }
        ...
    }
    ...
}

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/createUser")
    public Result<Void> createUser(@RequestBody @Valid UserCreateVO create, OpUserVO user) throws BusinessException {
        userService.createUser(create, user);
        return Result.success();
    }
    ...
}

Streamlining:

@Service
public class UserService {
    public void createUser(UserCreateVO create, OpUserVO user) {
        checkOperatorUser(user);
        ...
    }
    private void checkOperatorUser(OpUserVO user) {
        if (!hasPermission(user)) {
            throw new BusinessRuntimeException("User has no operation permission");
        }
        ...
    }
    ...
}

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/createUser")
    public Result<Void> createUser(@RequestBody @Valid UserCreateVO create, OpUserVO user) {
        userService.createUser(create, user);
        return Result.success();
    }
    ...
}

2. Use notes

2.1. Using Lombok annotation

Lombok provides a useful set of annotations that can be used to eliminate a lot of boilerplate code in Java classes.

Normal:

public class UserVO {
    private Long id;
    private String name;
    public Long getId() {
        return this.id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
    ...
}

Streamlining:

@Getter
@Setter
@ToString
public class UserVO {
    private Long id;
    private String name;
    ...
}

2.2. Using Validation notes

Normal:

@Getter
@Setter
@ToString
public class UserCreateVO {
    private String name;
    private Long companyId;
}

@Service
public class UserService {
    public Long createUser(UserCreateVO create) {
        // Validation parameters
        if (StringUtils.isBlank(create.getName())) {
            throw new IllegalArgumentException("User name cannot be empty");
        }
        if (Objects.isNull(create.getCompanyId())) {
            throw new IllegalArgumentException("Company ID cannot be empty");
        }

        // TODO: create user
        return null;
    }
}

Streamlining:

@Getter
@Setter
@ToString
public class UserCreateVO {
    @NotBlank(message = "User name cannot be empty")
    private String name;
    @NotNull(message = "Company ID cannot be empty")
    private Long companyId;
    ...
}

@Service
@Validated
public class UserService {
    public Long createUser(@Valid UserCreateVO create) {
        // TODO: create user
        return null;
    }
}

2.3. Use @ NonNull annotation

Spring's @ NonNull annotation, which is used to annotate parameters or return values that are not empty, is suitable for team collaboration within the project. As long as the implementer and the caller follow the specification, unnecessary null value judgment can be avoided, which fully reflects the "simple because of trust" advocated by Ali's "new six pulse sword".

Normal:

public List<UserVO> queryCompanyUser(Long companyId) {
    // Check company identification
    if (companyId == null) {
        return null;
    }

    // Query return user
    List<UserDO> userList = userDAO.queryByCompanyId(companyId);
    return userList.stream().map(this::transUser).collect(Collectors.toList());
}

Long companyId = 1L;
List<UserVO> userList = queryCompanyUser(companyId);
if (CollectionUtils.isNotEmpty(userList)) {
    for (UserVO user : userList) {
        // TODO: processing company users
    }
}

Streamlining:

public @NonNull List<UserVO> queryCompanyUser(@NonNull Long companyId) {
    List<UserDO> userList = userDAO.queryByCompanyId(companyId);
    return userList.stream().map(this::transUser).collect(Collectors.toList());
}

Long companyId = 1L;
List<UserVO> userList = queryCompanyUser(companyId);
for (UserVO user : userList) {
    // TODO: process company users
}

2.4. Using annotation features

Annotations have the following features that can be used to refine annotation declarations:

  1. When the annotation attribute value is consistent with the default value, the attribute assignment can be deleted;
  2. When the annotation has only value attribute, value can be removed for shorthand;
  3. When the annotation attribute combination is equal to another specific annotation, the specific annotation is adopted directly.

Normal:

@Lazy(true);
@Service(value = "userService")
@RequestMapping(path = "/getUser", method = RequestMethod.GET)

Streamlining:

@Lazy
@Service("userService")
@GetMapping("/getUser")

3. Using generics

3.1. Generic interface

Before the introduction of generics in Java, objects were used to represent general objects. The biggest problem was that types could not be strongly verified and forced type conversion was needed.

Normal:

public interface Comparable {
    public int compareTo(Object other);
}

@Getter
@Setter
@ToString
public class UserVO implements Comparable {
    private Long id;

    @Override
    public int compareTo(Object other) {
        UserVO user = (UserVO)other;
        return Long.compare(this.id, user.id);
    }
}

Streamlining:

public interface Comparable<T> {
    public int compareTo(T other);
}

@Getter
@Setter
@ToString
public class UserVO implements Comparable<UserVO> {
    private Long id;

    @Override
    public int compareTo(UserVO other) {
        return Long.compare(this.id, other.id);
    }
}

3.2. Generic classes

Normal:

@Getter
@Setter
@ToString
public class IntPoint {
    private Integer x;
    private Integer y;
}

@Getter
@Setter
@ToString
public class DoublePoint {
    private Double x;
    private Double y;
}

Streamlining:

@Getter
@Setter
@ToString
public class Point<T extends Number> {
    private T x;
    private T y;
}

3.3. Generic methods

Normal:

public static Map<String, Integer> newHashMap(String[] keys, Integer[] values) {
    // Check parameter is not empty
    if (ArrayUtils.isEmpty(keys) || ArrayUtils.isEmpty(values)) {
        return Collections.emptyMap();
    }

    // Convert hash map
    Map<String, Integer> map = new HashMap<>();
    int length = Math.min(keys.length, values.length);
    for (int i = 0; i < length; i++) {
        map.put(keys[i], values[i]);
    }
    return map;
}
...

Streamlining:

public static <K, V> Map<K, V> newHashMap(K[] keys, V[] values) {
    // Check parameter is not empty
    if (ArrayUtils.isEmpty(keys) || ArrayUtils.isEmpty(values)) {
        return Collections.emptyMap();
    }

    // Convert hash map
    Map<K, V> map = new HashMap<>();
    int length = Math.min(keys.length, values.length);
    for (int i = 0; i < length; i++) {
        map.put(keys[i], values[i]);
    }
    return map;
}

4. Use your own methods

4.1. Construction method

The construction method can simplify the initialization and property setting of objects. For classes with fewer property fields, you can customize the construction method.

Normal:

@Getter
@Setter
@ToString
public class PageDataVO<T> {
    private Long totalCount;
    private List<T> dataList;
}

PageDataVO<UserVO> pageData = new PageDataVO<>();
pageData.setTotalCount(totalCount);
pageData.setDataList(userList);
return pageData;

Streamlining:

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PageDataVO<T> {
    private Long totalCount;
    private List<T> dataList;
}

return new PageDataVO<>(totalCount, userList);

Note: if the property field is replaced, there is a constructor initialization assignment problem. For example, to replace the attribute field title with nickname, because the number and type of parameters of the constructor remain unchanged, the original constructor initialization statement will not report an error, resulting in the assignment of the original title value to nickname. If the Setter method is used to assign values, the compiler will prompt for errors and ask for repair.

4.2. add method with Set

By using the return value of the add method of Set, you can directly know whether the value already exists, and avoid calling the contains method to judge whether it exists.

Normal:

The following case is for the user to do the re transformation operation. First, we need to call the contains method to determine the existence and then call the add method to add it.

Set<Long> userIdSet = new HashSet<>();
List<UserVO> userVOList = new ArrayList<>();
for (UserDO userDO : userDOList) {
    if (!userIdSet.contains(userDO.getId())) {
        userIdSet.add(userDO.getId());
        userVOList.add(transUser(userDO));
    }
}

Streamlining:

Set<Long> userIdSet = new HashSet<>();
List<UserVO> userVOList = new ArrayList<>();
for (UserDO userDO : userDOList) {
    if (userIdSet.add(userDO.getId())) {
        userVOList.add(transUser(userDO));
    }
}

4.3. computeIfAbsent method using Map

By using the computeIfAbsent method of Map, we can ensure that the acquired object is not empty, thus avoiding unnecessary empty judgment and resetting values.

Normal:

Map<Long, List<UserDO>> roleUserMap = new HashMap<>();
for (UserDO userDO : userDOList) {
    Long roleId = userDO.getRoleId();
    List<UserDO> userList = roleUserMap.get(roleId);
    if (Objects.isNull(userList)) {
        userList = new ArrayList<>();
        roleUserMap.put(roleId, userList);
    }
    userList.add(userDO);
}

Streamlining:

Map<Long, List<UserDO>> roleUserMap = new HashMap<>();
for (UserDO userDO : userDOList) {
    roleUserMap.computeIfAbsent(userDO.getRoleId(), key -> new ArrayList<>())
        .add(userDO);
}

4.4. Using chain programming

Chain programming, also known as cascade programming, returns a this object to the object itself when calling the function of the object, achieves the chain effect, and can be called in cascade. The advantages of chain programming are: strong programmability, readability and simple code.

Normal:

StringBuilder builder = new StringBuilder(96);
builder.append("select id, name from ");
builder.append(T_USER);
builder.append(" where id = ");
builder.append(userId);
builder.append(";");

Streamlining:

StringBuilder builder = new StringBuilder(96);
builder.append("select id, name from ")
    .append(T_USER)
    .append(" where id = ")
    .append(userId)
    .append(";");

5. Tools and methods

5.1. Avoid null value judgment

Normal:

if (userList != null && !userList.isEmpty()) {
    // TODO: processing code
}

Streamlining:

if (CollectionUtils.isNotEmpty(userList)) {
    // TODO: processing code
}

5.2. Judgment of avoidance conditions

Normal:

double result;
if (value <= MIN_LIMIT) {
    result = MIN_LIMIT;
} else {
    result = value;
}

Streamlining:

double result = Math.max(MIN_LIMIT, value);

5.3. Simplified assignment statement

Normal:

public static final List<String> ANIMAL_LIST;
static {
    List<String> animalList = new ArrayList<>();
    animalList.add("dog");
    animalList.add("cat");
    animalList.add("tiger");
    ANIMAL_LIST = Collections.unmodifiableList(animalList);
}

Streamlining:

// JDK school
public static final List<String> ANIMAL_LIST = Arrays.asList("dog", "cat", "tiger");
// Guava school
public static final List<String> ANIMAL_LIST = ImmutableList.of("dog", "cat", "tiger");

Note: the List returned by Arrays.asList is not an ArrayList and does not support add and other change operations.

5.4. Simplify data copy

Normal:

UserVO userVO = new UserVO();
userVO.setId(userDO.getId());
userVO.setName(userDO.getName());
...
userVO.setDescription(userDO.getDescription());
userVOList.add(userVO);

Streamlining:

UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
userVOList.add(userVO);

Counter example:

List<UserVO> userVOList = JSON.parseArray(JSON.toJSONString(userDOList), UserVO.class);

Reduce code, but not at the expense of excessive performance loss. The example is shallow copy, which doesn't need a heavy weapon like JSON.

5.5. Simplify exception assertion

Normal:

if (Objects.isNull(userId)) {
    throw new IllegalArgumentException("User ID cannot be empty");
}

Streamlining:

Assert.notNull(userId, "User ID cannot be empty");

Note: some plug-ins may not agree with this judgment, resulting in a null pointer warning when using this object.

5.6. Simplify test cases

The test case data is stored in the file in JSON format and parsed into objects through the parseObject and parseArray methods of JSON. Although the execution efficiency is reduced, a large number of assignment statements can be reduced, thus simplifying the test code.

Normal:

@Test
public void testCreateUser() {
    UserCreateVO userCreate = new UserCreateVO();
    userCreate.setName("Changyi");
    userCreate.setTitle("Developer");
    userCreate.setCompany("AMAP");
    ...
    Long userId  = userService.createUser(OPERATOR, userCreate);
    Assert.assertNotNull(userId, "Failed to create user");
}

Streamlining:

@Test
public void testCreateUser() {
    String jsonText = ResourceHelper.getResourceAsString(getClass(), "createUser.json");
    UserCreateVO userCreate = JSON.parseObject(jsonText, UserCreateVO.class);
    Long userId  = userService.createUser(OPERATOR, userCreate);
    Assert.assertNotNull(userId, "Failed to create user");
}

Suggestion: the JSON file name is best named after the method being tested. If there are multiple versions, they can be represented by a numeric suffix.

5.7. Implementation of simplified algorithm

Some conventional algorithms, existing tools and methods, we do not need to implement their own.

Normal:

int totalSize = valueList.size();
List<List<Integer>> partitionList = new ArrayList<>();
for (int i = 0; i < totalSize; i += PARTITION_SIZE) {
    partitionList.add(valueList.subList(i, Math.min(i + PARTITION_SIZE, totalSize)));
}

Streamlining:

List<List<Integer>> partitionList = ListUtils.partition(valueList, PARTITION_SIZE);

5.8. Packaging tool method

Some special algorithms have no ready-made tools and methods, so we have to implement them ourselves.

Normal:

For example, the method of setting parameter value in SQL is difficult to use, and the setLong method cannot set the parameter value to null.

/** Set parameter value */
if (Objects.nonNull(user.getId())) {
    statement.setLong(1, user.getId());
} else {
    statement.setNull(1, Types.BIGINT);
}
...

Streamlining:

We can encapsulate SqlHelper as a tool class to simplify the code of setting parameter values.

/** SQL Auxiliary class */
public final class SqlHelper {
    /** Set long integer value */
    public static void setLong(PreparedStatement statement, int index, Long value) throws SQLException {
        if (Objects.nonNull(value)) {
            statement.setLong(index, value.longValue());
        } else {
            statement.setNull(index, Types.BIGINT);
        }
    }
    ...
}

/** Set parameter value */
SqlHelper.setLong(statement, 1, user.getId());

6. Use data structure

6.1. Simplify with array

For if else statements with fixed upper and lower bounds, we can simplify them with array + loop.

Normal:

public static int getGrade(double score) {
    if (score >= 90.0D) {
        return 1;
    }
    if (score >= 80.0D) {
        return 2;
    }
    if (score >= 60.0D) {
        return 3;
    }
    if (score >= 30.0D) {
        return 4;
    }
    return 5;
}

Streamlining:

private static final double[] SCORE_RANGES = new double[] {90.0D, 80.0D, 60.0D, 30.0D};
public static int getGrade(double score) {
    for (int i = 0; i < SCORE_RANGES.length; i++) {
        if (score >= SCORE_RANGES[i]) {
            return i + 1;
        }
    }
    return SCORE_RANGES.length + 1;
}

Thinking: the return value of the above case is incremental, so there is no problem to simplify it with array. However, if the return value is not incremental, can we simplify it with arrays? The answer is yes, please think about it.

6.2. Simplify with Map

Map can be used to simplify the if else statement of mapping relationship. In addition, this rule also applies to switch statements that simplify mapping relationships.

Normal:

public static String getBiologyClass(String name) {
    switch (name) {
        case "dog" :
            return "animal";
        case "cat" :
            return "animal";
        case "lavender" :
            return "plant";
        ...
        default :
            return null;
    }
}

Streamlining:

private static final Map<String, String> BIOLOGY_CLASS_MAP
    = ImmutableMap.<String, String>builder()
        .put("dog", "animal")
        .put("cat", "animal")
        .put("lavender", "plant")
        ...
        .build();
public static String getBiologyClass(String name) {
    return BIOLOGY_CLASS_MAP.get(name);
}

The method has been simplified into one line of code, but there is no need to encapsulate the method.

6.3. Simplify with container

Unlike Python and Go, Java does not support methods that return multiple objects. If you need to return more than one object, you must customize the class or use the container class. Common container classes include Apache pair class and Triple class. Pair class supports two objects and Triple class supports three objects.

Normal:

@Setter
@Getter
@ToString
@AllArgsConstructor
public static class PointAndDistance {
    private Point point;
    private Double distance;
}

public static PointAndDistance getNearest(Point point, Point[] points) {
    // Calculate closest point and distance
    ...

    // Return to nearest point and distance
    return new PointAndDistance(nearestPoint, nearestDistance);
}

Streamlining:

public static Pair<Point, Double> getNearest(Point point, Point[] points) {
    // Calculate closest point and distance
    ...

    // Return to nearest point and distance
    return ImmutablePair.of(nearestPoint, nearestDistance);
}

6.4. Simplify with ThreadLocal

ThreadLocal provides thread specific objects, which can be accessed at any time in the entire thread life cycle, greatly facilitating the implementation of some logic. Saving thread context object with ThreadLocal can avoid unnecessary parameter passing.

Normal:

Due to the unsafe thread of the format method of DateFormat (alternative method is recommended), the performance of frequently initializing DateFormat in the thread is too low. If reuse is considered, only parameters can be passed in to DateFormat. Examples are as follows:

public static String formatDate(Date date, DateFormat format) {
    return format.format(date);
}

public static List<String> getDateList(Date minDate, Date maxDate, DateFormat format) {
    List<String> dateList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(minDate);
    String currDate = formatDate(calendar.getTime(), format);
    String maxsDate = formatDate(maxDate, format);
    while (currDate.compareTo(maxsDate) <= 0) {
        dateList.add(currDate);
        calendar.add(Calendar.DATE, 1);
        currDate = formatDate(calendar.getTime(), format);
    }
    return dateList;
}

Streamlining:

You may think that the following code amount is more. If there are more places to call tool methods, you can save a lot of DateFormat initialization and passed in parameter code.

private static final ThreadLocal<DateFormat> LOCAL_DATE_FORMAT = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
};

public static String formatDate(Date date) {
    return LOCAL_DATE_FORMAT.get().format(date);
}

public static List<String> getDateList(Date minDate, Date maxDate) {
    List<String> dateList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(minDate);
    String currDate = formatDate(calendar.getTime());
    String maxsDate = formatDate(maxDate);
    while (currDate.compareTo(maxsDate) <= 0) {
        dateList.add(currDate);
        calendar.add(Calendar.DATE, 1);
        currDate = formatDate(calendar.getTime());
    }
    return dateList;
}

Note: ThreadLocal has a certain risk of memory leaking, and remove method is used to remove data before the end of business code.

7. Using Optional

In Java 8, an Optional class is introduced, which is a null able container object.

7.1. Guarantee value exists

Normal:

Integer thisValue;
if (Objects.nonNull(value)) {
    thisValue = value;
} else {
    thisValue = DEFAULT_VALUE;
}

Streamlining:

Integer thisValue = Optional.ofNullable(value).orElse(DEFAULT_VALUE);

7.2. Legal guarantee value

Normal:

Integer thisValue;
if (Objects.nonNull(value) && value.compareTo(MAX_VALUE) <= 0) {
    thisValue = value;
} else {
    thisValue = MAX_VALUE;
}

Streamlining:

Integer thisValue = Optional.ofNullable(value)
    .filter(tempValue -> tempValue.compareTo(MAX_VALUE) <= 0).orElse(MAX_VALUE);

7.3. Avoid empty judgment

Normal:

String zipcode = null;
if (Objects.nonNull(user)) {
    Address address = user.getAddress();
    if (Objects.nonNull(address)) {
        Country country = address.getCountry();
        if (Objects.nonNull(country)) {
            zipcode = country.getZipcode();
        }
    }
}

Streamlining:

String zipcode = Optional.ofNullable(user).map(User::getAddress)
    .map(Address::getCountry).map(Country::getZipcode).orElse(null);

8. Using Stream

Stream is a new member of Java 8, which allows you to explicitly process data sets. It can be seen as a high-level iterator that traverses data sets. The flow consists of three parts: obtaining a data source, data transformation, and performing operations to obtain the desired results. Each time the original stream object is transformed, a new stream object is returned, which allows its operation to be arranged like a chain, forming a pipeline. The functions provided by stream are very useful, mainly including matching, filtering, summarizing, transforming, grouping, grouping and summarizing.

8.1. Matching set data

Normal:

boolean isFound = false;
for (UserDO user : userList) {
    if (Objects.equals(user.getId(), userId)) {
        isFound = true;
        break;
    }
}

Streamlining:

boolean isFound = userList.stream()
    .anyMatch(user -> Objects.equals(user.getId(), userId));

8.2. Filtering set data

Normal:

List<UserDO> resultList = new ArrayList<>();
for (UserDO user : userList) {
    if (Boolean.TRUE.equals(user.getIsSuper())) {
        resultList.add(user);
    }
}

Streamlining:

List<UserDO> resultList = userList.stream()
    .filter(user -> Boolean.TRUE.equals(user.getIsSuper()))
    .collect(Collectors.toList());

8.3. Aggregate data

Normal:

double total = 0.0D;
for (Account account : accountList) {
    total += account.getBalance();
}

Streamlining:

double total = accountList.stream().mapToDouble(Account::getBalance).sum();

8.4. Transform set data

Normal:

List<UserVO> userVOList = new ArrayList<>();
for (UserDO userDO : userDOList) {
    userVOList.add(transUser(userDO));
}

Streamlining:

List<UserVO> userVOList = userDOList.stream()
    .map(this::transUser).collect(Collectors.toList());

8.5. Group set data

Normal:

Map<Long, List<UserDO>> roleUserMap = new HashMap<>();
for (UserDO userDO : userDOList) {
    roleUserMap.computeIfAbsent(userDO.getRoleId(), key -> new ArrayList<>())
        .add(userDO);
}

Streamlining:

Map<Long, List<UserDO>> roleUserMap = userDOList.stream()
    .collect(Collectors.groupingBy(UserDO::getRoleId));

8.6. Group summary Set

Normal:

Map<Long, Double> roleTotalMap = new HashMap<>();
for (Account account : accountList) {
    Long roleId = account.getRoleId();
    Double total = Optional.ofNullable(roleTotalMap.get(roleId)).orElse(0.0D);
    roleTotalMap.put(roleId, total + account.getBalance());
}

Streamlining:

roleTotalMap = accountList.stream().collect(Collectors.groupingBy(Account::getRoleId, Collectors.summingDouble(Account::getBalance)));

8.7. Generate range set

Python's range is very convenient, and Stream provides a similar approach.

Normal:

int[] array1 = new int[N];
for (int i = 0; i < N; i++) {
    array1[i] = i + 1;
}

int[] array2 = new int[N];
array2[0] = 1;
for (int i = 1; i < N; i++) {
    array2[i] = array2[i - 1] * 2;
}

Streamlining:

int[] array1 = IntStream.rangeClosed(1, N).toArray();
int[] array2 = IntStream.iterate(1, n -> n * 2).limit(N).toArray();

9. Using program structure

9.1. Return condition expression

The conditional expression judgment returns a Boolean value, and the conditional expression itself is the result.

Normal:

public boolean isSuper(Long userId)
    UserDO user = userDAO.get(userId);
    if (Objects.nonNull(user) && Boolean.TRUE.equals(user.getIsSuper())) {
        return true;
    }
    return false;
}

Streamlining:

public boolean isSuper(Long userId)
    UserDO user = userDAO.get(userId);
    return Objects.nonNull(user) && Boolean.TRUE.equals(user.getIsSuper());
}

9.2. Minimize conditional scope

Minimize the scope of the condition and propose common processing code as much as possible.

Normal:

Result result = summaryService.reportWorkDaily(workDaily);
if (result.isSuccess()) {
    String message = "Report work daily successfully";
    dingtalkService.sendMessage(user.getPhone(), message);
} else {
    String message = "Failed to report work daily report:" + result.getMessage();
    log.warn(message);
    dingtalkService.sendMessage(user.getPhone(), message);
}

Streamlining:

String message;
Result result = summaryService.reportWorkDaily(workDaily);
if (result.isSuccess()) {
    message = "Report work daily successfully";
} else {
    message = "Failed to report work daily report:" + result.getMessage();
    log.warn(message);
}
dingtalkService.sendMessage(user.getPhone(), message);

9.3. Adjust expression position

Adjust the expression position to make the code more concise without changing the logic.

Normal 1:

String line = readLine();
while (Objects.nonNull(line)) {
    ... // Processing logic code
    line = readLine();
}

Normal 2:

for (String line = readLine(); Objects.nonNull(line); line = readLine()) {
    ... // Processing logic code
}

Streamlining:

String line;
while (Objects.nonNull(line = readLine())) {
    ... // Processing logic code
}

Note: some specifications may not recommend this streamlined approach.

9.4. Using non empty objects

When comparing objects, we can avoid null pointer judgment by exchanging object positions and using non null objects.

Normal:

private static final int MAX_VALUE = 1000;
boolean isMax = (value != null && value.equals(MAX_VALUE));
boolean isTrue = (result != null && result.equals(Boolean.TRUE));

Streamlining:

private static final Integer MAX_VALUE = 1000;
boolean isMax = MAX_VALUE.equals(value);
boolean isTrue = Boolean.TRUE.equals(result);

10. Using design patterns

10.1. Template method mode

Template Method Pattern defines a fixed algorithm framework, and some steps of the algorithm are implemented in the subclass, so that the subclass can redefine some steps of the algorithm without changing the algorithm framework.

Normal:

@Repository
public class UserValue {
    /** Value operation */
    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> valueOperations;
    /** Value mode */
    private static final String KEY_FORMAT = "Value:User:%s";

    /** Set value */
    public void set(Long id, UserDO value) {
        String key = String.format(KEY_FORMAT, id);
        valueOperations.set(key, JSON.toJSONString(value));
    }

    /** Get value */
    public UserDO get(Long id) {
        String key = String.format(KEY_FORMAT, id);
        String value = valueOperations.get(key);
        return JSON.parseObject(value, UserDO.class);
    }

    ...
}

@Repository
public class RoleValue {
    /** Value operation */
    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> valueOperations;
    /** Value mode */
    private static final String KEY_FORMAT = "Value:Role:%s";

    /** Set value */
    public void set(Long id, RoleDO value) {
        String key = String.format(KEY_FORMAT, id);
        valueOperations.set(key, JSON.toJSONString(value));
    }

    /** Get value */
    public RoleDO get(Long id) {
        String key = String.format(KEY_FORMAT, id);
        String value = valueOperations.get(key);
        return JSON.parseObject(value, RoleDO.class);
    }

    ...
}

Streamlining:

public abstract class AbstractDynamicValue<I, V> {
    /** Value operation */
    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> valueOperations;

    /** Set value */
    public void set(I id, V value) {
        valueOperations.set(getKey(id), JSON.toJSONString(value));
    }

    /** Get value */
    public V get(I id) {
        return JSON.parseObject(valueOperations.get(getKey(id)), getValueClass());
    }

    ...

    /** Get primary key */
    protected abstract String getKey(I id);

    /** Get value class */
    protected abstract Class<V> getValueClass();
}

@Repository
public class UserValue extends AbstractValue<Long, UserDO> {
    /** Get primary key */
    @Override
    protected String getKey(Long id) {
        return String.format("Value:User:%s", id);
    }

    /** Get value class */
    @Override
    protected Class<UserDO> getValueClass() {
        return UserDO.class;
    }
}

@Repository
public class RoleValue extends AbstractValue<Long, RoleDO> {
    /** Get primary key */
    @Override
    protected String getKey(Long id) {
        return String.format("Value:Role:%s", id);
    }

    /** Get value class */
    @Override
    protected Class<RoleDO> getValueClass() {
        return RoleDO.class;
    }
}

10.2. Builder mode

Builder Pattern separates the construction of a complex object from its representation, so that the same construction process can create different representations. Such a design pattern is called Builder Pattern.

Normal:

public interface DataHandler<T> {
    /** Parse data */
    public T parseData(Record record);
    
    /** Store data */
    public boolean storeData(List<T> dataList);
}

public <T> long executeFetch(String tableName, int batchSize, DataHandler<T> dataHandler) throws Exception {
    // Build download session
    DownloadSession session = buildSession(tableName);

    // Number of acquired data
    long recordCount = session.getRecordCount();
    if (recordCount == 0) {
        return 0;
    }

    // Read data
    long fetchCount = 0L;
    try (RecordReader reader = session.openRecordReader(0L, recordCount, true)) {
        // Read data in turn
        Record record;
        List<T> dataList = new ArrayList<>(batchSize);
        while ((record = reader.read()) != null) {
            // Parse add data
            T data = dataHandler.parseData(record);
            if (Objects.nonNull(data)) {
                dataList.add(data);
            }

            // Bulk data storage
            if (dataList.size() == batchSize) {
                boolean isContinue = dataHandler.storeData(dataList);
                fetchCount += batchSize;
                dataList.clear();
                if (!isContinue) {
                    break;
                }
            }
        }

        // Store remaining data
        if (CollectionUtils.isNotEmpty(dataList)) {
            dataHandler.storeData(dataList);
            fetchCount += dataList.size();
            dataList.clear();
        }
    }

    // Return to get quantity
    return fetchCount;
}

/** Use cases */
long fetchCount = odpsService.executeFetch("user", 5000, new DataHandler() {
    /** Parse data */
    @Override
    public T parseData(Record record) {
        UserDO user = new UserDO();
        user.setId(record.getBigint("id"));
        user.setName(record.getString("name"));
        return user;
    }
    
    /** Store data */
    @Override
    public boolean storeData(List<T> dataList) {
        userDAO.batchInsert(dataList);
        return true;
    }
});

Streamlining:

public <T> long executeFetch(String tableName, int batchSize, Function<Record, T> dataParser, Function<List<T>, Boolean> dataStorage) throws Exception {
    // Build download session
    DownloadSession session = buildSession(tableName);

    // Number of acquired data
    long recordCount = session.getRecordCount();
    if (recordCount == 0) {
        return 0;
    }

    // Read data
    long fetchCount = 0L;
    try (RecordReader reader = session.openRecordReader(0L, recordCount, true)) {
        // Read data in turn
        Record record;
        List<T> dataList = new ArrayList<>(batchSize);
        while ((record = reader.read()) != null) {
            // Parse add data
            T data = dataParser.apply(record);
            if (Objects.nonNull(data)) {
                dataList.add(data);
            }

            // Bulk data storage
            if (dataList.size() == batchSize) {
                Boolean isContinue = dataStorage.apply(dataList);
                fetchCount += batchSize;
                dataList.clear();
                if (!Boolean.TRUE.equals(isContinue)) {
                    break;
                }
            }
        }

        // Store remaining data
        if (CollectionUtils.isNotEmpty(dataList)) {
            dataStorage.apply(dataList);
            fetchCount += dataList.size();
            dataList.clear();
        }
    }

    // Return to get quantity
    return fetchCount;
}

/** Use cases */
long fetchCount = odpsService.executeFetch("user", 5000, record -> {
        UserDO user = new UserDO();
        user.setId(record.getBigint("id"));
        user.setName(record.getString("name"));
        return user;
    }, dataList -> {
        userDAO.batchInsert(dataList);
        return true;
    });

In the common builder mode, the DataHandler interface needs to be defined when it is implemented, and the anonymous inner class of DataHandler needs to be implemented when it is called, so the code is more complicated. The simplified builder mode makes full use of functional programming, without defining the interface and using the Function interface directly; when calling, it does not need to implement the anonymous inner class and uses the lambda expression directly, so the code is less and simpler.

10.3. Agency mode

The most important agent mode in Spring is AOP (aspect oriented programming), which is implemented by using JDK dynamic agent and CGLIB dynamic agent technology.

Normal:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    /** User services */
    @Autowired
    private UserService userService;

    /** Query users */
    @PostMapping("/queryUser")
    public Result<?> queryUser(@RequestBody @Valid UserQueryVO query) {
        try {
            PageDataVO<UserVO> pageData = userService.queryUser(query);
            return Result.success(pageData);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.failure(e.getMessage());
        }
    }
    ...
}

Streamline 1:

Exception handling based on @ ControllerAdvice:

@RestController
@RequestMapping("/user")
public class UserController {
    /** User services */
    @Autowired
    private UserService userService;

    /** Query users */
    @PostMapping("/queryUser")
    public Result<PageDataVO<UserVO>> queryUser(@RequestBody @Valid UserQueryVO query) {
        PageDataVO<UserVO> pageData = userService.queryUser(query);
        return Result.success(pageData);
    }
    ...
}

@Slf4j
@ControllerAdvice
public class GlobalControllerAdvice {
    /** Handling exceptions */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error(e.getMessage(), e);
        return Result.failure(e.getMessage());
    }
}

Streamline 2:

AOP based exception handling:

// UserController code is the same as "thin 1"

@Slf4j
@Aspect
public class WebExceptionAspect {
    /** Point tangent */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    private void webPointcut() {}

    /** Handling exceptions */
    @AfterThrowing(pointcut = "webPointcut()", throwing = "e")
    public void handleException(Exception e) {
        Result<Void> result = Result.failure(e.getMessage());
        writeContent(JSON.toJSONString(result));
    }
    ...
}

11. Use delete code

"Less is more", "less" is not blank but simplified, "more" is not crowded but perfect. Remove redundant code to make the code more concise and perfect.

11.1. Delete obsolete code

Delete the obsolete package, class, field, method, variable, constant, import, annotation, annotation, annotated code, Maven package import, SQL statement of MyBatis, property configuration field, etc. in the project to simplify the project code and facilitate maintenance.

Normal:

import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class ProductService {
    @Value("discardRate")
    private double discardRate;
    ...
    private ProductVO transProductDO(ProductDO productDO) {
        ProductVO productVO = new ProductVO();
        BeanUtils.copyProperties(productDO, productVO);
        // productVO.setPrice(getDiscardPrice(productDO.getPrice()));
        return productVO;
    }
    private BigDecimal getDiscardPrice(BigDecimal originalPrice) {
        ...
    }
}

Streamlining:

@Service
public class ProductService {
    ...
    private ProductVO transProductDO(ProductDO productDO) {
        ProductVO productVO = new ProductVO();
        BeanUtils.copyProperties(productDO, productVO);
        return productVO;
    }
}

11.2. Delete public of interface method

For an interface, all fields and methods are public, and you don't need to explicitly declare them as public.

Normal:

public interface UserDAO {
    public Long countUser(@Param("query") UserQuery query);
    public List<UserDO> queryUser(@Param("query") UserQuery query);
}

Streamlining:

public interface UserDAO {
    Long countUser(@Param("query") UserQuery query);
    List<UserDO> queryUser(@Param("query") UserQuery query);
}

11.3. Delete private of enumeration construction method

For the enumeration (menu), the construction methods are all private, and you can not explicitly declare them as private.

Normal:

public enum UserStatus {
    DISABLED(0, "Prohibit"),
    ENABLED(1, "Enable");
    private final Integer value;
    private final String desc;
    private UserStatus(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    ...
}

Streamlining:

public enum UserStatus {
    DISABLED(0, "Prohibit"),
    ENABLED(1, "Enable");
    private final Integer value;
    private final String desc;
    UserStatus(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    ...
}

11.4. Delete the final of the final class method

For a final class, it cannot be inherited by a subclass, so its methods will not be overwritten and there is no need to add a final decoration.

Normal:

public final Rectangle implements Shape {
    ...
    @Override
    public final double getArea() {
        return width * height;
    }
}

Streamlining:

public final Rectangle implements Shape {
    ...
    @Override
    public double getArea() {
        return width * height;
    }
}

11.5. Delete the interface of base class implements

If the base class has implemented an interface, the subclass does not need to implement the interface. It only needs to directly implement the interface method.

Normal:

public interface Shape {
    ...
    double getArea();
}
public abstract AbstractShape implements Shape {
    ...
}
public final Rectangle extends AbstractShape implements Shape {
    ...
    @Override
    public double getArea() {
        return width * height;
    }
}

Streamlining:

...
public final Rectangle extends AbstractShape {
    ...
    @Override
    public double getArea() {
        return width * height;
    }
}

11.6. Delete unnecessary variables

Unnecessary variables will only make the code look more cumbersome.

Normal:

public Boolean existsUser(Long userId) {
    Boolean exists = userDAO.exists(userId);
    return exists;
}

Streamlining:

public Boolean existsUser(Long userId) {
    return userDAO.exists(userId);
}

Epilogue

As the old saying goes:

If there is a way, there is no skill, but the skill can be sought; if there is a skill, there is no way, it is only a skill.

It means that there is "Tao" but no "skill", and "skill" can be acquired gradually; if there is "skill" but no "Tao", it may stop at "skill". Therefore, we should not only be satisfied with summing up "art" from practice, because the manifestation of "Tao" is changeable; instead, we should rise to the height of "Tao", because the truth behind "art" is interlinked. When we encounter new things, we can find "Tao" from theory, find "art" from practice, and try to recognize new things.

Author information: Chen Changyi, Huaming Changyi, expert in cartography.

Tags: Java JSON Session Programming

Posted on Fri, 08 May 2020 02:21:07 -0700 by Rai_de