1. 程式人生 > >夯實Java基礎系列9:深入理解Class類和Object類

夯實Java基礎系列9:深入理解Class類和Object類

目錄

  • Java中Class類及用法
    • Class類原理
    • 如何獲得一個Class類物件
    • 使用Class類的物件來生成目標類的例項
  • Object類
    • 類構造器public Object();
    • registerNatives()方法;
    • Clone()方法實現淺拷貝
    • getClass()方法
    • equals()方法
    • hashCode()方法;
    • toString()方法
    • wait() notify() notifAll()
    • finalize()方法
  • CLass類和Object類的關係
  • 參考文章
  • 微信公眾號
    • Java技術江湖
    • 個人公眾號:黃小斜

- Object類

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯絡作者。
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯絡作者,歡迎你參與本系列博文的創作和修訂。

Java中Class類及用法

Java程式在執行時,Java執行時系統一直對所有的物件進行所謂的執行時型別標識,即所謂的RTTI。

這項資訊紀錄了每個物件所屬的類。虛擬機器通常使用執行時型別資訊選準正確方法去執行,用來儲存這些型別資訊的類是Class類。Class類封裝一個物件和介面執行時的狀態,當裝載類時,Class型別的物件自動建立。

說白了就是:

Class類也是類的一種,只是名字和class關鍵字高度相似。Java是大小寫敏感的語言。

Class類的物件內容是你建立的類的型別資訊,比如你建立一個shapes類,那麼,Java會生成一個內容是shapes的Class類的物件

Class類的物件不能像普通類一樣,以 new shapes() 的方式建立,它的物件只能由JVM建立,因為這個類沒有public建構函式

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
     //私有構造方法,只能由jvm進行例項化
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

Class類的作用是執行時提供或獲得某個物件的型別資訊,和C++中的typeid()函式類似。這些資訊也可用於反射。

Class類原理

看一下Class類的部分原始碼

//Class類中封裝了型別的各種資訊。在jvm中就是通過Class類的例項來獲取每個Java類的所有資訊的。

public class Class類 {
    Class aClass = null;

//    private EnclosingMethodInfo getEnclosingMethodInfo() {
//        Object[] enclosingInfo = getEnclosingMethod0();
//        if (enclosingInfo == null)
//            return null;
//        else {
//            return new EnclosingMethodInfo(enclosingInfo);
//        }
//    }

    /**提供原子類操作
     * Atomic operations support.
     */
//    private static class Atomic {
//        // initialize Unsafe machinery here, since we need to call Class.class instance method
//        // and have to avoid calling it in the static initializer of the Class class...
//        private static final Unsafe unsafe = Unsafe.getUnsafe();
//        // offset of Class.reflectionData instance field
//        private static final long reflectionDataOffset;
//        // offset of Class.annotationType instance field
//        private static final long annotationTypeOffset;
//        // offset of Class.annotationData instance field
//        private static final long annotationDataOffset;
//
//        static {
//            Field[] fields = Class.class.getDeclaredFields0(false); // bypass caches
//            reflectionDataOffset = objectFieldOffset(fields, "reflectionData");
//            annotationTypeOffset = objectFieldOffset(fields, "annotationType");
//            annotationDataOffset = objectFieldOffset(fields, "annotationData");
//        }

        //提供反射資訊
    // reflection data that might get invalidated when JVM TI RedefineClasses() is called
//    private static class ReflectionData<T> {
//        volatile Field[] declaredFields;
//        volatile Field[] publicFields;
//        volatile Method[] declaredMethods;
//        volatile Method[] publicMethods;
//        volatile Constructor<T>[] declaredConstructors;
//        volatile Constructor<T>[] publicConstructors;
//        // Intermediate results for getFields and getMethods
//        volatile Field[] declaredPublicFields;
//        volatile Method[] declaredPublicMethods;
//        volatile Class<?>[] interfaces;
//
//        // Value of classRedefinedCount when we created this ReflectionData instance
//        final int redefinedCount;
//
//        ReflectionData(int redefinedCount) {
//            this.redefinedCount = redefinedCount;
//        }
//    }
        //方法陣列
//    static class MethodArray {
//        // Don't add or remove methods except by add() or remove() calls.
//        private Method[] methods;
//        private int length;
//        private int defaults;
//
//        MethodArray() {
//            this(20);
//        }
//
//        MethodArray(int initialSize) {
//            if (initialSize < 2)
//                throw new IllegalArgumentException("Size should be 2 or more");
//
//            methods = new Method[initialSize];
//            length = 0;
//            defaults = 0;
//        }

