切換語言為:簡體

基於 Java 談談「物件導向」

  • 爱糖宝
  • 2024-05-15
  • 2079
  • 0
  • 0

一、物件導向概述

【概念】

物件導向(Object Oriented),對Java語言來說,一切皆是物件。把現實世界中的物件抽象地體現在程式設計世界中,一個物件代表了某個具體的操作。一個個物件最終組成了完整的程式設計,這些物件可以是獨立存在的,也可以是從別的物件繼承過來的。物件之間透過相互作用傳遞資訊,實現程式開發。

【物件】

所謂物件就是真實世界中的實體,物件與實體是一一對應的,也就是說現實世界中每一個實體都是一個物件,它是一種具體的概念。

【特點】

  1. 物件具有屬性和行為。

  2. 物件具有變化的狀態。

  3. 物件具有唯一性。

  4. 物件都是某個類別的例項。

  5. 一切皆為物件,真實世界中的所有事物都可以視為物件。

【例如】

現實生活中我們需要購物,其中會有顧客、商家、商品等實體,顧客有姓名、住址等屬性,還有下單、付款、退單等操作。這裏的顧客只是抽象的描述成為顧客“類”,但是在你執行購物的時候每一個顧客就是一個具體的物件,比如王老闆去下單了螺獅粉。

二、物件導向核心特性

1、封裝

【概念】

封裝是指把一個物件的狀態資訊(也就是屬性)隱藏在物件內部,不允許外部物件直接訪問。但是可以提供一些可以被外界訪問的方法來操作屬性。

【優點】

優點

說明

提高程式碼安全性

保護類中的資訊,阻止外部程式碼隨意訪問內部屬性。

介面的統一性

透過封裝類和方法實現標準化的介面,提升開發效率。

降低模組耦合

當一個模組的邏輯發生變化時,只要它的介面不變,就不會影響其他系統的使用。

【特點】

Java 語言的基本封裝單位是類,透過封裝可以隱藏內部的複雜實現邏輯,然後提供外部訪問的方法供需要者使用。

2、繼承

【概念】

繼承是指子類擁有父類的全部特徵和行為,這是類之間的一種關係。Java 只支援單繼承,透過繼承可以複用父類的程式碼,以其為基礎可以增加新的功能和資料,也可使用父類的功能。

【優點】

優點

說明

提升開發效率

減少建立類的工作量,子類直接擁有父類的方法和屬性

提高程式碼複用性

複用父類功能,減少程式碼冗餘,提高程式執行效率

提高程式碼的可擴充套件性

更好的實現父類的方法,方便功能擴充套件

【注意事項】

  1. Java使用extends關鍵字實現繼承;

  2. Java只允許單繼承,但可以多層繼承;

  3. 子類無法繼承private 成員;

  4. 構造方法不能被繼承;

  5. 子類透過super使用父類成員,透過this使用本類成員;

3、多型

【概念】

即“一個方法,多個實現”。多型體現在父類中定義的屬性和方法被子類繼承後,可以具有不同的屬性或表現方式。多型性允許一個介面被多個同類使用,彌補了單繼承的不足。

簡單說,就是設定好某個行為,但是不同物件去完成這個行為,得到的結果是不同的。

【分類】

分類

說明

編譯時多型

主要是指方法的過載,透過編譯之後會變成兩個不同的方法。

執行時多型

當傳遞不同類物件時,會動態呼叫對應類中的方法。

【實現條件】

  1. 繼承:在多型中必須存在有繼承關係的子類和父類;

  2. 重寫:子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法;

  3. 父類引用指向子類物件:只有這樣該引用才既能可以呼叫父類的方法,又能呼叫子類的方法。

【特點】

  1. 子類和父類存在同名的成員變數時,訪問的是父類的成員變數;

  2. 子父類存在同名的非靜態成員方法時,訪問的是子類中重寫的方法;

  3. 子父類存在同名的靜態成員變數成員方法時,訪問的是父類的成員函式;

  4. 不能訪問子類獨有的方法(需要進行引用型別轉換)。

【引用型別轉換】

分類

格式

說明

向上轉型

父類 變數名 = new 子類();

將子類的引用賦給父類物件,是自動轉換的

