1. 程式人生 > >Java——物件比較

Java——物件比較

前言



本篇部落格主要梳理一下Java中物件比較的需要注意的地方,將分為以下幾個方面進行介紹:

  • ==和equals()方法

  • hashCode()方法和equals()方法

  • Comparator介面和Comparable介面



    ==和equals()方法


    在前面對String介紹時,談到過使用==equals()去比較物件是否相等。

    使用==比較的是兩個物件在記憶體中的地址是否一致,也就是比較兩個物件是否為同一個物件。
    使用equals()方法可以依據物件的值來判定是否相等。

    equals()方法是根類Object的預設方法,檢視Object中equals()的預設實現:

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

    可以看出沒有重寫過的equals()方法和==是一樣的,都是比較兩個物件引用指向的記憶體地址是否一樣判斷兩個物件是否相等

    在介紹String時,我們發現並沒有重寫過equals()方法,但是可以使用equals()正確判斷兩個字串物件是否相等。檢視String原始碼可以發現是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;
    }

    Java中很多類都自身重寫了equals()方法,但是要使我們自定義的物件能正確比較,我們就需要重寫equals()方法。

    public class Student{  
        private String name;
        private int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override  //此關鍵字可以幫助我們檢查是否重寫合乎要求
        public boolean equals(Object obj) {
            if (this == obj)    //檢測this與obj是否指向同一物件。這條語句是一個優化,避免直接去比較同一物件的各個域
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass()) // 比較this和obj是否屬於同一個類 若是兩個物件都不是同一個類的 則不相等
                return false;
    
            Student other = (Student) obj;  //將obj轉換成相應的Student型別
        //對所有需要比較的域進行比較 基本型別使用== 物件域使用equal 陣列型別的域,可以使用靜態的Arrays.equals方法檢測相應的陣列元素是否相等
            if (age != other.age)
                return false;
            if (name == null) {
                if (other.name != null)
                    return false;
            } else if (!name.equals(other.name))
                return false;
            return true;
        }
    
        public static void main(String[] args) {
            Student stu1 = new Student("sakura",20);
            Student stu2 = new Student("sakura",20);
            System.out.println(stu1.equals(stu2));    //output: true
        }
    }

    以上重寫的equals方法是考慮最為全面的,在重寫時最好是照著這種格式來。若是使用eclipse,則有快捷鍵幫助我們自動生成此格式的equals方法。


    hashCode()方法和equals()方法



    可以從上圖中看出,hashCode()equals()是配套自動生成的,為什麼要附加生成hashCode()呢。

    hashCode()是根類Object中的預設方法,檢視JDK:

    hashCode()方法與equals()方法沒有任何關係,hashCode()的存在是為了服務於建立在散列表基礎上的類,如Java集合中的HashMap, HashSet等。hashCode()方法獲取物件的雜湊碼(雜湊碼)。雜湊碼是一個int型的整數,用於確定物件在雜湊表(散列表)中的索引位置

    hashCode()方法會根據不同的物件生成不同的雜湊值,預設情況下為了確保這個雜湊值的唯一性,是通過將該物件的內部地址轉換成一個整數來實現。

    下面我們看一個例子:

    public static void main(String[] args) {
        Student stu1 = new Student("sakura",20);
        Student stu2 = new Student("sakura",20);
        HashSet<Student> stuSet = new HashSet<>();
        stuSet.add(stu1);
        stuSet.add(stu2);
      System.out.println(stu1.equals(stu2));
        System.out.println(stu1);
        System.out.println(stu2);
        System.out.println(stuSet);
    }
    /*
    output:
    true
    [email protected]
    [email protected]
    [[email protected], [email protected]]
    */

    HashSet不會儲存相同的物件。按理來說,stu1和stu2是相等的,不應該被重複放進stuSet裡面。但是結果顯示,出現了重複的物件。

    但是stu1和stu2的hashCode()返回值不同,那麼它們將會被儲存在stuSet中的不同的位置。

    物件儲存在HashSet中時,先會根據物件的雜湊值來檢視是否雜湊表中相應的索引位置是否有物件,若是沒有則直接將物件插入;若是該位置有物件,則使用equals判斷該位置上的物件與待插入的物件是否為相同物件,兩個物件相等則不插入,不相等就將待插入物件掛在已存在物件的後面(就像連結串列一樣掛載)。

    總結來說就是:依據雜湊值找位置,若是該位置沒有物件則直接插入;若是有則比較,相等則不插入,不相等則懸掛在後面。

    所以,要使stu1和stu2不能都被插入stuSet中,則要在Student中重寫hashCode()方法。

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    在hashCode中為什麼加入31這個奇素數來計算雜湊值,總的目的是為了減少雜湊衝突(在同一位置插入多個數)。詳細理由可以參考此篇博文:為什麼在定義hashcode時要使用31這個數呢?

    然後我們在執行一次程式的輸出如下:

    /*
    true
    [email protected]
    [email protected]
    [[email protected]]
    */


    Comparator介面和Comparable介面



    我們使用equals()方法可以實現比較我們自定義類的物件是否相等,但是卻無法得到物件誰大誰小。Java中提供了兩種方式來使得物件可以比較,實現Comparator介面或者Comparable介面。

    Comparable介面

    able結尾的介面都表示擁有某種能力。若是某個自定義類實現了comparable介面,則表示該類的例項化物件擁有可以比較的能力
    實現comparable介面需要覆蓋其中的compareTo()方法(是一個泛型方法)。

    int compareTo(T o)

    返回負數:當前物件小於指定比較的物件;返回0,兩個物件相等;返回正數,當前物件大於指定比較的物件。

    public class Student implements Comparable<Student>{
        private String name;
        private int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
      //重寫comparaTo方法 以age作為標準比較大小
        @Override
        public int compareTo(Student o) {
            return this.age-o.age;//本類接收本類物件,物件可以直接訪問屬性(取消了封裝的形式)
        }
    
        @Override
        public String toString() {
            return "name:" +name + " age:"+age;
        }
    
        public static void main(String[] args) {
            Student stu1 = new Student("sakura",20);
            Student stu2 = new Student("sakura",21);
            Student stu3 = new Student("sakura",19);
        //TreeSet會對插入的物件進行自動排序,所以要求知道物件之間的大小
            TreeSet<Student> stuSet = new TreeSet<>();  
            stuSet.add(stu1);
            stuSet.add(stu2);
            stuSet.add(stu3);
        //使用foreach(), lambda表示式輸出stuSet中的值 forEach()方法從JDK1.8才開始有
            stuSet.forEach(stu->System.out.println(stu));
        }
    }
    /*
    output:
    name:sakura age:19
    name:sakura age:20
    name:sakura age:21
    */

    實現了comparaTo()方法使用age為標準升序排序。也可以以name為標準排序,或者其他自定義的比較依據。

    但是當Student已經實現了以age為依據從小到大排序後,我們又想以name為依據排序,在這個簡單的程式中可以直接將return this.age-o.age變為return this.name.compareTo(o.name)(name為String物件),但是這樣修改類結構會顯得十分麻煩,萬一在以後的程式中遇到的是別人封裝好的類不能直接改類結構又該怎麼辦。

    有沒有其他方便的比較方法,實現物件的大小比較。
    辦法是有的,那就是實現Comparator介面。

    Comparator介面

    實現Comparator介面需要重寫其中的compare()方法(一個泛型方法)。

    int compare(T o1,T o2)

    根據第一個引數小於、等於或大於第二個引數分別返回負整數、零或正整數,通常使用-1, 0, +1表示。

    需要注意,Comparator介面中也有一個equals方法,但是這是判斷該比較器與其他Comparator比較器是否相等。

    public class Student {  
        private String name;
        private int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public static void main(String[] args) {
            Student stu1 = new Student("sakuraamy",20);
            Student stu2 = new Student("sakurabob",21);
            Student stu3 = new Student("sakura",19);
    
            ArrayList<Student> stuList = new ArrayList<>();
            stuList.add(stu1);
            stuList.add(stu2);
            stuList.add(stu3);
    
        //沒有必要去建立一個比較器類 採用內部類的方式實現Comparator介面
            Collections.sort(stuList, new Comparator<Student>() {
                @Override
                public int compare(Student o1, Student o2) {
                    return o1.age-o2.age;
                    //return o1.name.compareTo(o2.name);
                }
            });
        //或者使用lambda表示式
        //Collections.sort(stuList, (o1,o2)->o1.age-o2.age);
            System.out.println(stuList);
        }
    }
    /*
    [name:sakura age:19, name:sakuraamy age:20, name:sakurabob age:21]
    */

    由上可見,實現Comparator介面比較物件比實現Comparable介面簡單和靈活。

    使用這兩個介面比較物件都需要注意幾點:

    • 對稱性:若存在compare(x, y)>0 則 compare(y, x) <0,反之亦然
    • 傳遞性:((compare(x, y)>0) && (compare(y, z)>0)) 可以推匯出compare(x, z)>0
    • 相等替代性:compare(x, y)==0可以推匯出compare(x, z)==compare(y, z)

    小結



    簡單總結一下本篇關於Java中物件比較的內容:要比較自定義類的物件是否相等需要重寫equals()方法;
    當物件要儲存在建立在雜湊表基礎上的集合中時,還需要重寫hashCode()方法用於判定物件在集合中的儲存位置;
    以某種依據比較物件的大小,可以實現Comparable介面或者Comparator介面,前者需要在類中實現表示該類擁有可以比較的能力,後者是在類外實現一個比較器,可以使用多種規則對物件進行比較,更靈活。

    參考博文:https://juejin.im/entry/586c6a6061ff4b006407e2b9