>  기사  >  Java  >  Java에서 Objects.equals의 함정 예 분석

Java에서 Objects.equals의 함정 예 분석

WBOY
WBOY앞으로
2023-05-04 13:28:06744검색

    1. 범죄 현장

    현재 로그인한 사용자를 확인하고 지정된 시스템 관리자라면 이메일을 보내세요. 시스템 관리자에게는 특별한 필드 ID가 없습니다. 그의 사용자 ID는 888입니다. 이 값은 개발, 테스트 및 프로덕션 환경에서 동일합니다.

    이 요구 사항은 구현하기가 정말 쉽습니다.

    UserInfo userInfo = CurrentUser.getUserInfo();
    
    if(Objects.isNull(userInfo)) {
       log.info("请先登录");
       return;
    }
    
    if(Objects.equals(userInfo.getId(),888)) {
       sendEmail(userInfo):
    }

    현재 로그인된 사용자의 컨텍스트에서 사용자 정보를 가져와서 판단하고, 사용자 정보가 비어 있으면 직접 반환합니다.

    획득한 사용자 정보가 비어 있지 않으면 사용자 ID가 888인지 확인합니다.

    • 888과 같으면 이메일을 보내세요.

    • 888과 같지 않으면 아무 작업도 수행되지 않습니다.

    ID=888의 시스템 관리자 계정으로 로그인한 후 관련 작업을 수행하고 큰 기대를 갖고 이메일을 받을 준비를 했으나 이메일이 외로웠음을 알게 되었습니다.

    나중에 UserInfo 클래스가 다음과 같이 정의되어 있음을 발견했습니다.

    @Data
    public class UserInfo {
        private Long id;
        private String name;
        private Integer age;
        private String address;
    }

    이때 일부 친구는 다음과 같이 말할 수 있습니다. 나는 아무런 문제가 없습니다.

    하지만 제가 말씀드리고 싶은 것은 이 코드에는 실제로 뭔가 문제가 있다는 것입니다.

    무엇이 문제인가요?

    답변: UserInfo 클래스의 멤버 변수 id=888은 Long 유형이고 Objects.equals 메서드 오른쪽에 있는 888은 int 유형입니다. 두 가지가 일치하지 않아 결과가 false로 반환됩니다. Long类型的,而Objects.equals方法右边的888是int类型的,两者不一致,导致返回的结果是false。

    这算哪门子原因?

    答:各位看官,别急,后面会细讲的。

    2. 判断相等的方法

    让我们一起回顾一下,以前判断两个值是否相等的方法有哪些。

    2.1 使用==号

    之前判断两个值是否相等,最快的方法是使用==号。

    int a = 1;
    int b = 1;
    byte c = 1;
    Integer d1 = new Integer(1);
    Integer d2 = new Integer(1);
    System.out.println(a == b); 
    //结果:true
    System.out.println(a == c); 
    //结果:true
    System.out.println(a == d1); 
    //结果:true
    System.out.println(d2 == a); 
    //结果:true
    System.out.println(d1 == d2); 
    //结果:false

    不知道大家有没有发现,java中的基本类型,包含:int、long、short、byte、char、boolean、float、double这8种,可以使用号判断值是否相等。如果出现了基本类型的包装类,比如:Integer,用一个基本类型和一个包装类,使用号也能正确判断,返回true。

    Integer和int比较时,会自动拆箱,这是比较值是否相等。

    但如果有两个包装类,比如:d1和d2,使用==号判断的结果可能是false。

    两个Integer比较时,比较的是它们指向的引用(即内存地址)是否相等。

    还有一个有意思的现象:

    Integer d3 = 1;
    Integer d4 = 1;
    Integer d5 = 128;
    Integer d6 = 128;
    System.out.println(d3 == d4); 
    //结果:true
    System.out.println(d5 == d6); 
    //结果:false

    都是给Integer类型的参数,直接赋值后进行比较。d3和d4判断的结果相等,但d5和d6判断的结果却不相等。

    小伙伴们,下巴惊掉了没?

    答:因为Integer有一个常量池,-128~127直接的Integer数据直接缓存进入常量池。所以1在常量池,而128不在。

    然而,new的Integer对象不适用常量池。从之前d1和d2例子的比较结果,就能看出这一点。

    接下来,看看字符串的判断:

    String e = "abc";
    String f = "abc";
    String g = new String("abc");
    String h = new String("abc");
    System.out.println(e == f); 
    //结果:true
    System.out.println(e == g); 
    //结果:false
    System.out.println(g == h); 
    //结果:false

    普通的字符串变量,使用==号判断,也能返回正确的结果。

    但如果一个普通的字符串变量,跟new出来的字符串对象使用号判断时,返回false。这一点,跟之前说过的用一个基本类型和一个包装类,使用号判断的结果有区别,字符串没有自动拆箱的功能,这一点需要特别注意。

    此外,两个new出来的字符串对象使用==号判断时,也返回false。

    2.2 使用equals方法

    使用上面的==号,可以非常快速判断8种基本数据类型是否相等,除此之外,还能判断两个对象的引用是否相等。

    但现在有个问题,它无法判断两个对象在内存中具体的数据值是否相等,比如:

    String g = new String("abc");
    String h = new String("abc");
    System.out.println(g == h); 
    //结果:false

    字符串对象g和h是两个不同的对象,它们使用==号判断引用是否相等时,返回的是false。

    那么,这种对象不同,但数据值相同的情况,我们该如何判断相等呢?

    答:使用equals

    이게 무슨 이유일까요?

    답변: 독자 여러분, 걱정하지 마세요. 나중에 자세히 설명하겠습니다.

    2. 동등성을 판단하는 방법

    두 값이 같은지 판단하는 데 어떤 방법이 사용되었는지 함께 살펴보겠습니다.

    2.1 == 기호 사용

    두 값이 같은지 확인하는 가장 빠른 방법은 == 기호를 사용하는 것입니다.

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

    Java의 기본 유형에는 int, long, short, byte, char, boolean, float, double이 포함된다는 것을 눈치채셨는지 모르겠습니다. 숫자를 사용하여 값이 같은지 확인할 수 있습니다. . 기본타입과 래퍼클래스를 이용하여 Integer와 같은 기본타입의 래퍼클래스가 나타나면 사용횟수를 정확하게 판단하여 true를 반환한다.

    Integer와 int를 비교할 때 자동으로 unboxing됩니다. 이는 값이 같은지 비교하기 위한 것입니다.

    하지만 d1, d2 등 두 개의 패키징 클래스가 있는 경우 == 기호를 사용한 결과가 false일 수 있습니다.

    두 개의 Integer를 비교할 때 비교는 그들이 가리키는 참조(예: 메모리 주소)가 동일한지 여부입니다.

    또 다른 흥미로운 현상이 있습니다.

    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;
    }

    는 모두 정수 유형의 매개변수이며 직접 할당되고 비교됩니다. d3과 d4의 판정 결과는 동일하지만, d5와 d6의 판정 결과는 동일하지 않습니다. 🎜🎜얘들아 입이 딱 벌어졌지? 🎜🎜답변: Integer에는 상수 풀이 있으므로 -128~127의 직접 정수 데이터가 상수 풀에 직접 캐시됩니다. 따라서 1은 상수 풀에 있지만 128은 그렇지 않습니다. 🎜🎜그러나 새로운 Integer 객체는 상수 풀에 적합하지 않습니다. 이는 앞선 d1과 d2 예시의 비교 결과를 보면 알 수 있다. 🎜🎜다음으로 문자열 판단을 살펴보겠습니다. 🎜
    String e = "abc";
    String f = "abc";
    System.out.println(e.equals(f)); 
    //结果:true
    🎜 일반 문자열 변수는 == 기호를 사용하여 올바른 결과를 반환할 수도 있습니다. 🎜🎜그러나 일반 문자열 변수를 사용하여 new가 생성한 문자열 객체를 판단하면 false가 반환됩니다. 이 점은 앞서 말씀드린 기본 타입과 패키징 클래스 사용, 숫자 판단을 사용한 결과와는 다르며, 문자열에는 자동 언박싱 기능이 없으므로 특별한 주의가 필요합니다. 🎜🎜또한 두 개의 새로운 문자열 객체가 == 기호를 사용하여 판단되면 false도 반환됩니다. 🎜

    2.2 같음 메서드 사용

    🎜위의 == 기호를 사용하면 8가지 기본 데이터 유형이 동일한지 여부를 빠르게 확인할 수 있습니다. 또한 두 객체의 참조가 동일한지 여부도 확인할 수 있습니다. 🎜🎜하지만 이제 문제가 있습니다. 메모리에 있는 두 개체의 특정 데이터 값이 동일한지 확인할 수 없습니다. 예: 🎜
    int a = 1;
    Integer b = new Integer(1);
    Integer c = null;
    System.out.println(a == b); 
    //结果:true
    System.out.println(a == c); 
    //结果:NullPointerException
    🎜문자열 개체 g와 h는 서로 다른 개체입니다. == 참조 여부를 확인하는 기호 동일하면 false가 반환됩니다. 🎜🎜그렇다면 객체는 다르지만 데이터 값은 동일한 경우 어떻게 동등성을 판단할 수 있을까요? 🎜🎜답변: equals 방법을 사용하세요. 🎜🎜equals 메서드는 실제로 Object 클래스의 메서드입니다. 🎜
    String e = null;
    String f = "abc";
    System.out.println(e.equals(f)); 
    //结果:NullPointerException
    🎜 이 메서드는 매우 간단하며 두 개체의 참조가 동일한지 여부만 확인합니다. 🎜🎜물론 문자열 타입이 부모 클래스(즉, Object 클래스)의 equals 메소드를 직접 사용하여 객체는 다르지만 값은 같은 상황을 판단한다면 문제가 발생합니다. 🎜🎜따라서 문자열(예: 문자열 클래스)은 equals 메서드를 다시 구현합니다. 🎜
    String e = null;
    String f = "abc";
    System.out.println(equals(e, f));
    🎜두 객체 참조가 동일한지 먼저 확인하고 동일하면 true를 반환합니다. 다음으로 두 문자열을 문자별로 비교하여 모든 문자가 동일한 경우에만 true를 반환합니다. 🎜🎜좋습니다. g와 h를 판단하는 문제를 해결할 수 있습니다. 🎜
    private static boolean equals(String e, String f) {
        if (e == null) {
            return f == null;
        }
        return e.equals(f);
    }
    🎜 두 문자열 객체가 다르지만 동일한 값을 가지며 true인지 판단하기 위해 String 클래스에서 다시 작성한 equals 메서드를 사용하는 것을 볼 수 있습니다. 반환됩니다. 🎜🎜3. Null 포인터 예외🎜🎜우리는 이미 두 객체가 동일한지 확인하려면 == 기호나 같음 메서드를 사용할 수 있다는 것을 이전 항목에서 알고 있습니다. 🎜🎜하지만 더 깊이 사용하면 문제를 발견하게 됩니다. 즉, 이 두 메서드는 값이 동일한 경우 널 포인터 예외를 보고할 수 있다는 것입니다. 🎜

    先看看==号判断的情况:

    int a = 1;
    Integer b = new Integer(1);
    Integer c = null;
    System.out.println(a == b); 
    //结果:true
    System.out.println(a == c); 
    //结果:NullPointerException

    int和Integer使用==号判断是否相等时,Integer会自动拆箱成int。

    但由于c在自动拆箱的过程中,需要给它赋值int的默认值0。而给空对象,赋值0,必然会报空指针异常。

    接下来,看看equals方法:

    String e = null;
    String f = "abc";
    System.out.println(e.equals(f)); 
    //结果:NullPointerException

    由于字符串对象e是空对象,直接调用它的equals方法时,就会报空指针异常。

    那么,如何解决空指针问题呢?

    答:在代码中判空。

    String e = null;
    String f = "abc";
    System.out.println(equals(e, f));

    我们抽取了一个新的equals方法:

    private static boolean equals(String e, String f) {
        if (e == null) {
            return f == null;
        }
        return e.equals(f);
    }

    该方法可以解决空指针问题,但有没有办法封装一下,变得更通用一下,也适用于Integer或者其他类型的对象比较呢?

    答:有办法,继续往下看。

    4. Objects.equals的作用

    Objects类位于java.util包下,它是里面提供了很多对象操作的辅助方法。

    下面我们重点看看它的equals方法:

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

    equals方法的判断逻辑如下:

    • 该方法先判断对象a和b的引用是否相等,如果相等则直接返回true。

    • 如果引用不相等,则判断a是否为空,如果a为空则返回false。

    • 如果a不为空,调用对象的equals方法进一步判断值是否相等。

    该方法是如何使用的?

    int a = 1;
    int b = 1;
    Integer c = null;
    
    System.out.println(Objects.equals(a, c)); 
    //结果:false
    System.out.println(Objects.equals(c, a)); 
    //结果:false
    System.out.println(Objects.equals(a, b)); 
    //结果:true

    从上面的列子看出,使用Objects.equals方法比较两个对象是否相等,确实可以避免空指针问题。

    但这个有个疑问:前面使用a==b这种方式比较引用是否相等,当时b为空时,程序直接抛了空指针异常。

    而Objects.equals方法内部也使用了a==b比较引用是否相等,为啥它没有抛异常?

    答:因为而Objects类的equals方法,使用了Object类型接收参数,它的默认值是null,不用进行类型转换,也不用像int类型对象赋值默认值0。

    从上面的理论可以看出,如果我们把代码改成这样,也不会抛异常:

    int a = 1;
    Integer c = null;
    System.out.println(equals(a, c));
    //结果:false

    新定义了一个方法:

    private static boolean equals(Object a, Object b) {
        return a == b;
    }

    执行之后发现,确实没有抛空指针了。

    所以Objects.equals方法再比较两个对象是否相等时,确实是一个不错的方法。

    但它有坑,不信继续往下看。

    5. Objects.equals的坑

    各位小伙们看到这里,可能有点心急了,到底是什么坑?

    废话不多说,直接上例子:

    Integer a = 1;
    long b = 1L;
    System.out.println(Objects.equals(a, b));
    //结果:false

    什么?返回结果是false?

    而如果你直接用==号判断:

    Integer a = 1;
    long b = 1L;
    System.out.println(a == b);
    //结果:true

    返回又是true。

    a和b明明都是1,为什么使用Objects.equals方法判断不相等呢?

    这就要从Integer的equals方法说起来了。

    它的equals方法具体代码如下:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

    先判断参数obj是否是Integer类型,如果不是,则直接返回false。如果是Integer类型,再进一步判断int值是否相等。

    而上面这个例子中b是long类型,所以Integer的equals方法直接返回了false。

    也就是说,如果调用了Integer的equals方法,必须要求入参也是Integer类型,否则该方法会直接返回false。

    原来坑在这里!!!

    其实,如果把代码改成这样:

    Integer a = 1;
    long b = 1L;
    System.out.println(Objects.equals(b, a));
    //结果:false

    执行结果也是false。

    因为Long的equals方法代码,跟之前Integer的类似:

    public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }

    也是判断入参,如果不是Long类型,则该方法直接返回false。

    除此之外,还有Byte、Short、Double、Float、Boolean和Character也有类似的equals方法判断逻辑。

    由此可见,我们在使用Objects.equals方法,判断两个值是否相等时,一定要保证两个入参的类型要一致。否则即使两个值相同,但其结果仍然会返回false,这是一个大坑。

    那么,如何解决上面的问题呢?

    可以将参数b的类型强制转换成int。

    Integer a = 1;
    long b = 1L;
    System.out.println(Objects.equals(a, (int)b));
    //结果:true

    或者将参数a的类型强制转换成long。

    Integer a = 1;
    long b = 1L;
    System.out.println(Objects.equals(b, (long)a));
    //结果:true

    有些情况也可以直接用==号判断:

    Integer a = 1;
    long b = 1L;
    System.out.println(a==b);
    //结果:true

    除了Objects.equals方法在两个入参类型不同,而会直接返回false之外,java的8种基本类型包装类的equals也会有相同的问题,需要小伙们特别注意。

    之前,如果直接使用java基本类型包装类的equals方法判断相等。

    Integer a = new Integer(1);
    long b = 1L;
    System.out.println(a.equals(b));

    在idea中,如果你将鼠标放在equals方法上,会出现下面的提示:

    Java에서 Objects.equals의 함정 예 분석

    这时你就知道方法用错了,赶紧修正。但如果直接用包装类的equals方法,有个问题就是可能存在报空指针异常的风险。

    如果你使用Objects.equals方法判断相等,在idea中就并没有错误提示。

    除此之外,我还测试了findBug、sonar等工具,Objects.equals方法两个参数类型不一致的问题,也没有标识出来。

    여러분, 코드를 잠깐 살펴보세요. 혹시 잘못된 점을 발견하셨나요?

    일반적인 함정은 다음과 같습니다.

    • Long 유형과 Integer 유형의 비교(예: 사용자 ID 시나리오)

    • 상태 판단 시나리오와 같은 바이트 유형과 정수 유형의 비교.

    • Double 유형과 Integer 유형의 비교 예: 금액이 0인 판단 시나리오.

    위 내용은 Java에서 Objects.equals의 함정 예 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제