向下轉型

子類 變數名 = (子類)父類變數名;

將父類物件轉化成子類物件,需要進行強轉

【例如】

public class Animal {
    public String name = "animal";
    public void eat() {
        System.out.println("午餐吃肉");
    }
}


public class Dog extends Animal{
    public String name = "dog";
    
    public void run(){
        System.out.println("狗跑的飛起");
    }
    
    @Override
    public void eat() {
        System.out.println("狗吃狗糧");
    }
}
public class FuXing {
    public static void main(String[] args) {
        // 父類型別 物件 = new 子類型別()--向上轉型
        Animal dog = new Dog();
        // 多型中成員變數特點--編譯執行看左邊
        System.out.println(dog.name);
        // 成員方法特點--編譯看左邊,執行看右邊
        dog.eat();
        // 不能訪問子類獨由的方法(須進行強轉,否則報錯)--向下轉型
        ((Dog) dog).run();
        //輸出結果:animal 狗吃狗糧 狗跑的飛起
    }
}


三、instanceof關鍵字

【概述】

在Java中可以使用instanceof關鍵字判斷一個物件是否為一個類(或介面、抽象類、父類)的例項,它可以幫助我們避免出現ClassCastException這樣的異常。

【語法格式】

boolean result = obj instanceof Class;

其中,obj是一個物件,Class表示一個類或介面。obj是class類(或介面)的例項或者子類例項時,結果result返回true,否則返回false。

【用法】

public class FuXing {
    public static void main(String[] args) {
        // 1.宣告一個class類的物件,判斷obj是否為class類的例項物件(很普遍的一種用法)
        String name = "fuxing";
        System.out.println(name instanceof String); // true
        // 2.宣告一個class介面實現類的物件obj,判斷obj是否為class介面實現類的例項物件
        ArrayList arrayList = new ArrayList();
        System.out.println(arrayList instanceof List);  // true
        // 3.obj是class類的直接或間接子類
        Animal a1 = new Animal();
        Dog dog = new Dog();
        Animal a2 = new Dog();
        System.out.println(a1 instanceof Dog);  // false --因為Animal是Dog的父類
        System.out.println(dog instanceof Dog);  // true
        System.out.println(a2 instanceof Dog);  // true
    }
}

四、物件、類和介面

1、物件

【概述】

在物件導向中,類和物件是最基本、最重要的組成單元。類實際上是表示一個客觀世界某類群體的一些基本特徵抽象。物件就是表示一個個具體的東西。所以說類是物件的抽象,物件是類的具體。

【物件建立】

分類

建立方式

格式

顯式

建立

物件

  1. 使用 new 關鍵字

父類型別 變數名 = new 子類型別();

  1. 呼叫java.lang.Class或java.lang.reflect.Constuctor類的newlnstance()例項方法

Class 類物件名稱 = Class.forName(要例項化的類全稱);

類名 物件名 = (類名)Class類物件名稱.newInstance();

  1. 呼叫物件的clone()方法

要例項化的類必須繼承java.lang.Cloneable 介面

類名物件名 = (類名)已建立好的類物件名.clone();

隱含

建立

物件

  1. String name = "fuxing",其中的"fuxing"就是一個String物件,由Java虛擬機器隱含地建立;

  2. 字串的"+"運算子運算的結果為一個新的String物件;

  3. 當Java虛擬機器載入一個類時,會隱含地建立描述這個類的 Class 例項。

【示例】

public class Student implements Cloneable {   
    // 實現 Cloneable 介面
    private String Name;    // 學生名字
    private int age;    // 學生年齡
    public Student(String name,int age) {    
        // 構造方法
        this.Name = name;
        this.age = age;
    }
    public Student() {
        this.Name = "name";
        this.age = 0;
    }
    public String toString() {
        return"學生名字:"+Name+",年齡:"+age;
    }
    public static void main(String[] args)throws Exception {       
        // 使用new關鍵字建立物件
        Student student1 = new Student("小劉",22);
        System.out.println(student1);
       
        // 呼叫 java.lang.Class 的 newInstance() 方法建立物件
        Class c1 = Class.forName("Student");
        Student student2 = (Student)c1.newInstance();
        System.out.println(student2);
        
        // 呼叫物件的 clone() 方法建立物件
        Student student3 = (Student)student2.clone();
        System.out.println(student3);
    }
}