    //註解資訊
    // annotation data that might get invalidated when JVM TI RedefineClasses() is called
//    private static class AnnotationData {
//        final Map<Class<? extends Annotation>, Annotation> annotations;
//        final Map<Class<? extends Annotation>, Annotation> declaredAnnotations;
//
//        // Value of classRedefinedCount when we created this AnnotationData instance
//        final int redefinedCount;
//
//        AnnotationData(Map<Class<? extends Annotation>, Annotation> annotations,
//                       Map<Class<? extends Annotation>, Annotation> declaredAnnotations,
//                       int redefinedCount) {
//            this.annotations = annotations;
//            this.declaredAnnotations = declaredAnnotations;
//            this.redefinedCount = redefinedCount;
//        }
//    }
}

我們都知道所有的java類都是繼承了object這個類,在object這個類中有一個方法:getclass().這個方法是用來取得該類已經被例項化了的物件的該類的引用,這個引用指向的是Class類的物件。

我們自己無法生成一個Class物件(建構函式為private),而 這個Class類的物件是在當各類被調入時,由 Java 虛擬機器自動建立 Class 物件,或通過類裝載器中的 defineClass 方法生成。

//通過該方法可以動態地將位元組碼轉為一個Class類物件
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}

我們生成的物件都會有個欄位記錄該物件所屬類在CLass類的物件的所在位置。如下圖所示:

