ホームページ  >  記事  >  Java  >  Java における Objects.equals の落とし穴例の分析

Java における Objects.equals の落とし穴例の分析

WBOY
WBOY転載
2023-05-04 13:28:06743ブラウズ

    1. 犯罪現場

    次のような要件があるとします。現在ログインしているユーザーを特定し、それが指定したシステム管理者である場合は、送信します。 Eメール。システム管理者には特別なフィールド 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# 型です。 ##. この 2 つは矛盾しています。返される結果は false です。 これはどのような理由でしょうか?

    回答: 読者の皆様、後ほど詳しく説明しますので、ご心配なく。

    2. 等しいかどうかを判断する方法

    2 つの値が等しいかどうかを判断するためにどのような方法が使用されているかを一緒に確認してみましょう。

    2.1 == 記号を使用する

    2 つの値が等しいかどうかを判断する前に、最も早い方法は == 記号を使用することです。

    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 があることをご存知かどうかはわかりません。数値を使用して、値が次のとおりであるかどうかを判断できます。等しいです。基本型とラッパークラスを使用して、Integerなどの基本型のラッパークラスが出現した場合、正しく使用回数を判定でき、trueを返します。

    Integer と int を比較する場合、比較値が等しいかどうかを判断するために、それらは自動的にボックス化されません。

    ただし、d1 と d2 などの 2 つのパッケージング クラスがある場合、== 記号を使用した結果は false になる可能性があります。

    2 つの整数を比較する場合、それらが指す参照 (つまり、メモリ アドレス) が等しいかどうかが比較されます。

    もう 1 つの興味深い現象があります。

    Integer d3 = 1;
    Integer d4 = 1;
    Integer d5 = 128;
    Integer d6 = 128;
    System.out.println(d3 == d4); 
    //结果:true
    System.out.println(d5 == d6); 
    //结果:false
    はすべて整数型のパラメーターであり、直接割り当てられて比較されます。 d3とd4の判定結果は等しいですが、d5とd6の判定結果は等しくありません。

    友よ、びっくりしてる?

    回答: Integer には定数プールがあるため、-128 から 127 までの直接の Integer データは定数プールに直接キャッシュされます。つまり、1 は定数プールにありますが、128 はありません。

    ただし、新しい 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 が返されます。この点は、先ほど述べた基本型とパッケージングクラスの使用、数値判定による結果とは異なり、文字列には自動アンボックス機能がないため注意が必要です。

    また、新たな2つの文字列オブジェクトを==記号で判定した場合もfalseが返されます。

    2.2 等号メソッドを使用する

    上記の == 記号を使用すると、8 つの基本データ型が等しいかどうかをすぐに判断できます。また、2 つのオブジェクトの参照が等しいかどうかも判断できます。は同じ。 。

    しかし、問題が発生しました。メモリ内の 2 つのオブジェクトの特定のデータ値が等しいかどうかを判断できません。例:

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

    String オブジェクト g と h 2 つの異なるオブジェクトであるため、== 記号を使用して参照が等しいかどうかを判断すると、false が返されます。

    では、オブジェクトは異なっていてもデータ値が同じ場合、どうやって等しいと判断するのでしょうか?

    回答:

    equals

    メソッドを使用します。

    equals メソッドは、実際には Object クラスのメソッドです。

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

    このメソッドは非常に単純で、2 つのオブジェクトの参照が等しいかどうかを判断するだけです。

    文字列型が親クラス(つまりオブジェクトクラス)のequalsメソッドを直接使用して、オブジェクトが異なるが値が同じである状況を判断すると、明らかに問題が発生します。

    したがって、文字列 (つまり String クラス) は、equals メソッドを再実装します。

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

    やはり、最初に 2 つのオブジェクト参照が等しいかどうかを判断し、等しい場合は true を返します。 。次に、2 つの文字列が文字ごとに比較され、すべての文字が等しい場合にのみ true が返されます。

    いいですね、これで g と h を判断する問題を解決できます:

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

    String クラスによって書き換えられた equals メソッドを使用して、2 つの文字列オブジェクトが異なると判断していることがわかります。 , ただし、値が同じ場合はtrueが返されます。

    3. Null ポインターの例外

    これまでの説明から、2 つのオブジェクトが等しいかどうかを判断するには、== 記号または equals メソッドを使用できることがわかりました。

    しかし、これらをより深く使用すると、問題が見つかります。つまり、これら 2 つのメソッドが値が等しいと判断した場合、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 シナリオ)。

    • Byte 型と Integer 型の比較 (例: ステータス判定シナリオ)。

    • Double 型と Integer 型の比較。例: 金額が 0 の場合の判定シナリオ。

    以上がJava における Objects.equals の落とし穴例の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。