【建立物件步驟】

  1. 給物件分配記憶體;

  2. 將物件的例項變數自動初始化為其變數型別的預設值;

  3. 初始化物件,給例項變數賦予正確的初始值;

【匿名物件】

每次new都相當於開闢了一個新的物件,並開闢了一個新的實體記憶體空間。如果一個物件只需要使用唯一的一次,就可以使用匿名物件,匿名物件還可以作為實際引數傳遞。匿名物件就是沒有明確的給出名字的物件,是物件的一種簡寫形式。一般匿名物件只使用一次,而且匿名物件只在堆記憶體中開闢空間,而不存在棧記憶體的引用。

例如,直接使用了new Animal("Dog")語句,這實際上就是一個匿名物件,與之前宣告的物件不同,此處沒有任何棧記憶體引用它,所以此物件使用一次之後就等待被 GC(垃圾收集機制)回收。

【注意事項】

  1. 每個物件都是相互獨立的,在記憶體中佔有獨立的記憶體地址;

  2. 每個物件都具有自己的生命週期;

  3. 當一個物件的生命週期結束時,物件就變成了垃圾,由 Java 虛擬機器自帶的垃圾回收機制處理。

【物件銷燬】

物件使用完之後需要對其進行清除。物件的清除是指釋放物件佔用的記憶體。在建立物件時,使用者必須使用 new 運算子為物件分配記憶體。不過,在清除物件時,由系統自動進行記憶體回收,不需要使用者額外處理。Java語言的記憶體自動回收稱為垃圾回收(Garbage Collection)機制,簡稱 GC,詳情請看垃圾回收。

【空物件null】

null是一種特殊的引用資料型別,一個引用變數(當變數指向一個物件時,這個變數就被稱為引用變數)沒有透過new分配記憶體空間,這個物件就是空物件,Java使用關鍵字null表示空物件,當試圖呼叫一個空物件的屬性或方法時,會丟擲空指標異常NullPointerException。

2、類

2.1 類的定義

【概述】

類是描述了一組有相同特性(屬性)和相同行為(方法)的一組物件的集合,所有的 Java 程式都是基於類的。

【建立步驟】

  1. 宣告類。編寫類的最外層框架,宣告一個名稱為Animal的類;

  2. 編寫類的屬性。類中的屬性和方法統稱為類成員。其中,類的屬性就是類的資料成員。透過在類的主體中定義變數來描述類所具有的特徵(屬性),這裏宣告的變數稱為類的成員變數;

  3. 編寫類的方法。類的方法描述了類所具有的行為,是類的方法成員。

2.2 抽象類

【概述】

在物件導向的概念中,所有的物件都是透過類來描繪的,但是反過來,並不是所有的類都是用來描繪物件的,如果一個類中沒有包含足夠的資訊來描繪一個具體的物件,那麼這樣的類稱為抽象類。

【抽象方法】

如果一個方法使用abstract來修飾,則說明該方法是抽象方法,抽象方法只有宣告沒有實現。

需要注意的是abstract關鍵字只能用於普通方法,不能用於static方法或者構造方法中,且不能使用private修飾,因為抽象方法必須被子類重寫,而如果使用了private宣告,則子類是無法重寫的。

【抽象方法特徵】

  1. 抽象方法沒有方法體;

  2. 抽象方法必須存在於抽象類中;

  3. 子類重寫父類時,必須重寫父類所有的抽象方法。

【使用規則】

  1. 抽象類和抽象方法都要使用abstract關鍵字宣告;

  2. 如果一個方法被宣告為抽象的,那麼這個類也必須宣告為抽象的。而一個抽象類中,可以有0~n個抽象方法,以及0~n個具體方法;

  3. 抽象類不能例項化,也就是不能使用 new 關鍵字建立物件。

  4. 抽象類繼承普通類的屬性和方法;

  5. 具體類繼承抽象類, 要重寫抽象類的抽象方法;

  6. 抽象類繼承抽象類, 可以重寫也可以選擇不重寫其中的抽象方法。