[外鏈圖片轉存失敗(img-ZfMJTzO4-1569074134147)(http://dl.iteye.com/upload/picture/pic/101542/0047a6e9-6608-3c3c-a67c-d8ee95e7fcb8.jpg)]

如何獲得一個Class類物件

請注意,以下這些方法都是值、指某個類對應的Class物件已經在堆中生成以後,我們通過不同方式獲取對這個Class物件的引用。而上面說的DefineClass才是真正將位元組碼載入到虛擬機器的方法,會在堆中生成新的一個Class物件。

第一種辦法,Class類的forName函式

public class shapes{}
Class obj= Class.forName("shapes");
第二種辦法,使用物件的getClass()函式

public class shapes{}
shapes s1=new shapes();
Class obj=s1.getClass();
Class obj1=s1.getSuperclass();//這個函式作用是獲取shapes類的父類的型別

第三種辦法,使用類字面常量

Class obj=String.class;
Class obj1=int.class;
注意,使用這種辦法生成Class類物件時,不會使JVM自動載入該類(如String類)。==而其他辦法會使得JVM初始化該類。==

使用Class類的物件來生成目標類的例項

生成不精確的object例項

==獲取一個Class類的物件後,可以用 newInstance() 函式來生成目標類的一個例項。然而,該函式並不能直接生成目標類的例項,只能生成object類的例項==

Class obj=Class.forName("shapes");
Object ShapesInstance=obj.newInstance();
使用泛化Class引用生成帶型別的目標例項

Class

Class obj1=int.class;
Class<Integer> obj2=int.class;
obj1=double.class;
//obj2=double.class; 這一行程式碼是非法的,obj2不能改指向別的類

然而,有個靈活的用法,使得你可以用Class的物件指向基類的任何子類。
Class<? extends Number> obj=int.class;
obj=Number.class;
obj=double.class;

因此,以下語法生成的Class物件可以指向任何類。
Class<?> obj=int.class;
obj=double.class;
obj=shapes.class;
最後一個奇怪的用法是,當你使用這種泛型語法來構建你手頭有的一個Class類的物件的基類物件時,必須採用以下的特殊語法

public class shapes{}
class round extends shapes{}
Class<round> rclass=round.class;
Class<? super round> sclass= rclass.getSuperClass();
//Class<shapes> sclass=rclass.getSuperClass();
我們明知道,round的基類就是shapes,但是卻不能直接宣告 Class < shapes >,必須使用特殊語法

Class < ? super round >

這個記住就可以啦。

Object類

這部分主要參考http://ihenu.iteye.com/blog/2233249

Object類是Java中其他所有類的祖先,沒有Object類Java面向物件無從談起。作為其他所有類的基類,Object具有哪些屬性和行為,是Java語言設計背後的思維體現。

Object類位於java.lang包中,java.lang包包含著Java最基礎和核心的類,在編譯時會自動匯入。Object類沒有定義屬性,一共有13個方法,13個方法之中並不是所有方法都是子類可訪問的,一共有9個方法是所有子類都繼承了的。

先大概介紹一下這些方法

1.clone方法
保護方法,實現物件的淺複製,只有實現了Cloneable接口才可以呼叫該方法,否則丟擲CloneNotSupportedException異常。
2.getClass方法
final方法,獲得執行時型別。
3.toString方法
該方法用得比較多,一般子類都有覆蓋。
4.finalize方法
該方法用於釋放資源。因為無法確定該方法什麼時候被呼叫,很少使用。
5.equals方法
該方法是非常重要的一個方法。一般equals和==是不一樣的,但是在Object中兩者是一樣的。子類一般都要重寫這個方法。
6.hashCode方法
該方法用於雜湊查詢,重寫了equals方法一般都要重寫hashCode方法。這個方法在一些具有雜湊功能的Collection中用到。
一般必須滿足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就滿足equals。不過為了提高效率,應該儘量使上面兩個條件接近等價。
7.wait方法
wait方法就是使當前執行緒等待該物件的鎖,當前執行緒必須是該物件的擁有者,也就是具有該物件的鎖。wait()方法一直等待,直到獲得鎖或者被中斷。wait(long timeout)設定一個超時間隔,如果在規定時間內沒有獲得鎖就返回。
呼叫該方法後當前執行緒進入睡眠狀態,直到以下事件發生。
(1)其他執行緒呼叫了該物件的notify方法。
(2)其他執行緒呼叫了該物件的notifyAll方法。
(3)其他執行緒呼叫了interrupt中斷該執行緒。
(4)時間間隔到了。
此時該執行緒就可以被排程了,如果是被中斷的話就丟擲一個InterruptedException異常。
8.notify方法
該方法喚醒在該物件上等待的某個執行緒。
9.notifyAll方法
該方法喚醒在該物件上等待的所有執行緒。

類構造器public Object();

大部分情況下,Java中通過形如 new A(args..)形式建立一個屬於該型別的物件。其中A即是類名,A(args..)即此類定義中相對應的建構函式。通過此種形式建立的物件都是通過類中的建構函式完成。

為體現此特性,Java中規定:在類定義過程中,對於未定義建構函式的類,預設會有一個無引數的建構函式,作為所有類的基類,Object類自然要反映出此特性,在原始碼中,未給出Object類建構函式定義,但實際上,此建構函式是存在的。

當然,並不是所有的類都是通過此種方式去構建,也自然的,並不是所有的類建構函式都是public。

registerNatives()方法;

private static native void registerNatives();

registerNatives函式前面有native關鍵字修飾,Java中,用native關鍵字修飾的函式表明該方法的實現並不是在Java中去完成,而是由C/C++去完成,並被編譯成了.dll,由Java去呼叫。

方法的具體實現體在dll檔案中,對於不同平臺,其具體實現應該有所不同。用native修飾,即表示作業系統,需要提供此方法,Java本身需要使用。

具體到registerNatives()方法本身,其主要作用是將C/C++中的方法對映到Java中的native方法,實現方法命名的解耦。

既然如此,可能有人會問,registerNatives()修飾符為private,且並沒有執行,作用何以達到?其實,在Java原始碼中,此方法的聲明後有緊接著一段靜態程式碼塊:

private static native void registerNatives();  
static {  
     registerNatives();  
}  

Clone()方法實現淺拷貝

protected native Object clone() throwsCloneNotSupportedException;

看,clode()方法又是一個被宣告為native的方法,因此,我們知道了clone()方法並不是Java的原生方法,具體的實現是有C/C++完成的。clone英文翻譯為"克隆",其目的是建立並返回此物件的一個副本。

形象點理解,這有一輛科魯茲,你看著不錯,想要個一模一樣的。你呼叫此方法即可像變魔術一樣變出一輛一模一樣的科魯茲出來。配置一樣,長相一樣。但從此刻起,原來的那輛科魯茲如果進行了新的裝飾,與你克隆出來的這輛科魯茲沒有任何關係了。

你克隆出來的物件變不變完全在於你對克隆出來的科魯茲有沒有進行過什麼操作了。Java術語表述為:clone函式返回的是一個引用,指向的是新的clone出來的物件,此物件與原物件分別佔用不同的堆空間。

明白了clone的含義後,接下來看看如果呼叫clone()函式物件進行此克隆操作。

首先看一下下面的這個例子:

package com.corn.objectsummary;  
  
import com.corn.Person;  
  
public class ObjectTest {  
  
    public static void main(String[] args) {  
  
        Object o1 = new Object();  
        // The method clone() from the type Object is not visible  
        Object clone = o1.clone();  
    }  
  
}  

例子很簡單,在main()方法中,new一個Oject物件後,想直接呼叫此物件的clone方法克隆一個物件,但是出現錯誤提示:"The method clone() from the type Object is not visible"

why? 根據提示,第一反應是ObjectTest類中定義的Oject物件無法訪問其clone()方法。回到Object類中clone()方法的定義,可以看到其被宣告為protected,估計問題就在這上面了,protected修飾的屬性或方法表示:在同一個包內或者不同包的子類可以訪問。

顯然,Object類與ObjectTest類在不同的包中,但是ObjectTest繼承自Object,是Object類的子類,於是,現在卻出現子類中通過Object引用不能訪問protected方法,原因在於對"不同包中的子類可以訪問"沒有正確理解。

"不同包中的子類可以訪問",是指當兩個類不在同一個包中的時候,繼承自父類的子類內部且主調(呼叫者)為子類的引用時才能訪問父類用protected修飾的成員(屬性/方法)。 在子類內部,主調為父類的引用時並不能訪問此protected修飾的成員。!(super關鍵字除外)

於是,上例改成如下形式,我們發現,可以正常編譯:

    public class clone方法 {
    public static void main(String[] args) {

    }
    public void test1() {

        User user = new User();
//        User copy = user.clone();
    }
    public void test2() {
        User user = new User();
//        User copy = (User)user.clone();
    }
}

是的,因為此時的主調已經是子類的引用了。

上述程式碼在執行過程中會丟擲"java.lang.CloneNotSupportedException",表明clone()方法並未正確執行完畢,問題的原因在與Java中的語法規定:

clone()的正確呼叫是需要實現Cloneable介面,如果沒有實現Cloneable介面,並且子類直接呼叫Object類的clone()方法,則會丟擲CloneNotSupportedException異常。

Cloneable介面僅是一個表示介面,介面本身不包含任何方法,用來指示Object.clone()可以合法的被子類引用所呼叫。

於是,上述程式碼改成如下形式,即可正確指定clone()方法以實現克隆。

public class User implements Cloneable{
public int id;
public String name;
public UserInfo userInfo;

public static void main(String[] args) {
    User user = new User();
    UserInfo userInfo = new UserInfo();
    user.userInfo = userInfo;
    System.out.println(user);
    System.out.println(user.userInfo);
    try {
        User copy = (User) user.clone();
        System.out.println(copy);
        System.out.println(copy.userInfo);
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
}
//拷貝的User例項與原來不一樣,是兩個物件。
//    com.javase.Class和Object.Object方法.用到的類.User@4dc63996
//    com.javase.Class和Object.Object方法.用到的類.UserInfo@d716361
        //而拷貝後物件的userinfo引用物件是同一個。
    //所以這是淺拷貝
//    com.javase.Class和Object.Object方法.用到的類.User@6ff3c5b5
//    com.javase.Class和Object.Object方法.用到的類.UserInfo@d716361
}

總結:
clone方法實現的是淺拷貝,只拷貝當前物件,並且在堆中分配新的空間,放這個複製的物件。但是物件如果裡面有其他類的子物件,那麼就不會拷貝到新的物件中。

==深拷貝和淺拷貝的區別==

淺拷貝
淺拷貝是按位拷貝物件,它會建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。如果屬性是基本型別,拷貝的就是基本型別的值;如果屬性是記憶體地址(引用型別),拷貝的就是記憶體地址 ,因此如果其中一個物件改變了這個地址,就會影響到另一個物件。

深拷貝
深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。
現在為了要在clone物件時進行深拷貝, 那麼就要Clonable介面,覆蓋並實現clone方法,除了呼叫父類中的clone方法得到新的物件, 還要將該類中的引用變數也clone出來。如果只是用Object中預設的clone方法,是淺拷貝的。

那麼這兩種方式有什麼相同和不同呢?

new操作符的本意是分配記憶體。程式執行到new操作符時, 首先去看new操作符後面的型別,因為知道了型別,才能知道要分配多大的記憶體空間。

分配完記憶體之後,再呼叫建構函式,填充物件的各個域,這一步叫做物件的初始化,構造方法返回後,一個物件建立完畢,可以把他的引用(地址)釋出到外部,在外部就可以使用這個引用操縱這個物件。

而clone在第一步是和new相似的, 都是分配記憶體,呼叫clone方法時,分配的記憶體和源物件(即呼叫clone方法的物件)相同,然後再使用原物件中對應的各個域,填充新物件的域,

填充完成之後,clone方法返回,一個新的相同的物件被建立,同樣可以把這個新物件的引用釋出到外部。

==也就是說,一個物件在淺拷貝以後,只是把物件複製了一份放在堆空間的另一個地方,但是成員變數如果有引用指向其他物件,這個引用指向的物件和被拷貝的物件中引用指向的物件是一樣的。當然,基本資料型別還是會重新拷貝一份的。==

getClass()方法

4.public final native Class<?> getClass();

getClass()也是一個native方法,返回的是此Object物件的類物件/執行時類物件Class<?>。效果與Object.class相同。

首先解釋下"類物件"的概念:在Java中,類是是對具有一組相同特徵或行為的例項的抽象並進行描述,物件則是此類所描述的特徵或行為的具體例項。

作為概念層次的類,其本身也具有某些共同的特性,如都具有類名稱、由類載入器去載入,都具有包,具有父類,屬性和方法等。

於是,Java中有專門定義了一個類,Class,去描述其他類所具有的這些特性,因此,從此角度去看,類本身也都是屬於Class類的物件。為與經常意義上的物件相區分,在此稱之為"類物件"。

public class getClass方法 {
    public static void main(String[] args) {
        User user = new User();
        //getclass方法是native方法,可以取到堆區唯一的Class<User>物件
        Class<?> aClass = user.getClass();
        Class bClass = User.class;
        try {
            Class cClass = Class.forName("com.javase.Class和Object.Object方法.用到的類.User");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(aClass);
        System.out.println(bClass);
//        class com.javase.Class和Object.Object方法.用到的類.User
//        class com.javase.Class和Object.Object方法.用到的類.User
        try {
            User a = (User) aClass.newInstance();

        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
} 

此處主要大量涉及到Java中的反射知識

equals()方法

5.public boolean equals(Object obj);

與equals在Java中經常被使用,大家也都知道與equals的區別:

==表示的是變數值完成相同(對於基礎型別,地址中儲存的是值,引用型別則儲存指向實際物件的地址);

equals表示的是物件的內容完全相同,此處的內容多指物件的特徵/屬性。

實際上,上面說法是不嚴謹的,更多的只是常見於String類中。首先看一下Object類中關於equals()方法的定義:

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

由此可見,Object原生的equals()方法內部呼叫的正是==,與==具有相同的含義。既然如此,為什麼還要定義此equals()方法?

equals()方法的正確理解應該是:判斷兩個物件是否相等。那麼判斷物件相等的標尺又是什麼?

如上,在object類中,此標尺即為==。當然,這個標尺不是固定的,其他類中可以按照實際的需要對此標尺含義進行重定義。如String類中則是依據字串內容是否相等來重定義了此標尺含義。如此可以增加類的功能型和實際編碼的靈活性。當然了,如果自定義的類沒有重寫equals()方法來重新定義此標尺,那麼預設的將是其父類的equals(),直到object基類。

如下場景的實際業務需求,對於User bean,由實際的業務需求可知當屬性uid相同時,表示的是同一個User,即兩個User物件相等。則可以重寫equals以重定義User物件相等的標尺。

ObjectTest中打印出true,因為User類定義中重寫了equals()方法,這很好理解,很可能張三是一個人小名,張三丰才是其大名,判斷這兩個人是不是同一個人,這時只用判斷uid是否相同即可。

如上重寫equals方法表面上看上去是可以了,實則不然。因為它破壞了Java中的約定:重寫equals()方法必須重寫hasCode()方法。

hashCode()方法;

  1. public native int hashCode()

hashCode()方法返回一個整形數值,表示該物件的雜湊碼值。

hashCode()具有如下約定:

1).在Java應用程式程式執行期間,對於同一物件多次呼叫hashCode()方法時,其返回的雜湊碼是相同的,前提是將物件進行equals比較時所用的標尺資訊未做修改。在Java應用程式的一次執行到另外一次執行,同一物件的hashCode()返回的雜湊碼無須保持一致;

2).如果兩個物件相等(依據:呼叫equals()方法),那麼這兩個物件呼叫hashCode()返回的雜湊碼也必須相等;

3).反之,兩個物件呼叫hasCode()返回的雜湊碼相等,這兩個物件不一定相等。

即嚴格的數學邏輯表示為: 兩個物件相等 <=>  equals()相等  => hashCode()相等。因此,重寫equlas()方法必須重寫hashCode()方法,以保證此邏輯嚴格成立,同時可以推理出:hasCode()不相等 => equals()不相等 <=> 兩個物件不相等。
 
可能有人在此產生疑問:既然比較兩個物件是否相等的唯一條件(也是衝要條件)是equals,那麼為什麼還要弄出一個hashCode(),並且進行如此約定,弄得這麼麻煩?
 
其實,這主要體現在hashCode()方法的作用上,其主要用於增強雜湊表的效能。
 
以集合類中,以Set為例,當新加一個物件時,需要判斷現有集合中是否已經存在與此物件相等的物件,如果沒有hashCode()方法,需要將Set進行一次遍歷,並逐一用equals()方法判斷兩個物件是否相等,此種演算法時間複雜度為o(n)。通過藉助於hasCode方法,先計算出即將新加入物件的雜湊碼,然後根據雜湊演算法計算出此物件的位置,直接判斷此位置上是否已有物件即可。(注:Set的底層用的是Map的原理實現)

在此需要糾正一個理解上的誤區:物件的hashCode()返回的不是物件所在的實體記憶體地址。甚至也不一定是物件的邏輯地址,hashCode()相同的兩個物件,不一定相等,換言之,不相等的兩個物件,hashCode()返回的雜湊碼可能相同。

因此,在上述程式碼中,重寫了equals()方法後,需要重寫hashCode()方法。

public class equals和hashcode方法 {
    @Override
    //修改equals時必須同時修改hashcode方法,否則在作為key時會出問題
    public boolean equals(Object obj) {
        return (this == obj);
    }
    
    @Override
    //相同的物件必須有相同hashcode,不同物件可能有相同hashcode
    public int hashCode() {
        return hashCode() >> 2;
    }
}

toString()方法

7.public String toString();

toString()方法返回該物件的字串表示。先看一下Object中的具體方法體:

 public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}  

toString()方法相信大家都經常用到,即使沒有顯式呼叫,但當我們使用System.out.println(obj)時,其內部也是通過toString()來實現的。

getClass()返回物件的類物件,getClassName()以String形式返回類物件的名稱(含包名)。Integer.toHexString(hashCode())則是以物件的雜湊碼為實參,以16進位制無符號整數形式返回此雜湊碼的字串表示形式。

如上例中的u1的雜湊碼是638,則對應的16進製為27e,呼叫toString()方法返回的結果為:com.corn.objectsummary.User@27e。

因此:toString()是由物件的型別和其雜湊碼唯一確定,同一型別但不相等的兩個物件分別呼叫toString()方法返回的結果可能相同。

wait() notify() notifAll()

8/9/10/11/12. wait(...) / notify() / notifyAll()

一說到wait(...) / notify() | notifyAll()幾個方法,首先想到的是執行緒。確實,這幾個方法主要用於java多執行緒之間的協作。先具體看下這幾個方法的主要含義:

wait():呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notify()/notifyAll()方法。

wait(long timeout)/wait(long timeout, int nanos):呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notisfy()/notisfyAll()方法,或超過指定的超時時間量。

notify()/notifyAll():喚醒在此物件監視器上等待的單個執行緒/所有執行緒。

wait(...) / notify() | notifyAll()一般情況下都是配套使用。下面來看一個簡單的例子:

這是一個生產者消費者的模型,只不過這裡只用flag來標識哪個執行緒需要工作

public class wait和notify {
    //volatile保證執行緒可見性
    volatile static int flag = 1;
    //object作為鎖物件,用於執行緒使用wait和notify方法
    volatile static Object o = new Object();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //wait和notify只能在同步程式碼塊內使用
                synchronized (o) {
                    while (true) {
                        if (flag == 0) {
                            try {
                                Thread.sleep(2000);
                                System.out.println("thread1 wait");
                                //釋放鎖,執行緒掛起進入object的等待佇列,後續程式碼執行
                                o.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("thread1 run");
                        System.out.println("notify t2");
                        flag = 0;
                        //通知等待佇列的一個執行緒獲取鎖
                        o.notify();
                    }
                }
            }
        }).start();
        //解釋同上
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (o) {
                        if (flag == 1) {
                            try {
                                Thread.sleep(2000);
                                System.out.println("thread2 wait");
                                o.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("thread2 run");
                        System.out.println("notify t1");
                        flag = 1;
                        o.notify();
                    }
                }
            }
        }).start();
    }

    //輸出結果是
