欢迎你来读这篇博客,这篇博客主要是关于Guava 对象工具、断言与比较器。 其中包括 Preconditions、Objects、MoreObjects、ComparisonChain、assert 与 Preconditions 的区别,以及在业务代码中优雅实现参数校验、toString、equals、hashCode、compareTo 的实战写法。
序言 在 Java 后端开发中,代码健壮性往往不是靠“业务逻辑很复杂”体现出来的,而是靠很多小细节体现出来的。
比如:
参数是不是 null;
金额是不是大于 0;
分页参数是否合法;
用户状态是不是允许执行当前操作;
下标有没有越界;
对象比较有没有空指针;
equals 和 hashCode 是否一致;
compareTo 是否稳定;
日志里的对象打印是否清晰。
这些问题看起来很基础,但它们决定了系统是否稳定。
Guava 在这一块提供了几类非常实用的工具:
1 2 3 第 2 章:对象工具、断言与比较器 2.1 Preconditions、Objects、assert 讲解 2.2 Objects、MoreObjects、ComparisonChain
对应原课程:
1 2 03 Preconditions、Objects、assert 讲解 04 Objects、MoreObjects、ComparisonChain
这一章重点不是 API 背诵,而是理解:
在业务代码里,如何让参数校验更清晰,让对象方法更可靠,让比较逻辑更优雅。
Guava 的这些工具类,有些在 Java 7/8 之后已经被 JDK 标准库替代了一部分。
比如:
已经能完成很多事情。
但 Guava 的 Preconditions、MoreObjects.toStringHelper、ComparisonChain 仍然非常有学习价值。
它们体现的是一种工程化思想:
尽早失败,错误明确,代码可读,边界清晰。
这比单纯记住某个 API 更重要。
正文 chapter 1:这一章解决什么问题 这一章主要解决三类问题。
1. 参数校验问题 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void createOrder (Long userId, BigDecimal amount) { if (userId == null ) { throw new IllegalArgumentException ("userId can not be null" ); } if (amount == null ) { throw new IllegalArgumentException ("amount can not be null" ); } if (amount.compareTo(BigDecimal.ZERO) <= 0 ) { throw new IllegalArgumentException ("amount must be greater than 0" ); } }
这种代码很常见。
但是写多了会很啰嗦。
Guava 提供:
1 2 3 Preconditions.checkNotNull(...) Preconditions.checkArgument(...) Preconditions.checkState(...)
让校验逻辑更清晰。
2. 对象辅助方法问题 比如:
1 2 3 equals() hashCode() toString()
这些方法写不好,很容易导致:
HashMap 查不到 key;
Set 去重失败;
日志打印一堆对象地址;
对象比较空指针;
调试成本增加。
Guava 提供:
1 2 3 Objects.equal(...) Objects.hashCode(...) MoreObjects.toStringHelper(...)
JDK 也提供:
1 2 java.util.Objects.equals(...) java.util.Objects.hash(...)
3. 比较器链式写法问题 比如实现 Comparable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public int compareTo (User other) { int result = this .age.compareTo(other.age); if (result != 0 ) { return result; } result = this .name.compareTo(other.name); if (result != 0 ) { return result; } return this .id.compareTo(other.id); }
比较字段一多,代码就会很丑。
Guava 提供:
可以写成:
1 2 3 4 5 return ComparisonChain.start() .compare(this .age, other.age) .compare(this .name, other.name) .compare(this .id, other.id) .result();
可读性会高很多。
chapter 2:Preconditions 是什么 Preconditions 是 Guava 提供的前置条件校验工具类。
它的核心思想是:
方法执行前先检查参数、状态和索引,不满足条件就立即抛出明确异常。
常用方法包括:
1 2 3 4 5 checkArgument(...) checkNotNull(...) checkState(...) checkElementIndex(...) checkPositionIndex(...)
这些方法都在:
1 com.google.common.base.Preconditions
中。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.base.Preconditions;public class PreconditionsDemo { public void createUser (String username, int age) { Preconditions.checkNotNull(username, "username can not be null" ); Preconditions.checkArgument(!username.isBlank(), "username can not be blank" ); Preconditions.checkArgument(age > 0 , "age must be greater than 0" ); System.out.println("创建用户:" + username); } }
相比手写 if,它的表达更简洁。
chapter 3:checkArgument checkArgument 用来检查方法参数是否合法。
典型场景:
金额必须大于 0;
页码必须大于等于 1;
用户名不能为空;
数量必须大于 0;
时间范围必须合法。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.base.Preconditions;import java.math.BigDecimal;public class CheckArgumentDemo { public void createOrder (Long userId, BigDecimal amount, Integer quantity) { Preconditions.checkNotNull(userId, "userId can not be null" ); Preconditions.checkNotNull(amount, "amount can not be null" ); Preconditions.checkNotNull(quantity, "quantity can not be null" ); Preconditions.checkArgument(amount.compareTo(BigDecimal.ZERO) > 0 , "amount must be greater than 0, amount = %s" , amount); Preconditions.checkArgument(quantity > 0 , "quantity must be greater than 0, quantity = %s" , quantity); System.out.println("创建订单成功" ); } }
如果条件不成立,会抛出:
1 IllegalArgumentException
例如:
1 amount must be greater than 0, amount = -1
checkArgument 的语义是:
调用方传入的参数不合法。
所以它适合用在方法入参校验上。
chapter 4:checkNotNull checkNotNull 用来检查对象不能为 null。
示例:
1 2 3 4 5 6 7 8 9 10 import com.google.common.base.Preconditions;public class CheckNotNullDemo { public String normalizeUsername (String username) { Preconditions.checkNotNull(username, "username can not be null" ); return username.trim().toLowerCase(); } }
如果 username 为 null,会抛出:
注意,checkNotNull 返回被校验对象本身。
所以可以这样写:
1 this .username = Preconditions.checkNotNull(username, "username can not be null" );
示例:
1 2 3 4 5 6 7 8 9 10 import com.google.common.base.Preconditions;public class User { private final String username; public User (String username) { this .username = Preconditions.checkNotNull(username, "username can not be null" ); } }
这在构造方法中非常常见。
chapter 5:checkState checkState 用来检查对象当前状态是否合法。
它和 checkArgument 的区别是:
checkArgument 检查参数;
checkState 检查对象状态。
例如订单支付:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.google.common.base.Preconditions;public class Order { private String status; public Order (String status) { this .status = status; } public void pay () { Preconditions.checkState("CREATED" .equals(status), "only CREATED order can be paid, current status = %s" , status); this .status = "PAID" ; System.out.println("订单支付成功" ); } }
如果订单不是 CREATED 状态,会抛出:
checkState 的语义是:
当前对象状态不允许执行这个操作。
这非常适合:
订单状态流转;
任务状态校验;
工作流节点校验;
连接状态校验;
初始化状态校验。
chapter 6:checkElementIndex checkElementIndex 用来检查元素下标是否合法。
语义是:
index 必须能指向一个已经存在的元素。
范围是:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.google.common.base.Preconditions;import java.util.List;public class CheckElementIndexDemo { public String getName (List<String> names, int index) { Preconditions.checkNotNull(names, "names can not be null" ); Preconditions.checkElementIndex(index, names.size(), "index" ); return names.get(index); } }
如果:
合法 index 是:
index = 3 不合法。
因为第 3 个位置不能取元素。
chapter 7:checkPositionIndex checkPositionIndex 用来检查位置是否合法。
语义是:
index 可以表示插入位置。
范围是:
注意它和 checkElementIndex 不一样。
如果:
合法 position 是:
因为可以在末尾插入。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import com.google.common.base.Preconditions;import java.util.ArrayList;import java.util.List;public class CheckPositionIndexDemo { public void insertName (List<String> names, int index, String name) { Preconditions.checkNotNull(names, "names can not be null" ); Preconditions.checkNotNull(name, "name can not be null" ); Preconditions.checkPositionIndex(index, names.size(), "index" ); names.add(index, name); } public static void main (String[] args) { List<String> names = new ArrayList <>(); names.add("Mario" ); names.add("Luigi" ); new CheckPositionIndexDemo ().insertName(names, 2 , "Peach" ); System.out.println(names); } }
输出:
index = size 对插入来说合法,但对取元素来说不合法。
这就是 checkElementIndex 和 checkPositionIndex 的区别。
chapter 8:checkPositionIndexes 除了单个位置,Guava 还提供:
1 checkPositionIndexes(start, end, size)
用于检查一个范围是否合法。
要求:
1 0 <= start <= end <= size
示例:
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.base.Preconditions;public class CheckPositionIndexesDemo { public String substring (String text, int start, int end) { Preconditions.checkNotNull(text, "text can not be null" ); Preconditions.checkPositionIndexes(start, end, text.length()); return text.substring(start, end); } }
适合:
截取字符串;
截取列表;
分页范围;
buffer 范围;
数组区间。
chapter 9:Preconditions 的异常类型总结
方法
校验对象
失败异常
checkArgument
方法参数
IllegalArgumentException
checkNotNull
非 null
NullPointerException
checkState
对象状态
IllegalStateException
checkElementIndex
元素下标
IndexOutOfBoundsException
checkPositionIndex
插入位置
IndexOutOfBoundsException
checkPositionIndexes
范围区间
IndexOutOfBoundsException
这几个方法最重要的是语义。
不要乱用。
比如参数不合法,用 checkArgument。
对象状态不合法,用 checkState。
null 检查用 checkNotNull。
下标越界用 index 相关方法。
chapter 10:Preconditions 的消息格式 Guava 的 Preconditions 支持 %s 占位符。
例如:
1 2 Preconditions.checkArgument(age > 0 , "age must be greater than 0, age = %s" , age);
注意,它不是 String.format 的完整格式能力。
它主要支持 %s。
不要写:
如果你需要复杂格式,提前格式化字符串即可。
好处是:
只有校验失败时,错误消息才需要真正构造。
这有利于性能,也避免无意义字符串拼接。
chapter 11:assert 是什么 assert 是 Java 语言内置的断言机制。
语法:
或者:
1 assert condition : message;
示例:
1 2 3 4 5 6 7 8 public class AssertDemo { public int divide (int a, int b) { assert b != 0 : "b can not be zero" ; return a / b; } }
如果断言开启,并且条件不成立,会抛出:
chapter 12:assert 默认不会执行 Java 的 assert 默认是关闭的。
需要通过 JVM 参数开启:
或者:
例如:
如果没有开启,assert 语句不会生效。
所以不要用 assert 做业务参数校验。
因为生产环境通常不会开启断言。
这意味着下面这种写法是危险的:
1 2 3 4 5 public void createOrder (Long userId) { assert userId != null : "userId can not be null" ; }
如果生产环境没开断言,null 就会继续往下走。
然后可能在更深层抛出莫名其妙的空指针。
chapter 13:assert 与 Preconditions 的区别
对比项
assert
Preconditions
类型
Java 语言特性
Guava 工具类
默认是否启用
默认关闭
永远执行
失败异常
AssertionError
明确异常类型
适合场景
内部不变量、开发调试
方法参数、业务状态校验
是否适合生产参数校验
不适合
适合
是否可被关闭
可以
不会
一句话总结:
assert 用于开发期验证内部假设,Preconditions 用于运行期校验外部输入和业务状态。
chapter 14:assert 适合什么场景 assert 适合检查那些“理论上不应该发生”的内部不变量。
例如:
1 2 3 4 5 6 7 8 public class SortUtils { public static int middle (int left, int right) { assert left <= right : "left must be <= right" ; return left + (right - left) / 2 ; } }
或者在复杂算法中检查内部状态:
1 assert stack.size() >= 0 ;
它更偏开发调试。
不适合用户输入、接口参数、数据库状态等业务校验。
chapter 15:业务代码中参数校验推荐写法 在业务代码中,可以按层次处理校验。
1. Controller 层 适合做基础参数格式校验。
例如:
Spring Boot 中推荐使用 Bean Validation:
1 2 3 4 5 @NotNull @NotBlank @Min @Max @Email
例如:
1 2 3 4 5 6 7 8 9 10 11 public class CreateOrderRequest { @NotNull private Long userId; @NotNull private Long productId; @Min(1) private Integer quantity; }
2. Application Service 层 适合做用例级参数校验。
例如:
1 2 Preconditions.checkNotNull(command, "command can not be null" ); Preconditions.checkNotNull(command.getUserId(), "userId can not be null" );
3. Domain 层 适合做业务不变量校验。
例如订单金额必须大于 0:
1 2 Preconditions.checkArgument(amount.compareTo(BigDecimal.ZERO) > 0 , "order amount must be greater than 0" );
或者状态校验:
1 2 Preconditions.checkState(canPay(), "current order status can not pay, status = %s" , status);
4. Infrastructure 层 适合做外部系统返回值校验。
例如:
1 Preconditions.checkNotNull(response, "payment response can not be null" );
不要把所有校验都堆在 Controller。
也不要完全依赖数据库约束。
好的校验应该分层。
chapter 16:Preconditions 实战:订单创建命令校验 定义命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.math.BigDecimal;public class CreateOrderCommand { private final Long userId; private final Long productId; private final Integer quantity; private final BigDecimal amount; public CreateOrderCommand (Long userId, Long productId, Integer quantity, BigDecimal amount) { this .userId = userId; this .productId = productId; this .quantity = quantity; this .amount = amount; } public Long getUserId () { return userId; } public Long getProductId () { return productId; } public Integer getQuantity () { return quantity; } public BigDecimal getAmount () { return amount; } }
校验器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.google.common.base.Preconditions;import java.math.BigDecimal;public class CreateOrderCommandValidator { public void validate (CreateOrderCommand command) { Preconditions.checkNotNull(command, "command can not be null" ); Preconditions.checkNotNull(command.getUserId(), "userId can not be null" ); Preconditions.checkNotNull(command.getProductId(), "productId can not be null" ); Preconditions.checkNotNull(command.getQuantity(), "quantity can not be null" ); Preconditions.checkNotNull(command.getAmount(), "amount can not be null" ); Preconditions.checkArgument(command.getQuantity() > 0 , "quantity must be greater than 0, quantity = %s" , command.getQuantity()); Preconditions.checkArgument(command.getAmount().compareTo(BigDecimal.ZERO) > 0 , "amount must be greater than 0, amount = %s" , command.getAmount()); } }
使用:
1 2 3 4 5 6 7 8 9 10 public class OrderApplicationService { private final CreateOrderCommandValidator validator = new CreateOrderCommandValidator (); public void createOrder (CreateOrderCommand command) { validator.validate(command); System.out.println("创建订单" ); } }
这种写法比在业务方法里堆一大堆 if 更清晰。
chapter 17:Objects.equal Guava 的 Objects.equal(a, b) 用来做 null-safe 比较。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.google.common.base.Objects;public class ObjectsEqualDemo { public static void main (String[] args) { String a = null ; String b = null ; System.out.println(Objects.equal(a, b)); System.out.println(Objects.equal("a" , "a" )); System.out.println(Objects.equal("a" , null )); } }
输出:
它避免了:
在 a 为 null 时抛出空指针。
不过 Java 7 之后,JDK 提供了:
1 java.util.Objects.equals(a, b)
新项目中更推荐使用 JDK:
1 java.util.Objects.equals(a, b)
chapter 18:Objects.hashCode Guava 的 Objects.hashCode(...) 可以生成多个字段的 hash。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.google.common.base.Objects;public class ObjectsHashCodeDemo { private Long id; private String username; @Override public int hashCode () { return Objects.hashCode(id, username); } }
Java 7 之后,JDK 提供:
1 java.util.Objects.hash(id, username)
新项目中更推荐 JDK:
1 2 3 4 @Override public int hashCode () { return java.util.Objects.hash(id, username); }
chapter 19:Guava Objects 与 JDK Objects 对比
能力
Guava Objects
JDK java.util.Objects
null-safe equals
Objects.equal(a, b)
Objects.equals(a, b)
多字段 hash
Objects.hashCode(a, b)
Objects.hash(a, b)
非空校验
不推荐用它做
Objects.requireNonNull
推荐程度
老项目常见
新项目优先推荐
Guava 的 Objects 在早期非常实用。
但现在 Java 标准库已经提供类似能力。
所以建议:
老项目已有 Guava,可以继续维护;
新项目优先使用 java.util.Objects;
MoreObjects.toStringHelper 和 ComparisonChain 仍然有独特价值。
chapter 20:MoreObjects.toStringHelper MoreObjects.toStringHelper 用来优雅实现 toString()。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.google.common.base.MoreObjects;public class User { private Long id; private String username; private Integer age; @Override public String toString () { return MoreObjects.toStringHelper(this ) .add("id" , id) .add("username" , username) .add("age" , age) .toString(); } }
输出类似:
1 User{id=1, username=Mario, age=18}
相比手写:
1 2 3 4 5 return "User{" + "id=" + id + ", username='" + username + '\'' + ", age=" + age + '}' ;
toStringHelper 更清晰,也更不容易漏逗号。
chapter 21:toStringHelper omitNullValues 有时候不希望打印 null 字段。
可以使用:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import com.google.common.base.MoreObjects;public class UserProfile { private Long userId; private String nickname; private String email; public UserProfile (Long userId, String nickname, String email) { this .userId = userId; this .nickname = nickname; this .email = email; } @Override public String toString () { return MoreObjects.toStringHelper(this ) .omitNullValues() .add("userId" , userId) .add("nickname" , nickname) .add("email" , email) .toString(); } }
如果 email 为 null:
1 UserProfile{userId=1, nickname=Mario}
这在日志打印中很有用。
chapter 22:toStringHelper 的使用建议 toString() 经常用于日志和调试。
建议:
1. 不要打印敏感信息 例如:
密码;
token;
身份证;
手机号完整值;
银行卡;
密钥。
不要这样:
1 .add("password" , password)
2. 不要打印超大字段 例如:
大文本;
文件内容;
二进制内容;
超大 JSON。
否则日志会爆。
3. 重要业务对象建议实现 toString 例如:
Command;
DTO;
VO;
Event;
Query;
Domain 对象。
排查问题时非常有帮助。
chapter 23:equals 和 hashCode 的契约 实现 equals 和 hashCode 时,要遵守契约。
1. equals 相等,hashCode 必须相等 如果:
那么必须:
1 a.hashCode() == b.hashCode()
2. hashCode 相等,equals 不一定相等 hash 冲突是允许的。
3. equals 应该满足
自反性;
对称性;
传递性;
一致性;
非 null 比较返回 false。
如果这些规则破坏,集合类会出问题。
例如:
HashSet 去重异常;
HashMap key 查不到;
List contains 行为异常。
chapter 24:实战:优雅实现 equals、hashCode、toString 定义用户对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import com.google.common.base.MoreObjects;import java.util.Objects;public class UserProfile { private final Long userId; private final String username; private final String email; private final Integer age; public UserProfile (Long userId, String username, String email, Integer age) { this .userId = Objects.requireNonNull(userId, "userId can not be null" ); this .username = Objects.requireNonNull(username, "username can not be null" ); this .email = email; this .age = age; } public Long getUserId () { return userId; } public String getUsername () { return username; } public String getEmail () { return email; } public Integer getAge () { return age; } @Override public boolean equals (Object o) { if (this == o) { return true ; } if (!(o instanceof UserProfile that)) { return false ; } return Objects.equals(userId, that.userId) && Objects.equals(username, that.username) && Objects.equals(email, that.email) && Objects.equals(age, that.age); } @Override public int hashCode () { return Objects.hash(userId, username, email, age); } @Override public String toString () { return MoreObjects.toStringHelper(this ) .omitNullValues() .add("userId" , userId) .add("username" , username) .add("email" , email) .add("age" , age) .toString(); } }
这里建议:
equals 和 hashCode 使用 JDK Objects;
toString 使用 Guava MoreObjects.toStringHelper;
构造参数非空用 Objects.requireNonNull 或 Guava Preconditions.checkNotNull。
chapter 25:ComparisonChain 是什么 ComparisonChain 是 Guava 提供的链式比较工具。
它主要解决:
多字段比较时代码冗长的问题。
比如用户排序规则:
1 2 3 先按年龄升序 年龄相同按用户名升序 用户名相同按用户 ID 升序
传统写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public int compareTo (User other) { int result = this .age.compareTo(other.age); if (result != 0 ) { return result; } result = this .username.compareTo(other.username); if (result != 0 ) { return result; } return this .userId.compareTo(other.userId); }
使用 ComparisonChain:
1 2 3 4 5 6 7 8 9 10 import com.google.common.collect.ComparisonChain;@Override public int compareTo (User other) { return ComparisonChain.start() .compare(this .age, other.age) .compare(this .username, other.username) .compare(this .userId, other.userId) .result(); }
可读性更好。
chapter 26:ComparisonChain 的执行特点 ComparisonChain 有一个重要特点:
一旦前面的比较结果不为 0,后面的比较不会再影响结果。
例如:
1 2 3 4 5 ComparisonChain.start() .compare(age1, age2) .compare(name1, name2) .compare(id1, id2) .result();
如果 age1 和 age2 已经不同,后面的 name、id 就不再决定结果。
这和我们手写多字段比较逻辑一致。
chapter 27:ComparisonChain 基本示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import com.google.common.collect.ComparisonChain;public class User implements Comparable <User> { private final Long userId; private final String username; private final Integer age; public User (Long userId, String username, Integer age) { this .userId = userId; this .username = username; this .age = age; } @Override public int compareTo (User other) { return ComparisonChain.start() .compare(this .age, other.age) .compare(this .username, other.username) .compare(this .userId, other.userId) .result(); } }
排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.ArrayList;import java.util.List;public class ComparisonChainDemo { public static void main (String[] args) { List<User> users = new ArrayList <>(); users.add(new User (3L , "Peach" , 18 )); users.add(new User (1L , "Mario" , 20 )); users.add(new User (2L , "Luigi" , 20 )); users.sort(null ); System.out.println(users); } }
chapter 28:ComparisonChain 处理基本类型 ComparisonChain 支持多种类型比较:
1 2 3 4 5 6 7 8 compare(int left, int right) compare(long left, long right) compare(float left, float right) compare(double left, double right) compare(Comparable left, Comparable right) compare(left, right, Comparator) compareFalseFirst(boolean left, boolean right) compareTrueFirst(boolean left, boolean right)
例如:
1 2 3 4 5 return ComparisonChain.start() .compareFalseFirst(this .deleted, other.deleted) .compare(this .priority, other.priority) .compare(this .createdAt, other.createdAt) .result();
compareFalseFirst 表示 false 排在 true 前面。
compareTrueFirst 表示 true 排在 false 前面。
这在排序“启用状态”“删除状态”“置顶状态”时很有用。
chapter 29:ComparisonChain 结合 Comparator 有时候字段本身不是自然顺序,或者需要 null 处理。
可以传入 Comparator。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import com.google.common.collect.ComparisonChain;import com.google.common.collect.Ordering;public class Product implements Comparable <Product> { private final Long productId; private final String name; private final Integer sortOrder; public Product (Long productId, String name, Integer sortOrder) { this .productId = productId; this .name = name; this .sortOrder = sortOrder; } @Override public int compareTo (Product other) { return ComparisonChain.start() .compare(this .sortOrder, other.sortOrder, Ordering.natural().nullsLast()) .compare(this .name, other.name, Ordering.natural().nullsLast()) .compare(this .productId, other.productId) .result(); } }
这里使用了 Guava 的 Ordering。
在 Java 8 之后,也可以用 JDK Comparator:
1 Comparator.nullsLast(Comparator.naturalOrder())
示例:
1 2 3 4 5 return ComparisonChain.start() .compare(this .sortOrder, other.sortOrder, Comparator.nullsLast(Comparator.naturalOrder())) .compare(this .name, other.name, Comparator.nullsLast(Comparator.naturalOrder())) .compare(this .productId, other.productId) .result();
chapter 30:Java 8 Comparator 链式写法对比 Java 8 之后,也可以使用 Comparator 链式写法。
1 2 3 4 Comparator<User> comparator = Comparator .comparing(User::getAge) .thenComparing(User::getUsername) .thenComparing(User::getUserId);
排序:
那 ComparisonChain 和 Java 8 Comparator 怎么选?
场景
推荐
实现 compareTo
ComparisonChain 很清晰
临时排序列表
Java 8 Comparator.comparing
复杂 null 排序
Java 8 Comparator 或 Guava Ordering
老项目已有 Guava
ComparisonChain 可以继续用
新项目偏 JDK 标准
Java 8 Comparator 优先
例如实现 Comparable:
1 2 3 4 5 6 7 8 @Override public int compareTo (User other) { return ComparisonChain.start() .compare(this .age, other.age) .compare(this .username, other.username) .compare(this .userId, other.userId) .result(); }
可读性非常好。
而在外部临时排序:
1 2 users.sort(Comparator.comparing(User::getAge) .thenComparing(User::getUsername));
更符合 Java 8 风格。
chapter 31:实战:优雅实现 Comparable、equals、hashCode、toString 定义一个订单排序对象。
排序规则:
未删除排在已删除前面;
优先级高的排前面;
创建时间早的排前面;
订单 ID 小的排前面。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import com.google.common.base.MoreObjects;import com.google.common.collect.ComparisonChain;import java.time.LocalDateTime;import java.util.Objects;public class OrderView implements Comparable <OrderView> { private final Long orderId; private final Long userId; private final Integer priority; private final boolean deleted; private final LocalDateTime createdAt; public OrderView (Long orderId, Long userId, Integer priority, boolean deleted, LocalDateTime createdAt) { this .orderId = Objects.requireNonNull(orderId, "orderId can not be null" ); this .userId = Objects.requireNonNull(userId, "userId can not be null" ); this .priority = Objects.requireNonNull(priority, "priority can not be null" ); this .deleted = deleted; this .createdAt = Objects.requireNonNull(createdAt, "createdAt can not be null" ); } public Long getOrderId () { return orderId; } public Long getUserId () { return userId; } public Integer getPriority () { return priority; } public boolean isDeleted () { return deleted; } public LocalDateTime getCreatedAt () { return createdAt; } @Override public int compareTo (OrderView other) { return ComparisonChain.start() .compareFalseFirst(this .deleted, other.deleted) .compare(other.priority, this .priority) .compare(this .createdAt, other.createdAt) .compare(this .orderId, other.orderId) .result(); } @Override public boolean equals (Object o) { if (this == o) { return true ; } if (!(o instanceof OrderView that)) { return false ; } return deleted == that.deleted && Objects.equals(orderId, that.orderId) && Objects.equals(userId, that.userId) && Objects.equals(priority, that.priority) && Objects.equals(createdAt, that.createdAt); } @Override public int hashCode () { return Objects.hash(orderId, userId, priority, deleted, createdAt); } @Override public String toString () { return MoreObjects.toStringHelper(this ) .add("orderId" , orderId) .add("userId" , userId) .add("priority" , priority) .add("deleted" , deleted) .add("createdAt" , createdAt) .toString(); } }
这里有几个点:
1 .compareFalseFirst(this .deleted, other.deleted)
表示未删除的订单排前面。
1 .compare(other.priority, this .priority)
表示优先级大的排前面。
因为默认是升序,所以要反过来比较。
chapter 32:compareTo 和 equals 是否必须一致 这是一个很重要的问题。
如果一个类实现 Comparable,建议尽量让:
和:
保持一致。
否则在 TreeSet、TreeMap 这类依赖比较结果的集合里,可能出现奇怪行为。
例如:
1 TreeSet<User> set = new TreeSet <>();
如果两个不同对象 compareTo 返回 0,TreeSet 会认为它们重复。
即使 equals 返回 false。
所以排序字段设计要谨慎。
如果只是临时排序,不一定要让类实现 Comparable,可以使用外部 Comparator。
chapter 33:业务中什么时候不要实现 Comparable 不是所有对象都应该实现 Comparable。
如果排序规则是对象的“自然顺序”,可以实现。
例如:
时间对象按时间排序;
金额对象按金额排序;
版本号按版本排序。
但如果排序规则是业务场景相关的,比如:
后台列表按优先级排序;
前台页面按销量排序;
搜索结果按相关度排序;
财务报表按金额排序。
这些规则可能随场景变化,不建议直接写进 compareTo。
更适合使用外部 Comparator:
1 orders.sort(Comparator.comparing(OrderView::getCreatedAt));
或者定义多个 Comparator:
1 2 3 OrderComparators.BY_PRIORITY OrderComparators.BY_CREATED_AT OrderComparators.BY_AMOUNT_DESC
chapter 34:Preconditions、Objects、MoreObjects、ComparisonChain 使用建议 1. 参数校验用 Preconditions 或 JDK requireNonNull Guava:
1 Preconditions.checkNotNull(value, "value can not be null" );
JDK:
1 Objects.requireNonNull(value, "value can not be null" );
如果是非 null 检查,新项目用 JDK 也很好。
如果是参数条件检查,Preconditions.checkArgument 很清晰。
2. 状态校验用 checkState 不要用 checkArgument 表达状态错误。
推荐:
1 Preconditions.checkState(order.canPay(), "order can not pay" );
3. assert 不要用于业务校验 它默认关闭,不可靠。
4. equals/hashCode 优先用 JDK Objects 1 2 Objects.equals(a, b) Objects.hash(a, b)
5. toString 可以用 MoreObjects.toStringHelper 特别适合 DTO、Command、Event、VO。
6. 多字段 compareTo 用 ComparisonChain 尤其是比较字段比较多时,可读性很好。
7. 临时排序用 Java 8 Comparator 1 2 Comparator.comparing(...) .thenComparing(...)
8. 不要滥用工具类 工具类不是越多越好。
代码语义清晰才是目标。
chapter 35:完整案例代码汇总 CreateOrderCommand 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.math.BigDecimal;public class CreateOrderCommand { private final Long userId; private final Long productId; private final Integer quantity; private final BigDecimal amount; public CreateOrderCommand (Long userId, Long productId, Integer quantity, BigDecimal amount) { this .userId = userId; this .productId = productId; this .quantity = quantity; this .amount = amount; } public Long getUserId () { return userId; } public Long getProductId () { return productId; } public Integer getQuantity () { return quantity; } public BigDecimal getAmount () { return amount; } }
CreateOrderCommandValidator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.google.common.base.Preconditions;import java.math.BigDecimal;public class CreateOrderCommandValidator { public void validate (CreateOrderCommand command) { Preconditions.checkNotNull(command, "command can not be null" ); Preconditions.checkNotNull(command.getUserId(), "userId can not be null" ); Preconditions.checkNotNull(command.getProductId(), "productId can not be null" ); Preconditions.checkNotNull(command.getQuantity(), "quantity can not be null" ); Preconditions.checkNotNull(command.getAmount(), "amount can not be null" ); Preconditions.checkArgument(command.getQuantity() > 0 , "quantity must be greater than 0, quantity = %s" , command.getQuantity()); Preconditions.checkArgument(command.getAmount().compareTo(BigDecimal.ZERO) > 0 , "amount must be greater than 0, amount = %s" , command.getAmount()); } }
UserProfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import com.google.common.base.MoreObjects;import java.util.Objects;public class UserProfile { private final Long userId; private final String username; private final String email; private final Integer age; public UserProfile (Long userId, String username, String email, Integer age) { this .userId = Objects.requireNonNull(userId, "userId can not be null" ); this .username = Objects.requireNonNull(username, "username can not be null" ); this .email = email; this .age = age; } public Long getUserId () { return userId; } public String getUsername () { return username; } public String getEmail () { return email; } public Integer getAge () { return age; } @Override public boolean equals (Object o) { if (this == o) { return true ; } if (!(o instanceof UserProfile that)) { return false ; } return Objects.equals(userId, that.userId) && Objects.equals(username, that.username) && Objects.equals(email, that.email) && Objects.equals(age, that.age); } @Override public int hashCode () { return Objects.hash(userId, username, email, age); } @Override public String toString () { return MoreObjects.toStringHelper(this ) .omitNullValues() .add("userId" , userId) .add("username" , username) .add("email" , email) .add("age" , age) .toString(); } }
OrderView 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import com.google.common.base.MoreObjects;import com.google.common.collect.ComparisonChain;import java.time.LocalDateTime;import java.util.Objects;public class OrderView implements Comparable <OrderView> { private final Long orderId; private final Long userId; private final Integer priority; private final boolean deleted; private final LocalDateTime createdAt; public OrderView (Long orderId, Long userId, Integer priority, boolean deleted, LocalDateTime createdAt) { this .orderId = Objects.requireNonNull(orderId, "orderId can not be null" ); this .userId = Objects.requireNonNull(userId, "userId can not be null" ); this .priority = Objects.requireNonNull(priority, "priority can not be null" ); this .deleted = deleted; this .createdAt = Objects.requireNonNull(createdAt, "createdAt can not be null" ); } public Long getOrderId () { return orderId; } public Long getUserId () { return userId; } public Integer getPriority () { return priority; } public boolean isDeleted () { return deleted; } public LocalDateTime getCreatedAt () { return createdAt; } @Override public int compareTo (OrderView other) { return ComparisonChain.start() .compareFalseFirst(this .deleted, other.deleted) .compare(other.priority, this .priority) .compare(this .createdAt, other.createdAt) .compare(this .orderId, other.orderId) .result(); } @Override public boolean equals (Object o) { if (this == o) { return true ; } if (!(o instanceof OrderView that)) { return false ; } return deleted == that.deleted && Objects.equals(orderId, that.orderId) && Objects.equals(userId, that.userId) && Objects.equals(priority, that.priority) && Objects.equals(createdAt, that.createdAt); } @Override public int hashCode () { return Objects.hash(orderId, userId, priority, deleted, createdAt); } @Override public String toString () { return MoreObjects.toStringHelper(this ) .add("orderId" , orderId) .add("userId" , userId) .add("priority" , priority) .add("deleted" , deleted) .add("createdAt" , createdAt) .toString(); } }
chapter 36:一句话总结 这一章的核心是:
用清晰的前置条件校验提升代码健壮性,用对象辅助工具减少样板代码,用链式比较器让排序逻辑更可读。
Preconditions 解决的是参数和状态校验问题。
assert 适合开发期内部断言,不适合生产业务校验。
Objects.equal 和 Objects.hashCode 在早期很有用,但新项目更推荐 JDK java.util.Objects。
MoreObjects.toStringHelper 适合优雅实现 toString()。
ComparisonChain 适合优雅实现多字段 compareTo()。
真正的重点不是工具类本身,而是代码风格:
1 2 3 4 参数非法,尽早失败。 状态错误,明确报错。 对象方法,稳定可靠。 比较逻辑,清晰可读。
如果一个系统的参数校验、对象比较、日志输出都很随意,那么它的复杂业务逻辑再漂亮,也很难稳定。
健壮性不是写在 PPT 里的,它就藏在这些小方法里。
参考资料
Google Guava User Guide: Preconditions.
Google Guava API: Preconditions.
Google Guava API: Objects.
Google Guava API: MoreObjects.
Google Guava API: ComparisonChain.
Java Documentation: java.util.Objects.
Java Documentation: Comparable.
Java Documentation: Comparator.
Joshua Bloch. Effective Java .
启示录 富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。