【與普通類區別】

  1. 普通類不能包含抽象方法,抽象類可以;

  2. 普通類可以直接例項化,抽象類不能;

  3. 抽象類中不一定非要有抽象方法,且抽象類不能被final修飾 (定義抽象類就是讓其他類繼承的,如果修飾final就不能被繼承,這樣就會產生矛盾)。

2.3 內部類

2.3.1 簡介

【概念】

如果在類(命名為Outer)的內部再定義一個類(命名為Inner),此時Inner類就稱為內部類(或稱為巢狀類),而Outer類則稱為外部類(或稱為宿主類)。

【分類】

例項內部類、靜態內部類、區域性內部類、匿名內部類

【特點】

  1. 內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class檔案,但是前面冠以外部類的類名和$符號。

  2. 內部類不能用普通的方式訪問。內部類是外部類的一個成員,因此內部類可以自由地訪問外部類的成員變數,無論是否為private的。

  3. 內部類宣告成靜態的,就不能隨便訪問外部類的成員變數,仍然是隻能訪問外部類的靜態成員變數。

  4. 外部類只有兩種訪問級別:public 和預設;內部類則有 4 種訪問級別:public、protected、 private 和預設。

【示例】

public class Outer {
    public class Inner {
        public int getSum(int x,int y) {
            return x + y;
        }
    }
    public static void main(String[] args) {
        Outer.Inner ti = new Outer().new Inner();
        int i = ti.getSum(2,3);
        System.out.println(i);    // 輸出5
    }
}

2.3.2 例項內部類

【概念】

例項內部類是指沒有用static修飾的內部類,有的地方也稱為非靜態內部類。

【特點】

  1. 在外部類的靜態方法和外部類以外的其他類中,必須透過外部類的例項建立內部類的例項;

  2. 在例項內部類中,可以訪問外部類的所有成員;

  3. 在外部類中不能直接訪問內部類的成員,而必須透過內部類的例項去訪問。如果類 A包含內部類B,類B中包含內部類C,則在類 A 中不能直接訪問類C,而應該透過類B的例項去訪問類C;

  4. 外部類例項與內部類例項是一對多的關係,也就是說一個內部類例項只對應一個外部類例項,而一個外部類例項則可以對應多個內部類例項;

  5. 在例項內部類中不能定義static成員,除非同時使用final和static修飾。

【示例】

public class Outer {
    public int a = 100;
    static int b = 100;
    final int c = 100;
    private int d = 100;
    
    public class Inner {
        // 2.在例項內部類中,可以訪問外部類的所有成員。
        public int getSum() {
            return a + b + c + d;
        }
    }
    public static void main(String[] args) {
        // 1.在外部類的靜態方法和外部類以外的其他類中,必須透過外部類的例項建立內部類的例項。
        Outer.Inner ti = new Outer().new Inner();
        int i = ti.getSum();
        System.out.println(i);    // 輸出400
    }
}

2.3.3 靜態內部類

【概念】

指使用static修飾的內部類。

【特點】

  1. 在建立靜態內部類的例項時,不需要建立外部類的例項;

  2. 靜態內部類中可以定義靜態成員和例項成員。外部類以外的其他類需要透過完整的類名訪問靜態內部類中的靜態成員,需要透過靜態內部類的例項訪問靜態內部類中的例項成員;

  3. 靜態內部類可以直接訪問外部類的靜態成員,如果要訪問外部類的例項成員,則需要透過外部類的例項去訪問。

【示例】

public class Outer {
    int o = 0;    // 例項變數
    static int p = 0;    // 靜態變數
    static class Inner {
        // 2.1靜態內部類中可以定義靜態成員和例項成員。
        int a = 0;    // 例項變數a
        static int b = 0;    // 靜態變數 b
        // 3.靜態內部類可以直接訪問外部類的靜態成員,如果要訪問外部類的例項成員,則需要透過外部類的例項去訪問。
        Outer o = new Outer;
        int a2 = o.o;    // 訪問例項變數
        int b2 =p;    // 訪問靜態變數
    }
}
class OtherClass {
    // 1.在建立靜態內部類的例項時,不需要建立外部類的例項。
    Outer.Inner oi = new Outer.Inner();
    // 2.2外部類以外的其他類需要透過完整的類名訪問靜態內部類中的靜態成員,如果要訪問靜態內部類中的例項成員,則需要透過靜態內部類的例項。
    int a2 = oi.a;    // 訪問例項成員
    int b2 = Outer.Inner.b;    // 訪問靜態成員
}