//    thread1 run
//    notify t2
//    thread1 wait
//    thread2 run
//    notify t1
//    thread2 wait
//    thread1 run
//    notify t2
//不斷迴圈
}

從上述例子的輸出結果中可以得出如下結論:

1、wait(...)方法呼叫後當前執行緒將立即阻塞,且適當其所持有的同步程式碼塊中的鎖,直到被喚醒或超時或打斷後且重新獲取到鎖後才能繼續執行;

2、notify()/notifyAll()方法呼叫後,其所線上程不會立即釋放所持有的鎖,直到其所在同步程式碼塊中的程式碼執行完畢,此時釋放鎖,因此,如果其同步程式碼塊後還有程式碼,其執行則依賴於JVM的執行緒排程。

在Java原始碼中,可以看到wait()具體定義如下:

public final void wait() throws InterruptedException {  
     wait(0);  
}  

且wait(long timeout, int nanos)方法定義內部實質上也是通過呼叫wait(long timeout)完成。而wait(long timeout)是一個native方法。因此,wait(...)方法本質上都是native方式實現。

notify()/notifyAll()方法也都是native方法。

Java中執行緒具有較多的知識點,是一塊比較大且重要的知識點。後期會有博文專門針對Java多執行緒作出詳細總結。此處不再細述。

