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 his 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 the fact that the code can be compiled without handling them, so they are called unchecked exceptions. With unchecked exception, unnecessary try catch and throws exception handling can be avoided.
Normal:
@Servicepublic 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:
@Servicepublic 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@ToStringpublic class UserVO { private Long id; private String name; ...}
2.2. Using Validation notes
Normal:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...}@Service@Validatedpublic class UserService { public Long createUser(@Valid UserCreateVO create) { // TODO: create user return null;}}}
Streamlining:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...} @Service@Validatedpublic 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: process 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 to simplify 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, it can be abbreviated without value;
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@ToStringpublic 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@ToStringpublic 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@ToStringpublic class IntPoint { private Integer x; private Integer y;} @Getter@Setter@ToStringpublic class DoublePoint { private Double x; private Double y;}
Streamlining:
@Getter@Setter@ToStringpublic 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@ToStringpublic 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@AllArgsConstructorpublic 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:
SSet<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:
be careful: Arrays.asList The returned List is not 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.
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 decreased, a large number of assignment statements can be reduced, thus simplifying the test code.
Normal:
@Testpublic 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:
@Testpublic 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.
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 valueSqlHelper.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[] ;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@AllArgsConstructorpublic 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:
tring 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:
@Repositorypublic 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); } ...} @Repositorypublic 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();} @Repositorypublic 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; }} @Repositorypublic 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 caseslong fetchCount = odpsService.executeFetch("user", 5000, new DataHandler() { /** Parse data */ @Overridepublic T parseData(Record record) { UserDO user = new UserDO(); user.setId(record.getBigint("id")); user.setName(record.getString("name")); return user; } /** Store data */ @Overridepublic 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 caseslong 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@ControllerAdvicepublic 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@Aspectpublic 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@Servicepublic 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:
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, "Disable"), 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, "Disable"), 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 only make the code look more tedious.
Normal:
public Boolean existsUser(Long userId) { Boolean exists = userDAO.exists(userId); return exists;}
Streamlining:
public Boolean existsUser(Long userId) { return userDAO.exists(userId);}
Postscript
The old saying goes:
If there is a way without skill, skill can be sought; if there is a way without skill, it ends in 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.
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 his 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 the fact that the code can be compiled without handling them, so they are called unchecked exceptions. With unchecked exception, unnecessary try catch and throws exception handling can be avoided.
Normal:
@Servicepublic 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:
@Servicepublic 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@ToStringpublic class UserVO { private Long id; private String name; ...}
2.2. Using Validation notes
Normal:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...}@Service@Validatedpublic class UserService { public Long createUser(@Valid UserCreateVO create) { // TODO: create user return null;}}}
Streamlining:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...} @Service@Validatedpublic 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: process 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 to simplify 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, it can be abbreviated without value;
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@ToStringpublic 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@ToStringpublic 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@ToStringpublic class IntPoint { private Integer x; private Integer y;} @Getter@Setter@ToStringpublic class DoublePoint { private Double x; private Double y;}
Streamlining:
@Getter@Setter@ToStringpublic 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@ToStringpublic 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@AllArgsConstructorpublic 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:
SSet<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:
be careful: Arrays.asList The returned List is not 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.
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 decreased, a large number of assignment statements can be reduced, thus simplifying the test code.
Normal:
@Testpublic 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:
@Testpublic 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.
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 valueSqlHelper.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[] ;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@AllArgsConstructorpublic 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:
tring 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:
@Repositorypublic 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); } ...} @Repositorypublic 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();} @Repositorypublic 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; }} @Repositorypublic 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 caseslong fetchCount = odpsService.executeFetch("user", 5000, new DataHandler() { /** Parse data */ @Overridepublic T parseData(Record record) { UserDO user = new UserDO(); user.setId(record.getBigint("id")); user.setName(record.getString("name")); return user; } /** Store data */ @Overridepublic 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 caseslong 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@ControllerAdvicepublic 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@Aspectpublic 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@Servicepublic 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:
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, "Disable"), 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, "Disable"), 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 only make the code look more tedious.
Normal:
public Boolean existsUser(Long userId) { Boolean exists = userDAO.exists(userId); return exists;}
Streamlining:
public Boolean existsUser(Long userId) { return userDAO.exists(userId);}
Postscript
The old saying goes:
If there is a way without skill, skill can be sought; if there is a way without skill, it ends in 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.
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 his 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 the fact that the code can be compiled without handling them, so they are called unchecked exceptions. With unchecked exception, unnecessary try catch and throws exception handling can be avoided.
Normal:
@Servicepublic 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:
@Servicepublic 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@ToStringpublic class UserVO { private Long id; private String name; ...}
2.2. Using Validation notes
Normal:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...}@Service@Validatedpublic class UserService { public Long createUser(@Valid UserCreateVO create) { // TODO: create user return null;}}}
Streamlining:
@Getter@Setter@ToStringpublic class UserCreateVO { @NotBlank(message = "User name cannot be empty") private String name; @NotNull(message = "Company ID cannot be empty") private Long companyId; ...} @Service@Validatedpublic 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: process 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 to simplify 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, it can be abbreviated without value;
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@ToStringpublic 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@ToStringpublic 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@ToStringpublic class IntPoint { private Integer x; private Integer y;} @Getter@Setter@ToStringpublic class DoublePoint { private Double x; private Double y;}
Streamlining:
@Getter@Setter@ToStringpublic 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@ToStringpublic 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@AllArgsConstructorpublic 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:
SSet<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:
be careful: Arrays.asList The returned List is not 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.
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 decreased, a large number of assignment statements can be reduced, thus simplifying the test code.
Normal:
@Testpublic 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:
@Testpublic 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.
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 valueSqlHelper.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[] ;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@AllArgsConstructorpublic 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:
tring 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:
@Repositorypublic 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); } ...} @Repositorypublic 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();} @Repositorypublic 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; }} @Repositorypublic 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 caseslong fetchCount = odpsService.executeFetch("user", 5000, new DataHandler() { /** Parse data */ @Overridepublic T parseData(Record record) { UserDO user = new UserDO(); user.setId(record.getBigint("id")); user.setName(record.getString("name")); return user; } /** Store data */ @Overridepublic 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 caseslong 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@ControllerAdvicepublic 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@Aspectpublic 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@Servicepublic 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:
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, "Disable"), 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, "Disable"), 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 only make the code look more tedious.
Normal:
public Boolean existsUser(Long userId) { Boolean exists = userDAO.exists(userId); return exists;}
Streamlining:
public Boolean existsUser(Long userId) { return userDAO.exists(userId);}
Postscript
The old saying goes:
If there is a way without skill, skill can be sought; if there is a way without skill, it ends in 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.