2.3.4 區域性內部類

【概念】

區域性內部類是指在一個方法中定義的內部類。

【特點】

  1. 區域性內部類與區域性變數一樣,不能使用訪問控制修飾符(public、private 和 protected)和 static 修飾符修飾;

  2. 靜區域性內部類只在當前方法中有效;

public class Outer {
    Inner i = new Inner();    // 編譯出錯
    Outer.Inner ti = new Outer.Inner();    // 編譯出錯
    Outer.Inner ti2 = new Outer().new Inner();    // 編譯出錯
    public void method() {
        class Inner{
        
        }
        Inner i = new Inner();
    }
}
  1. 區域性內部類中不能定義 static 成員;

  2. 區域性內部類中還可以包含內部類,子內部類同特點1;

  3. 在區域性內部類中可以訪問外部類的所有成員。

  4. 區域性內部類中只可以訪問當前方法中 final 型別的引數與變數。(Java8以後可以不加,系統預設加,但不能在被重新賦值)

public class Outer {
    int a = 0;
    int d = 0;
    public void method() {
        int b = 0;
        final int c = 0;
        final int d = 10;
        class Inner {
            int a2 = a;    // 訪問外部類中的成員
            // int b2 = b;    // 編譯出錯
            int c2 = c;    // 訪問方法中的成員
            int d2 = d;    // 訪問方法中的成員
            int d3 = Test.this.d;    //訪問外部類中的成員
        }
        Inner i = new Inner();
        System.out.println(i.d2);    // 輸出10
        System.out.println(i.d3);    // 輸出0
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.method();
    }
}

2.3.5 匿名內部類

【概念】

匿名類是指沒有類名的內部類,必須在建立時使用new語句來宣告類。

【實現方式】

  1. 透過子類繼承父類,重寫其方法;

public class Out {
    void show() {
        System.out.println("呼叫 Out 類的 show() 方法");
    }
    public static void main(String[] args) {
        // 透過匿名內部類建立了一個Out類的子類物件,該子類物件只使用一次。
        Out anonymousClass = new Out() {
            @Override
            void show() {
                System.out.println("子類覆蓋父類方法");
            }
        };
        anonymousClass.show();
    }
}
  1. 實現一個介面(可以是多個),實現其方法。

public interface Out {
    void show();
}


public class AnonymousClass {
    public static void main(String[] args) {
        // 透過匿名內部類建立了一個實現了Out介面的類物件,該類物件只使用一次。
        Out anonymousClass = new Out() {
            @Override
            public void show() {
            }
        };
        anonymousClass.show();
    }
}


【特點】

  1. 匿名類和區域性內部類一樣,可以訪問外部類的所有成員。

  2. 如果匿名類位於一個方法中,則匿名類只能訪問方法中final型別的區域性變數和引數。(Java8以後可以不加,系統預設加,但不能在被重新賦值)

public class Out {
	void show() {
        System.out.println("呼叫 Out 類的 show() 方法");
    }
}
public static void main(String[] args) {
    int a = 5;
    final int b = 10;
    Out anonymousClass = new Out() {
        void show() {
            // JDK1.8 之前編譯出錯
            // System.out.println("呼叫了匿名類的 show() 方法"+a);    
            System.out.println("呼叫了匿名類的 show() 方法"+b);   
        }
    };
    anonymousClass.show();
}
  1. 匿名類中允許使用非靜態程式碼塊進行成員初始化操作。

Out anonyInter = new Out() {
    int i; {    // 非靜態程式碼塊
        i = 10;    //成員初始化
    }
    public void show() {
        System.out.println("呼叫了匿名類的 show() 方法"+i);
    }
};
  1. 匿名類的非靜態程式碼塊會在父類的構造方法之後被執行。

3、介面

3.1 簡介

【概念】