finalize()方法

  1. protected void finalize();

finalize方法主要與Java垃圾回收機制有關。首先我們看一下finalized方法在Object中的具體定義:

protected void finalize() throws Throwable { }  

我們發現Object類中finalize方法被定義成一個空方法,為什麼要如此定義呢?finalize方法的呼叫時機是怎麼樣的呢?

首先,Object中定義finalize方法表明Java中每一個物件都將具有finalize這種行為,其具體呼叫時機在:JVM準備對此對形象所佔用的記憶體空間進行垃圾回收前,將被呼叫。由此可以看出,此方法並不是由我們主動去呼叫的(雖然可以主動去呼叫,此時與其他自定義方法無異)。

CLass類和Object類的關係

Object類和Class類沒有直接的關係。

Object類是一切java類的父類,對於普通的java類,即便不宣告,也是預設繼承了Object類。典型的,可以使用Object類中的toString()方法。

Class類是用於java反射機制的,一切java類,都有一個對應的Class物件,他是一個final類。Class 類的例項表示,正在執行的 Java 應用程式中的類和介面。

轉一個知乎很有趣的問題
https://www.zhihu.com/question/30301819

Java的物件模型中:
1 所有的類都是Class類的例項,Object是類,那麼Object也是Class類的一個例項。

2 所有的類都最終繼承自Object類,Class是類,那麼Class也繼承自Object。

