夯實Java基礎系列9:深入理解Class類和Object類
- 2019 年 10 月 3 日
- 筆記
目錄
– 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
obj=shapes.class;
shapes newShape=obj.newInstance();
因為有了類型限制,所以使用泛化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()方法;
- 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()方法
- 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技術學習資源: 一些程序員學習技術的資源大禮包,關注公眾號後,後台回復關鍵字 「資料」 即可免費無套路獲取。