介面是 Java 中最重要的概念之一,它可以被理解為一種特殊的類,不同的是介面的成員沒有執行體,是由全域性常量和公共的抽象方法所組成。定義方式與類基本相同,不過介面定義使用的關鍵字是interface。

【特點】

  1. 具有public訪問控制符的介面,允許任何類使用。沒有指定public的介面,其訪問將侷限於所屬的包;

  2. 方法的宣告不需要其他修飾符,在介面中宣告的方法,將隱式地宣告為公有的public和抽象的abstract;

  3. 在Java介面中宣告的變數其實都是常量,介面中的變數宣告,將隱式地宣告為public、static和final,即常量,所以介面中定義的變數必須初始化;

  4. 介面沒有構造方法,不能被例項化。

3.2 介面和抽象類的對比

【聯絡】

  1. 介面和抽象類都不能被例項化,主要用於被其他類實現和繼承。

  2. 介面和抽象類都可以包含抽象方法,實現介面或繼承抽象類的普通子類都必須實現這些抽象方法。

【區別】

對比維度

抽象類

介面

設計目的

  1. 體現一種模板設計的思想;

  2. 爲了程式碼複用;

  1. 對類進行約束;

  2. 規定呼叫方式和標準;

使用方法

實現方式

子類使用 extends繼承抽象類

子類使用implements實現介面

訪問修飾符

可以用public、protected和default修飾

預設且修飾符只能是public

方法

完全可以包含普通方法

不能為普通方法提供方法實現

變數

普通成員變數、靜態常量均可定義

只能定義靜態常量

構造方法

有,但不是用於建立物件,而是讓其子類呼叫這些構造方法來完成屬於抽象類的初始化操作

沒有構造方法

初始化塊

可以包含初始化塊

不能包含初始化塊

main 方法

可以包含且能執行

不支援

執行速度

較快

較慢

繼承/實現數量

一個類只能有一個直接父類

一個類可以實現多個介面

應用場景

  1. 如果擁有一些方法並且想讓它們有預設實現,則使用抽象類。

  2. 如果基本功能在不斷改變,那麼就需要使用抽象類。

如果想實現多重繼承,那麼必須使用介面

五、常見類總結

1、Object類

1.1 簡介

【概述】

Java 類庫中的一個特殊類,也是所有類的父類,也就是說,Java允許把任何型別的物件賦給Object型別的變數。當一個類被定義後,如果沒有指定繼承的父類,那麼預設父類就是Object類。

【常用方法】

/**
 * 用於返回當前執行時物件的Class物件
 */
public final native Class<?> getClass()
    
/**
 * 用於返回物件的雜湊碼
 */
public native int hashCode()
    
/**
 * 用於比較2個物件的記憶體地址是否相等
 */
public boolean equals(Object obj)
    
/**
 * 用於建立並返回當前物件的一份複製
 */
protected native Object clone() 
    
/**
 * 返回類的名字例項的雜湊碼的16進位制的字串。
 */
public String toString()
    
/**
 * 喚醒一個在此物件監視器上等待的執行緒
 * 如果有多個執行緒在等待只會任意喚醒一個
 */
public final native void notify()
    
/**
 * 喚醒一個在此物件監視器上等待的所有執行緒
 */
public final native void notifyAll()
    
/**
 * 停執行緒的執行。
 * sleep沒有釋放鎖,而wait釋放了鎖 ,timeout是等待時間。
 */
public final native void wait(long timeout) 
    
/**
 * 多了 nanos 引數,這個參數列示額外時間(以毫微秒為單位)。 
 * 所以超時的時間還需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) 
    
/**
 * 跟之前的2個wait方法一樣
 * 只不過該方法一直等待,沒有超時時間這個概念
 */
public final void wait() 
    
/**
 * 例項被垃圾回收器回收的時候觸發的操作
 */
protected void finalize()
1.2 equals()介紹

【概述】

用於比較2個物件的記憶體地址是否相等。

【示例】

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

【使用場景】

  1. 類沒有重寫equals()方法:等價於透過==比較這兩個物件(如示例)。

  2. 類重寫了equals()方法:一般我們都重寫equals()方法來比較兩個物件中的屬性是否相等,若它們的屬性相等,則返回 true。