3 這就像是先有雞還是先有蛋的問題,請問實際中JVM是怎麼處理的?

這個問題中,第1個假設是錯的:java.lang.Object是一個Java類,但並不是java.lang.Class的一個例項。後者只是一個用於描述Java類與介面的、用於支援反射操作的型別。這點上Java跟其它一些更純粹的面嚮物件語言(例如Python和Ruby)不同。

而第2個假設是對的:java.lang.Class是java.lang.Object的派生類,前者繼承自後者。雖然第1個假設不對,但“雞蛋問題”仍然存在:在一個已經啟動完畢、可以使用的Java物件系統裡,必須要有一個java.lang.Class例項對應java.lang.Object這個類;而java.lang.Class是java.lang.Object的派生類,按“一般思維”前者應該要在後者完成初始化之後才可以初始化…

事實是:這些相互依賴的核心型別完全可以在“混沌”中一口氣都初始化好,然後物件系統的狀態才叫做完成了“bootstrap”,後面就可以按照Java物件系統的一般規則去執行。JVM、JavaScript、Python、Ruby等的執行時都有這樣的bootstrap過程。

在“混沌”(boostrap過程)裡,JVM可以為物件系統中最重要的一些核心型別先分配好記憶體空間,讓它們進入[已分配空間]但[尚未完全初始化]狀態。此時這些物件雖然已經分配了空間,但因為狀態還不完整所以尚不可使用。

