56.?惰性初始化
- public ? class ?Lazy?{??
- ? private ? static ? boolean ?initial?=? false ;??
- ? static ?{??
- ??Thread?t?=? new ?Thread( new ?Runnable()?{??
- ??? public ? void ?run()?{??
- ????System.out.println( "befor..." ); //此句會輸出 ??
- ???? /* ?
- ?????*?由于使用Lazy.initial靜態成員,又因為Lazy還未?初 ?
- ?????*?始化完成,所以該線程會在這里等待主線程初始化完成 ?
- ?????*/ ??
- ????initial?=? true ;??
- ????System.out.println( "after..." ); //此句不會輸出 ??
- ???}??
- ??});??
- ??t.start();??
- ?? try ?{??
- ???t.join(); //?主線程等待t線程結束 ??
- ??}? catch ?(InterruptedException?e)?{??
- ???e.printStackTrace();??
- ??}??
- ?}??
- ??
- ? public ? static ? void ?main(String[]?args)?{??
- ??System.out.println(initial);??
- ?}??
- }??
看看上面變態的程序,一個靜態變量的初始化由靜態塊里的線程來初始化,最后的結果怎樣?
?
當一個線程訪問一個類的某個成員的時候,它會去檢查這個類是否已經被初始化,在這一過程中會有以下四種情況:
1、?這個類尚未被初始化
2、?這個類正在被當前線程初始化:這是對初始化的遞歸請求,會直接忽略掉(另,請參考《構造器中靜態常量的引用問題》一節)
3、?這個類正在被其他線程而不是當前線程初始化:需等待其他線程初始化完成再使用類的Class對象,而不會兩個線程都會去初始化一遍(如果這樣,那不類會初始化兩遍,這顯示不合理)
4、?這個類已經被初始化
當主線程調用Lazy.main,它會檢查Lazy類是否已經被初始化。此時它并沒有被初始化(情況1),所以主線程會記錄下當前正在進行的初始化,并開始對這個類進行初始化。這個過程是:主線程會將initial的值設為false,然后在靜態塊中創建并啟動一個初始化initial的線程t,該線程的run方法會將initial設為true,然后主線程會等待t線程執行完畢,此時,問題就來了。
由于t線程將Lazy.initial設為true之前,它也會去檢查Lazy類是否已經被初始化。這時,這個類正在被另外一個線程(mian線程)進行初始化(情況3)。在這種情況下,當前線程,也就是t線程,會等待Class對象直到初始化完成,可惜的是,那個正在進行初始化工作的main線程,也正在等待t線程的運行結束。因為這兩個線程現在正相互等待,形成了死鎖。
?
修正這個程序的方法就是讓主線程在等待線程前就完成初始化操作:
- public ? class ?Lazy?{??
- ? private ? static ? boolean ?initial?=? false ;??
- ? static ?Thread?t?=? new ?Thread( new ?Runnable()?{??
- ?? public ? void ?run()?{??
- ???initial?=? true ;??
- ??}??
- ?});??
- ? static ?{??
- ??t.start();??
- ?}??
- ??
- ? public ? static ? void ?main(String[]?args)?{??
- ?? //?讓Lazy類初始化完成后再調用join方法 ??
- ?? try ?{??
- ???t.join(); //?主線程等待t線程結束 ??
- ??}? catch ?(InterruptedException?e)?{??
- ???e.printStackTrace();??
- ??}??
- ??System.out.println(initial);??
- ?}??
- }??
雖然修正了該程序掛起問題,但如果還有另一線程要訪問Lazy的initial時,則還是很有可能不等initial最后賦值就被使用了。
?
總之,在類的初始化期間等待某個線程很可能會造成死鎖,要讓類初始化的動作序列盡可能地簡單。
57.?繼承內部類
一般地,要想實例化一個內部類,如類Inner1,需要提供一個外圍類的實例給構造器。一般情況下,它是隱式地傳遞給內部類的構造器,但是它也是可以以 expression.super(args) 的方式即通過調用超類的構造器顯式的傳遞。
- public ? class ?Outer?{??
- ? class ?Inner1? extends ?Outer{??
- ??Inner1(){??
- ??? super ();??
- ??}??
- ?}??
- ? class ?Inner2? extends ?Inner1{??
- ??Inner2(){??
- ???Outer. this . super ();??
- ??}??
- ??Inner2(Outer?outer){??
- ???outer. super ();??
- ??}??
- ?}??
- }??
- class ?WithInner?{??
- ? class ?Inner?{}??
- }??
- class ?InheritInner? extends ?WithInner.Inner?{??
- ? //?!?InheritInner()?{}?//?不能編譯 ??
- ? /* ?
- ??*?這里的super指InheritInner類的父類WithInner.Inner的默認構造函數,而不是 ?
- ??*?WithInner的父類構造函數,這種特殊的語法只在繼承一個非靜態內部類時才用到, ?
- ??*?表示繼承非靜態內部類時,外圍對象一定要存在,并且只能在?第一行調用,而且一 ?
- ??*?定要調用一下。為什么不能直接使用?super()或不直接寫出呢?最主要原因就是每個 ?
- ??*?非靜態的內部類都會與一個外圍類實例對應,這個外圍類實例是運行時傳到內 ?
- ??*?部類里去的,所以在內部類里可以直接使用那個對象(比如Outer.this),但這里 ?
- ??*?是在外部內外?,使用時還是需要存在外圍類實例對象,所以這里就顯示的通過構造 ?
- ??*?器傳遞進來,并且在外圍對象上顯示的調用一下內部類的構造器,這樣就確保了在 ?
- ??*?繼承至一個類部類的情況下?,外圍對象一類會存在的約束。 ?
- ??*/ ??
- ?InheritInner(WithInner?wi)?{??
- ??wi. super ();??
- ?}??
- ??
- ? public ? static ? void ?main(String[]?args)?{??
- ??WithInner?wi?=? new ?WithInner();??
- ??InheritInner?ii?=? new ?InheritInner(wi);??
- ?}??
- }??
?
58.?Hash集合序列化問題
- class ?Super? implements ?Serializable{??
- ? //?HashSet要放置在父類中會百分百機率出現 ??
- ? //?放置到子類中就不一定會出現問題了 ??
- ? final ?Set?set?=? new ?HashSet();???
- }??
- class ?Sub? extends ?Super?{??
- ? private ? int ?id;??
- ? public ?Sub( int ?id)?{??
- ?? this .id?=?id;??
- ??set.add( this );??
- ?}??
- ? public ? int ?hashCode()?{??
- ?? return ?id;??
- ?}??
- ? public ? boolean ?equals(Object?o)?{??
- ?? return ?(o? instanceof ?Sub)?&&?(id?==?((Sub)?o).id);??
- ?}??
- }??
- ??
- public ? class ?SerialKiller?{??
- ? public ? static ? void ?main(String[]?args)? throws ?Exception?{??
- ??Sub?sb?=? new ?Sub( 888 );??
- ??System.out.println(sb.set.contains(sb)); //?true ??
- ????
- ??ByteArrayOutputStream?bos?=? new ?ByteArrayOutputStream();??
- ?? new ?ObjectOutputStream(bos).writeObject(sb);??
- ????
- ??ByteArrayInputStream?bin?=? new ?ByteArrayInputStream(bos.toByteArray());??
- ??sb?=?(Sub)? new ?ObjectInputStream(bin).readObject();??
- ????
- ??System.out.println(sb.set.contains(sb)); //?false ??
- ?}??
- }??
Hash一類集合都實現了序列化的writeObject()與readObject()方法。這里錯誤原因是由HashSet的readObject方法引起的。在某些情況下,這個方法會間接地調用某個未初始化對象的被覆寫的方法。為了組裝正在反序列化的HashSet,HashSet.readObject調用了HashMap.put方法,而put方法會去調用鍵的hashCode方法。由于整個對象圖正在被反序列
化,并沒有什么可以保證每個鍵在它的hashCode方法被調用時已經被完全初始化了,因為HashSet是在父類中定義的,而在序列化HashSet時子類還沒有開始初始化(這里應該是序列化)子類,所以這就造成了在父類中調用還沒有初始完成(此時id為0)的被子類覆寫的hashCode方法,導致該對象重新放入hash表格的位置與反序列化前不一樣了。hashCode返回了錯誤的值,相應的鍵值對條目將會放入錯誤的單元格中,當id被初始化為888時,一切都太遲了。
?
這個程序的說明,包含了HashMap的readObject方法的序列化系統總體上違背了不能從類的構造器或偽構造器(如序列化的readObject)中調用可覆寫方法的規則。
?
如果一個HashSet、Hashtable或HashMap被序列化,那么請確認它們的內容沒有直接或間接地引用它們自身,即正在被序列化的對象。
?
另外,在readObject或readResolve方法中,請避免直接或間接地在正在進行反序列化的對象上調用任何方法,因為正在反序列化的對象處于不穩定狀態。
59.?迷惑的內部類
- public ? class ?Twisted?{??
- ? private ? final ?String?name;??
- ?Twisted(String?name)?{??
- ?? this .name?=?name;??
- ?}??
- ? //?私有的不能被繼承,但能被內部類直接訪問 ??
- ? private ?String?name()?{??
- ?? return ?name;??
- ?}??
- ? private ? void ?reproduce()?{??
- ?? new ?Twisted( "reproduce" )?{??
- ??? void ?printName()?{??
- ???? //?name()為外部類的,因為沒有被繼承過來 ??
- ????System.out.println(name()); //?main ??
- ???}??
- ??}.printName();??
- ?}??
- ??
- ? public ? static ? void ?main(String[]?args)?{??
- ?? new ?Twisted( "main" ).reproduce();??
- ?}??
- }??
在頂層的類型中,即本例中的Twisted類,所有的本地的、內部的、嵌套的長匿名的類都可以毫無限制地訪問彼此的成員。
?
另一個原因是私有的不能被繼承。
60.?編譯期常量表達式
第一個PrintWords代表客戶端,第二個Words代表一個類庫:
- class ?PrintWords?{??
- ? public ? static ? void ?main(String[]?args)?{??
- ??System.out //引用常量變量 ??
- ????.println(Words.FIRST?+? "?" ???
- ??????+?Words.SECOND?+? "?" ???
- ??????+?Words.THIRD);??
- ?}??
- }??
- ??
- class ?Words?{??
- ? //?常量變量 ??
- ? public ? static ? final ?String?FIRST?=? "the" ;??
- ? //?非常量變量 ??
- ? public ? static ? final ?String?SECOND?=? null ;??
- ? //?常量變量 ??
- ? public ? static ? final ?String?THIRD?=? "set" ;??
- }??
現在假設你像下面這樣改變了那個庫類并且重新編譯了這個類,但并不重新編譯客戶端的程序PrintWords:
- class ?Words?{??
- ? public ? static ? final ?String?FIRST?=? "physics" ;??
- ? public ? static ? final ?String?SECOND?=? "chemistry" ;??
- ? public ? static ? final ?String?THIRD?=? "biology" ;??
- }??
此時,端的程序會打印出什么呢?結果是 the chemistry set,不是the null set,也不是physics chemistry biology,為什么?原因就是 null 不是一個編譯期常量表達式,而其他兩個都是。
?
對于常量變量(如上面Words類中的FIRST、THIRD)的引用(如在PrintWords類中對Words.FIRST、Words.THIRD的引用)會在編譯期被轉換為它們所表示的常量的值(即PrintWords類中的Words.FIRST、Words.THIRD引用會替換成"the"與"set")。
?
一個常量變量(如上面Words類中的FIRST、THIRD)的定義是,一個在編譯期被常量表達式(即編譯期常量表達式)初
始化的final的原生類型或String類型的變量。
?
那什么是“編譯期常量表達式”?精確定義在[JLS 15.28]中可以找到,這樣要說的是null不是一個編譯期常量表達式。
?
由于常量變量會編譯進客戶端,API的設計者在設計一個常量域之前應該仔細考慮一下是否應該定義成常量變量。
?
如果你使用了一個非常量的表達式去初始化一個域,甚至是一個final或,那么這個域就不是一個常量。下面你可以通過將一個常量表達式傳給一個方法使用得它變成一個非常量:
- class ?Words?{??
- ? //?以下都成非常量變量 ??
- ? public ? static ? final ?String?FIRST?=?ident( "the" );??
- ? public ? static ? final ?String?SECOND?=?ident( null );??
- ? public ? static ? final ?String?THIRD?=?ident( "set" );??
- ? private ? static ?String?ident(String?s)?{??
- ?? return ?s;??
- ?}??
- }??
總之,常量變量將會被編譯進那些引用它們的類中。一個常量變量就是任何常量表達式初始化的原生類型或字符串變量。且null不是一個常量表達式。
61.?打亂數組
- class ?Shuffle?{??
- ? private ? static ?Random?rd?=? new ?Random();??
- ? public ? static ? void ?shuffle(Object[]?a)?{??
- ?? for ?( int ?i?=? 0 ;?i?<?a.length;?i++)?{??
- ???swap(a,?i,?rd.nextInt(a.length));??
- ??}??
- ?}??
- ? public ? static ? void ?swap(Object[]?a,? int ?i,? int ?j)?{??
- ??Object?tmp?=?a[i];??
- ??a[i]?=?a[j];??
- ??a[j]?=?tmp;??
- ?}??
- ? public ? static ? void ?main(String[]?args)?{??
- ??Map?map?=? new ?TreeMap();??
- ?? for ?( int ?i?=? 0 ;?i?<? 9 ;?i++)?{??
- ???map.put(i,? 0 );??
- ??}??
- ????
- ?? //?測試數組上的每個位置放置的元素是否等概率 ??
- ?? for ?( int ?i?=? 0 ;?i?<? 10000 ;?i++)?{??
- ???Integer[]?intArr?=? new ?Integer[]?{? 0 ,? 1 ,? 2 ,? 3 ,? 4 ,? 5 ,? 6 ,? 7 ,? 8 ?};??
- ???shuffle(intArr);??
- ??? for ?( int ?j?=? 0 ;?j?<? 9 ;?j++)?{??
- ????map.put(j,(Integer)map.get(j)+intArr[j]);??
- ???}??
- ??}??
- ??System.out.println(map);??
- ?? for ?( int ?i?=? 0 ;?i?<? 9 ;?i++)?{??
- ???map.put(i,(Integer)?map.get(i)/10000f);??
- ??}??
- ??System.out.println(map);??
- ?}??
- }??
上面的算法不是很等概率的讓某個元素打亂到其位置,程序運行了多次,大致的結果為:
{0=36031, 1=38094, 2=39347, 3=40264, 4=41374, 5=41648, 6=41780, 7=41188, 8=40274}
{0=3.6031, 1=3.8094, 2=3.9347, 3=4.0264, 4=4.1374, 5=4.1648, 6=4.178, 7=4.1188, 8=4.0274}
?
如果某個位置上等概率出現這9個值的話,則平均值會趨近于4,但測試的結果表明:開始的時候比較低,然后增長超過了平均值,最后又降下來了。
?
如果改用下面算法:
- public ? static ? void ?shuffle(Object[]?a)?{??
- ? for ?( int ?i?=? 0 ;?i?<?a.length;?i++)?{??
- ??swap(a,?i,?i?+?rd.nextInt(a.length?-?i));??
- ?}??
- }??
多次測試的結果大致如下:
{0=40207, 1=40398, 2=40179, 3=39766, 4=39735, 5=39710, 6=40074, 7=39871, 8=40060}
{0=4.0207, 1=4.0398, 2=4.0179, 3=3.9766, 4=3.9735, 5=3.971, 6=4.0074, 7=3.9871, 8=4.006}
所以修改后的算法是合理的。
?
另一種打亂集合的方式是通過Api中的Collections工具類:
- public ? static ? void ?shuffle(Object[]?a)?{??
- ?Collections.shuffle(Arrays.asList(a));??
- }??
其實算法與上面的基本相似,當然我們使用API中提供的會更好,會在效率上獲得最大的受益。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