【與==的區別】

方向

==

equals()

比較內容

基本型別和引用型別的資料均可比較

只能判斷引用型別的資料

  1. 對於基本資料型別,比較的是值

  2. 對於引用資料型別,比較的是物件的記憶體地址

  1. 一般比較的是物件的記憶體地址

  2. 重寫之後可能比較的是物件的值

1.3 HashCode()介紹

【概述】

hashCode()的作用是獲取雜湊碼,也稱為雜湊碼;它實際上是返回一個int整數。這個雜湊碼的作用是確定該物件在雜湊表中的索引位置。

【作用域】

hashCode()在雜湊表中才有用,在其它情況下沒用。在雜湊表中hashCode()的作用是獲取物件的雜湊碼,進而確定該物件在雜湊表中的位置。

【雜湊衝突】

物件Hash的前提是實現equals()和hashCode()兩個方法,那麼HashCode()的作用就是保證物件返回唯一hash值,但當兩個物件計算值一樣時,這就發生了碰撞衝突。當我們對某個元素進行雜湊運算,得到一個儲存地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的雜湊衝突。

【與equals()關係】

  1. 使用equals方法中進行比較的引數沒有修改,那麼多次呼叫的hashCode()方法返回的雜湊值應該是相同的。

  2. 透過equals方法比較是相等的,要求這兩個物件的hashCode方法返回的值也應該是相等的。

  3. 如果兩個物件透過equals方法比較是不同的,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,以提高雜湊表效能。

1.4  為什麼重寫 equals() 時必須重寫 hashCode()

如果插入某個沒有重寫hashCode方法的物件,會導致兩個相等的例項o1和o2返回了不同的雜湊碼。

會違反了“兩個相同物件的hashCode()返回的值也應該是相等”這一約定。

一般對於HashSet、HashMap等基於hash值的類就會出現問題。下面將舉例說明:

以 HashMap 為例,新增物件時,會用當前雜湊表中的元素 p 和新增物件比較。

  1. p 為空:則直接插入;

  2. 兩者 Hash 值相同:透過 equals() 繼續進行比較,相同就替換,不相同就插入。

這樣避免了頻繁呼叫 equals() 問題,詳情見HashMap的底層原理。

public class Point {
	private final int x, y;
    
	public Point(int x, int y) {
	    this.x = x;
	    this.y = y;
	}
	@Override
	public boolean equals(Object obj) {
	    if (this == obj) return true;
	    if (!(obj instanceof Point)) return false;
	    Point point = (Point) obj;
	    return x == point.x && y == point.y;
	}
    
	public static void main(String[] args) {
		Point p1 = new Point(1, 2);
		Point p2 = new Point(1, 2);
		System.out.println(p1.equals(p2));// true
		
		Map<Point, String> map = new HashMap<>();
		map.put(p1, "p1");
        String result = map.get(p2);
        System.out.println(result); // null
	}
}

根據上述程式碼,你可能覺得result應該返回字串 p1, 但是卻返回null, 這是因為Point類並沒有重寫hashCode方法,導致兩個相等的例項p1和p2返回了不同的雜湊碼,違反了hashCode的約定,key不同,自然獲取不到。

@Override
public int hashCode() {
	int result = Integer.hashCode(x);
    result = 31 * result + Integer.hashCode(y);
    return result;
}

重寫hashCode方法後,你會發現result返回的就是字串p1了, 因為hashCode()重寫後,相等的Point例項計算出的雜湊值是相同的。

注意事項:

  1. 當你重寫hashCode方法後,請一定問一下自己是否滿足相等的例項有相同的雜湊碼這一條件。

  2. 重寫的hashCode()中涉及到的屬性應與equals()中保持一致,不要試圖從雜湊碼計算中排除重要的屬性來提高效能。

此外Objects類有一個靜態方法Objects.hash,它接受任意數量的物件併爲它們返回一個雜湊碼。其質量與根據這個專案中的上面編寫的方法相當。

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

參考:

  1. 《Java Guide》

  2. 《C語言中文網--Java教程》

  3. 《為啥子重寫equals方法時一定要重寫hashCode方法》

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.