buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

【避坑指南】告别equals,这些姿势助你比较两个对象

tag:慎用Object#equals(obj) | 其实,我并不鼓励使用Object#equals | 告别equals,这些姿势助你比较两个对象

 

🍀先来看看两个小测试

  1. 表达式 user.getUserId().equals(userSign.getUserId()) ,当user#userId与userSign#userId类型不一致时,结果是什么?
  2. 表达式 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,理由是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#contentEquals,或String#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;
}
View Code

 

在比较两个明确的字符串的内容时,使用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。

 

🍀比较枚举直接使用 ==

 我们知道,枚举类中的每个枚举项编译后会被标记为public final static,这限定了每个枚举项实例都是一个常量。同时,从java.lang.Enum重写的equals方法实现可以看到,它也是用==来实现的比较。

# java.lang.Enum 中equals 代码

public final boolean equals(Object other) {
    return this == other;
}

 

🍀不光equals,所有接收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;
    }

 

 

 

▄︻┻┳═一Agenda:

▄︻┻┳═一告别equals,这些姿势助你比较两个对象

▄︻┻┳═一听说,你也一直钟爱着equals。。。 

▄︻┻┳═一用int还是用Integer?

 

【感悟】授人以鱼不如授人以渔?现在反而是授人以鱼要好一些,大家普遍不那么爱思考了。你告诉一个人如何钓鱼,他可能漫不经心,他更喜欢你钓鱼给他。

posted on 2023-09-01 17:58  buguge  阅读(52)  评论(0编辑  收藏  举报