然後,通過這些分配好的空間把這些核心型別之間的引用關係串好。到此為止所有動作都由JVM完成,尚未執行任何Java位元組碼。然後這些核心型別就進入了[完全初始化]狀態,物件系統就可以開始自我執行下去,也就是可以開始執行Java位元組碼來進一步完成Java系統的初始化了。

參考文章

https://www.cnblogs.com/congsg2016/p/5317362.html
https://www.jb51.net/article/125936.htm
https://blog.csdn.net/dufufd/article/details/80537638
https://blog.csdn.net/farsight1/article/details/80664104
https://blog.csdn.net/xiaomingdetianxia/article/details/77429180

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,偶爾講點演算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

相關推薦

夯實Java基礎系列9深入理解ClassObject

目錄 Java中Class類及用法 Class類原理 如何獲得一個Class類物件 使用Class類的物件來生成目標類的例項 Object類 類構造器public Object(); registerNatives()方法; Clone()方法實現淺拷貝 getClass()方法 equals()方法

夯實Java基礎系列10深入理解Java中的異常體系

目錄 為什麼要使用異常 異常基本定義 異常體系 初識異常 異常和錯誤 異常的處理方式 "不負責任"的throws 糾結的finally throw : JRE也使用的關鍵字 異常呼叫鏈 自定義異常 異常的注意事項 當finally遇上return JAVA異常常見面試題 參考文章 微信公眾號 Java技術

夯實Java基礎系列11深入理解Java中的回撥機制

目錄 模組間的呼叫 多執行緒中的“回撥” Java回撥機制實戰 例項一 : 同步呼叫 例項二:由淺入深 例項三:Tom做題 參考文章

夯實Java基礎系列12深入理解Java中的反射機制

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 列舉(enum)型別是Java

Java基礎系列6深入理解Java異常體系

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   前言: Java的基本理念是“結構不佳的程式碼不能執行”。 “異常”這個詞有“我對此感到意外”的意思。問題出現了,你

夯實Java基礎系列4一文了解final關鍵字的特性、使用方法,以及實現原理

目錄 final使用 final變數 final修飾基本資料型別變數和引用 final類 final關鍵字的知識點 final關鍵字的最佳實踐 final的用法 關於空白final final記憶體分配 使用final修飾方法會提高速度和效率嗎 使用final修飾變數會讓變數的值不能被改變嗎; 如何保

