【避坑指南】告别equals,这些姿势助你比较两个对象
tag:慎用Object#equals(obj) | 其实,我并不鼓励使用Object#equals | 告别equals,这些姿势助你比较两个对象
🍀先来个小测试(答案见文末)
- 表达式
user.getUserId().equals(userSign.getUserId())
,当user#userId类型是String,userSign#userId类型是Long时,结果是什么? - 表达式
user.getUserId().equals(userSign.getUserId())
,当user#userId类型是Integer,userSign#userId类型是Long时,结果是什么? - 表达式
new BigDecimal("0.60").equals(new BigDecimal("0.6"))
的结果是?
🍀anyway,企业应用开发中,我并不鼓励使用equals(Object)
《 阿里巴巴Java开发手册1.1.0》→一、编程规约→(四)OOP规约 →6节中有如下规约:
【强制】Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。
正例: "test".equals(object);
反例: object.equals("test");
说明:推荐使用java.util.Objects#equals (JDK7引入的工具类)
上面规约为我们提供了使用equals规避NPE的避坑指南。
equals
方法因其参数为 Object
,存在类型安全隐患。
应用程序开发中,我其实并不鼓励使用equals,理由是java.lang.Object及其所有派生类的equals的参数类型是Object,无法保证运行期安全。我们复杂的企业级应用,总不可避免的会涉及到代码的重构,而一旦重构数据的数据类型时,equals的弊端就显而易见了,因为equals在编译期无法检测类型的一致性。
例如在语句 if (user.getUserId().equals(userSign.getUserId())) {...} 中,当user和userSign的userId的类型出现不一致,就会致使这个if条件不成立。这个类型不一致,并不影响程序的编译和运行,但这往往会导致业务处理的缺陷。也就是说,这个业务缺陷在编译期和运行时无法被我们发现出来。
基于此,在业务代码中,尽量不要使用Object作为方法/函数的参数,也尽量不要调用那些参数类型是Object的方法/函数。例如,上面阿里规范提到的java.util.Objects#equals(Object,Object)。
因此,我们有必要使用其他方式取代equals来判断两个对象的内容是否相同。
🍀比较数字类型(java.lang.Number),优先使用 == 来比较基本类型。
下面是用 != 比较两个Integer对象,IDE给出提示。
IDE提示用equals来比较。但最好的方式是用 != 来比较基本类型。
if (riskCompanyEmployee.getType().intValue() != riskCompanyMap.get(dto.getIdCard()).getType())
支付结算系统中,如果金额的类型是BigDecimal,那在比较时,可要注意 new BigDecimal("0.60").equals(new BigDecimal("0.6")) 的结果是false。最佳实践是,涉及BigDecimal金额比较时,可以转换成基本数字来比较。例如元转分后对两个int进行比较。
🍀比较String字符串选择equals/contentEquals/equalsIgnoreCase?最佳实践来了!
java.lang.String 的 #contentEquals 和 #equalsIgnoreCase 这2个方法都接收明确的字符串。
/****** ======= java.lang.String中的字符串比较函数 ======= ******/ public boolean equalsIgnoreCase(String anotherString) { return (this == anotherString) ? true : (anotherString != null) && (anotherString.value.length == value.length) && regionMatches(true, 0, anotherString, 0, value.length); } public boolean contentEquals(@NotNull CharSequence cs) { // Argument is a StringBuffer, StringBuilder if (cs instanceof AbstractStringBuilder) { if (cs instanceof StringBuffer) { synchronized(cs) { return nonSyncContentEquals((AbstractStringBuilder)cs); } } else { return nonSyncContentEquals((AbstractStringBuilder)cs); } } // Argument is a String if (cs instanceof String) { return equals(cs); } // Argument is a generic CharSequence char v1[] = value; int n = v1.length; if (n != cs.length()) { return false; } for (int i = 0; i < n; i++) { if (v1[i] != cs.charAt(i)) { return false; } } return true; } public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
在比较两个明确的字符串的内容时,使用String#contentEquals来代替String#equals。如果涉及到忽略大小写,则使用String#equalsIgnoreCase来代替String#equals。
案例:下面含有equals的代码,IDE会提示你使用String#equalsIgnoreCase。重构后的代码更易读→ if(!captcha.equalsIgnoreCase(checkCode.toString()))
同样,比较字符串还可以借助诸如下面apache-common的工具方法。
org.apache.commons.lang3.StringUtils.equals(final CharSequence cs1, final CharSequence cs2)
当然,对于字符串常量,你依然可以使用 == 来比较。
需要说明的是 String#contentEquals 要求入参不为null,这在某些情况下可能会产生NPE异常。
那么,String#contentEquals 或 String#equals,当如何权衡选择呢?
我建议的最佳实践
我建议的最佳实践是:取决于你比较的数据所表达的业务含义。如果明确是String字符串,例如用户身份证号、用户姓名、快递收件人详细住址、不妨直接用 String#equals 来比较。 如果是下面情况的数据,请考虑 contentEquals(同时,程序要保证变量不是null),既满足可理解性、可读性,又可规避不必要的潜在bug↓↓↓
- 可能会被理解为String也可能会被理解为Long或其他类型的数据。例如 订单号 -orderNo/orderId、用户id - userId、一些数据的code - bankCode/channelCode。
- 可能会由String重构为其他类型的数据。如 系统里的 userId 是 String,可能会被重构为 Long。系统里 int类型的 bankCode,可能会被重构为 String。
- 复杂业务系统同时存在 String 和 Long 的数据。 如 系统里一部分服务的 userId 是 String,另一部分服务的 userId 是 Long。
🍀比较enum(枚举)直接使用 ==
enum(枚举)是在Java 5中引入的特性,用于增强类型安全。
首先,枚举确保变量只能被赋予定义中包含的值之一,避免了类型错误。
其次,枚举是一种特殊的类,枚举类中的每个值(枚举实例)编译后会被标记为public final static,由此,我们也称枚举类中的枚举值为枚举常量,每个枚举常量在JVM中是唯一的。因此,可以通过 == 来比较两个枚举值,这一点,从java.lang.Enum重写的equals方法源码也可以看出来。
# java.lang.Enum 中equals 代码 public final boolean equals(Object other) { return this == other; }
🍀不光equals(Object),所有接收Object参数的方法,都应该慎用!
hutool-all-4.5.11.jar工具包StrUtil.java里封装了如下isBlankIfStr、isEmptyIfStr工具方法。这些入参是Object的工具方法,在企业应用开发者中,与equals(Object)方法一样,都应该慎用。以StrUtil#isBlankIfStr(Object)来举例,StrUtil.isBlankIfStr(orderNo); 当orderNo是空串时返回true。而当orderNo重构为数字类型后,在orderNo=0的情况下,此方法将会返回false。这可能不符合实际的业务判断逻辑。
public static boolean isBlankIfStr(Object obj) { if (null == obj) { return true; } else if (obj instanceof CharSequence) { return isBlank((CharSequence) obj); } return false; } public static boolean isEmptyIfStr(Object obj) { if (null == obj) { return true; } else if (obj instanceof CharSequence) { return 0 == ((CharSequence) obj).length(); } return false; }
🍀答案
- 表达式
user.getUserId().equals(userSign.getUserId())
,当user#userId类型是String,userSign#userId类型是Long时,结果是什么? -->false - 表达式
user.getUserId().equals(userSign.getUserId())
,当user#userId类型是Integer,userSign#userId类型是Long时,结果是什么?-->false - 表达式
new BigDecimal("0.60").equals(new BigDecimal("0.6"))
的结果是?-->false
▄︻┻┳═一Agenda:
▄︻┻┳═一告别equals,这些姿势助你比较两个对象
▄︻┻┳═一听说,你也一直钟爱着equals。。。
▄︻┻┳═一用int还是用Integer?
【感悟】授人以鱼不如授人以渔?现在反而是授人以鱼要好一些,大家普遍不那么爱思考了。你告诉一个人如何钓鱼,他可能漫不经心,他更喜欢你钓鱼给他。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/17666701.html