夯實Java基礎系列5Java檔案Java包結構

目錄 Java中的包概念 包的作用 package 的目錄結構 設定 CLASSPATH 系統變數 常用jar包 java軟體包的型別 dt.jar rt.jar *.java檔案的奧祕 *.Java檔案簡介 為什麼一個java原始檔中只能有一個public類? Main方法 外部類的訪問許可權

夯實Java基礎系列6一文搞懂抽象介面,從基礎到面試題,揭祕其本質區別!

目錄 抽象類介紹 為什麼要用抽象類 一個抽象類小故事 一個抽象類小遊戲 介面介紹 介面與類相似點: 介面與類的區別: 介面特性 抽象類和介面的區別 介面的使用: 介面最佳實踐:設計模式中的工廠模式 介面與抽象類的本質區別是什麼? 基本語法區別 設計思想區別 如何回答面試題:介面和抽象類的區別?

夯實Java基礎系列7一文讀懂Java 程式碼塊執行順序

目錄 Java中的構造方法 構造方法簡介 構造方法例項 例 1 例 2 Java中的幾種構造方法詳解 普通構造方法 預設構造方法 過載構造方法 java子類構造方法呼叫父類構造方法 Java中的程式碼塊簡介 Java程式碼塊使用 區域性程式碼塊 構造程式碼塊 靜態程式碼塊 Java程式碼塊、

夯實Java基礎系列19一文搞懂Java集合框架,以及常見面試題

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 本文參考 https://ww

夯實Java基礎系列21Java8新特性終極指南

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 這是一個Java8新增特性的總

夯實Java基礎系列22一文讀懂Java序列化反序列化

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 本文參考 http://www

夯實Java基礎系列23一文讀懂繼承、封裝、多型的底層實現原理

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 從JVM結構開始談多型 Jav

java併發系列二(深入!!!理解synchronized,volatile)

一,synchronized詳解 這個關鍵字大家想必是相當熟悉了,它是一個比較重量級的鎖,主要有兩層含義,一個是互斥性,一個是可見性。三種用法:1,修飾普通方法2,修飾靜態方法3,修飾程式碼塊 這裡有一點需要注意,普通方法要拿到當前例項的鎖,靜態方法要拿到當前class物件的鎖。 重點來

內存系列深入理解硬件原理

還要 傳輸 矩陣 bios 怎麽 線上 工程 什麽 所有 本篇文章承接上文繼續介紹DDR內存的硬件原理,包括如何尋址,時序和時延以及可以為提高內存的效能可以有哪些方法。 上次雖然解決了小張的問題,卻引發了他對內存原理的興趣。這不他又來找我了,說我還欠他一個解釋。這次我們約在

記憶體系列深入理解硬體原理

本篇文章承接上文繼續介紹DDR記憶體的硬體原理,包括如何定址,時序和時延以及可以為提高記憶體的效能可以有哪些方法。 上次雖然解決了小張的問題,卻引發了他對記憶體原理的興趣。這不他又來找我了,說我還欠他一個解釋。這次我們約在一個咖啡館見面,這次內容有點深入,我帶了些圖片,小張也點了一大杯美式,計劃大幹一

Java併發指南2深入理解Java記憶體模型JMM

一:JMM基礎與happens-before 1併發程式設計模型的分類 1.1執行緒之間如何通訊及執行緒之間如何同步 1.11執行緒之間的通訊機制 通訊:執行緒之間以何種機制來交換資訊 通訊機制有兩種:共享記憶體和訊息傳遞。 共享記憶體併發模型 執行

Java基礎系列16使用JSONObjectJSONArray解析構造json字串

轉自:https://www.zifangsky.cn/561.html 一 介紹 在Java開發中,我們通常需要進行XML文件或JSON字串的構造和解析。當然在Java Web開發中有一些第三方外掛是可以自動完成Java物件和json之間的轉換的,比

Java基礎12——深入理解ClassObject

深入理解Class類和Object類 一、所有類的型別資訊的記錄員——Class類 先來簡單瞭解一下Java虛擬機器中類的載入過程: 「載入」階段是「類載入」過程的第一個階段,虛擬機器需要完成以下三件事情: 1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流(.class檔案

二、JAVA多執行緒深入理解Thread建構函式(Thread、Runnable、守護執行緒、ThreadGroup)

本章主要介紹了所有與Thread有關的建構函式,執行緒的父子關係(並非繼承關係,而是一種包含關係),Thread和ThreadGroup之間的關係,Thread與虛擬機器棧的關係(學習這部分內容需要讀者有JVM的相關基礎,尤其是對棧記憶體要有深入的理解),最後還介紹了守護執行緒的概念、特點和使用場景