聲明:原創(chuàng)作品,轉載時請注明文章來自SAP師太技術博客( 博/客/園www.cnblogs.com):m.survivalescaperooms.com/jiangzhengjun,并以超鏈接形式標明文章原始出處,否則將追究法律責任!原文鏈接:http://m.survivalescaperooms.com/jiangzhengjun/p/4257679.html 應用47.不可變的引用類型BigInteger total = BigInteger.ZERO; total.add(new BigInteger("1")); total.add(new BigInteger("10")); System.out.println(total);//0 上面程序的結果為11嗎?答案是0。 BigInteger實例是不可變的。String、BigDecimal以及包裝類型:Integer、Long、Short、Byte、Character、Boolean、Float和Double也是如此。對這些類型的操作將返回新的實例。 不可變類型更容易設計、實現(xiàn)與作用;它們出錯的可能性更小,并且更加安全。 本程序修改如下: BigInteger total = BigInteger.ZERO; total=total.add(new BigInteger("1")); total=total.add(new BigInteger("10")); System.out.println(total);//11
48.請同時重寫equals()與hashCode()class T { private String str; T(String str) { this.str = str; } public boolean equals(Object obj) { if(!(obj instanceof T)){ return false; } T t = (T)obj; return t.equals(this.str); } public static void main(String[] args) { Set set = new HashSet(); set.add(new T("str")); System.out.println(set.contains(new T("str")));//false } } 上面的程序不會打印true,而是false,為什么? hashCode約定要求相等的對象要具有相同的散列碼。 無論何時,只要你重寫了equals方法,你就必須同時重寫hashCode方法。 如果將自定的類型對象放入HashSet、HashMap、Hashtable、LinkedHashSet、LinkedHashMap這此散列集合時,一定需要重寫equals與hashCode方法,這樣在放入進去之后還能查找出來。如果放入其他非散列類型的集合時,其實只需要重寫equals就可以了。 本程序解決辦法重寫hashCode()方法: public int hashCode() { return 37 * this.str.hashCode(); }
49.日期設置Calendar c = Calendar.getInstance(); c.set(2010, 12, 31);// 月是從0開始的,11其實表示12月 System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH)); c = Calendar.getInstance(); c.set(2010, 11, 31); System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH)); 本程序較簡單,只需注意月是從0開始的就可以了,如果你設置月為12,則會自動轉換為下一年。
50.IdentityHashMapclass T { private String str; T(String str) { this.str = str; } public int hashCode() { return 37 * this.str.hashCode(); } public boolean equals(Object obj) { return this.str.equals(((T) obj).str); } public static void put(Map m) { m.put("str", "1"); /* * 由于上面程序?qū)?"str" 放入了字符串常量池, * 所以str是同一個對象,不管是什么樣類型的 * Map,即使使用IdentityHashMap都只放入一次 */ m.put("str", "2"); m.put(new T("str"), "3"); m.put(new T("str"), "4"); } public static void main(String[] args) { Map m = new HashMap(); put(m); System.out.println(m.size());// 2 //IdentityHashMap比較時使用==替換equals()方法 m = new IdentityHashMap(); put(m); System.out.println(m.size());// 3 } }
51.靜態(tài)導入的優(yōu)先權import static java.util.Arrays.toString; import java.util.Arrays; public class T { public static void main(String[] args) { prt(1, 2, 3); } static void prt(Object... args) { // 自身繼承至Object類的toString的優(yōu)先級高于靜態(tài)導入的方法 //!! System.out.println(toString(args));//不能編譯 System.out.println(Arrays.toString(args)); } } 本身就屬于某個范圍的成員在該范圍內(nèi)與靜態(tài)導入相比具有優(yōu)先權。
52.PrintStream對輸出結果的緩沖public static void main(String[] args) { String str = "Hello World"; for (int i = 0; i < str.length(); i++) { System.out.write(str.charAt(i)); } } 上面的程序沒有輸出結果。 這里的問題在于System.out是帶有緩沖的。輸出的結果被寫入了System.out的緩沖區(qū),但是緩沖區(qū)從來都沒有被刷新。大多數(shù)人認為,當有輸出產(chǎn)生的時候System.out和System.err會自動地進制刷新,但這并不完全正確,這兩個流都屬于PrintStream類型,請看API DOC描述:一個PrintStream被創(chuàng)建為自動刷新,這意味著當一個字節(jié)數(shù)組(byte[])被寫入、或者某個println方法被調(diào)用、或者一個換行字符或字節(jié)('/n')被寫入之后,PrintStream類型的flush方法就會被自動調(diào)用。 令人奇怪的是,如果這個程序用print(char)去替代write(int),它就會刷新System.out并輸出結果,這種行為與print(char)的文檔是矛盾的,因為其文檔敘述道:“打印一個字符,這個字符將根據(jù)平臺缺省的字符編碼方式翻譯成一個或多個字節(jié),并且這些字節(jié)將完全按照write(int)方法的方式輸出。”,但這里沒有換行符卻也自動的刷新了。類似的,如果程序改用print(String),它也會對流進行刷新。所以調(diào)用print方法也是會自動刷新的。 請加入到原博客中:PrintStream也可以對OutputStream進行包裝并指定編碼方式:PrintStream(OutputStream out, boolean autoFlush, String encoding),但實質(zhì)上也是調(diào)用OutputStreamWriter來實現(xiàn)的。 System.err在eclipse中輸出時是紅色的字體。
53.調(diào)用操作系統(tǒng)命令時被阻塞問題public static void main(String[] args) throws IOException, InterruptedException { String command = "java ProcessTest exc"; if (args.length != 0) { for (int i = 0; i < 200; i++) { System.out.println(command); System.err.println(command); } } else { Process process = Runtime.getRuntime().exec(command); int exitValue = process.waitFor(); System.out.println("exit value = " + exitValue); } } 執(zhí)行java ProcessTest發(fā)現(xiàn)程序阻塞。 Process文檔描述:由于某些本地平臺只提供有限大小的緩沖,所以如果不能迅速地讀取子進程的輸出流,就有可能會導致子進程的阻塞,甚至是死鎖。這恰好就是這里所發(fā)生的事情:沒有足夠的緩沖空間來保存這些輸出結果。為了結子進程(Process線程),父進程(Main線程)必須排空它的輸出流(標準流與錯誤流都需要排空),即要去緩存中讀取結果: static void readResult(final InputStream is) { new Thread(new Runnable() { public void run() { try { // 排空緩存內(nèi)容 while (is.read() >= 0); } catch (IOException e) { e.printStackTrace(); } } }).start(); } 然后在process.waitFor()之前加上 readResult(process.getErrorStream()); readResult(process.getInputStream()); 即可輸出exit value = 0。 另外,只能根據(jù)process.waitFor返回的結果來判斷操作系統(tǒng)命令執(zhí)行是否成功(成功:0,失敗:1),我們不能根據(jù)錯誤流中是否有內(nèi)容來判斷是否執(zhí)行成功。
54.實現(xiàn)Serializable的單例問題class Dog implements Serializable{ public static final Dog INSTANCE = new Dog(); private Dog(){} } 上面能控制只生成一個單實例嗎? 如果對實現(xiàn)了Serializable的對象進行序列化后,再反序列化,內(nèi)中會不只一個實例了,因為反序列化時會重新生成一個對象。 既然INSTANCE為靜態(tài)域,那序列化時返回的對象如果也是INSTANCE就可以解決問題了,而打開API我們發(fā)現(xiàn)Serializable接口確實有這樣兩個特殊的方法描述: l 將對象寫入流時需要指定要使用的替代對象的可序列化類,應使用準確的簽名來實現(xiàn)此特殊方法: ANY-access-MODIFIER Object writeReplace() throws ObjectStreamException; 此 writeReplace 方法將由序列化調(diào)用,前提是如果此方法存在,而且它可以通過被序列化對象的類中定義的一個方法訪問。因此,該方法可以擁有私有 (private)、受保護的 (protected) 和包私有 (package-private) 訪問。子類對此方法的訪問遵循 java 訪問規(guī)則。 l 在從流中讀取類的一個實例時需要指定替代的類應使用的準確簽名來實現(xiàn)此特殊方法: ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; 此 readResolve 方法遵循與 writeReplace 相同的調(diào)用規(guī)則和訪問規(guī)則。 上述兩個方法的只要出現(xiàn),就會履蓋以下兩個方法(這兩個方法本質(zhì)的意義就是用來替換序列與反序列的對象),雖然會執(zhí)行它們,但最后得到的結果卻是writeReplace、readResolve兩個方法寫入或讀出的對象: l private void writeObject(java.io.ObjectOutputStream out) throws IOException l private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException; 另外,writeObject與readObject需成對實現(xiàn),而writeReplace與readResolve則不需要成對出現(xiàn),一般單獨使用。如果同時出現(xiàn)這四個方法,最后寫入與讀出的結果以writeReplace和readResolve方法的結果為準。 所以下要解決真真單實例問題,我們?nèi)缦滦拚?class Dog implements Serializable { public static final Dog INSTANCE = new Dog(); private Dog() {} private Object readResolve() { return INSTANCE; } } public class SerialDog { public static void main(String[] args) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); new ObjectOutputStream(bos).writeObject(Dog.INSTANCE); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); Dog dog = (Dog) new ObjectInputStream(bin).readObject(); System.out.println(dog == Dog.INSTANCE);//true } } 一個實現(xiàn)了Serializable的單例類,必須有一個readResolve方法,用以返回它的唯一實例。
55.thread. isInterrupted()與Thread.interrupted()public class SelfInerruption { public static void main(String[] args) { Thread.currentThread().interrupt(); if (Thread.interrupted()) { // Interruped:false System.out.println("Interruped:" + Thread.interrupted()); } else { System.out.println("Not interruped:" + Thread.interrupted()); } } } 上面結果走的是第一個分支,但結果卻不是Interruped:true? Thread.interrupted()為Thread的靜態(tài)方法,調(diào)用它首先會返回當前線程的中斷狀態(tài)(如果當前線程上調(diào)用了interrupt()方法,則返回true,否則為false),然后再清除當前線程的中斷狀態(tài),即將中斷狀態(tài)設置為false。換句話說,如果連續(xù)兩次調(diào)用該方法,則第二次調(diào)用將返回 false。 而isInterrupted()方法為實例方法,測試線程是否已經(jīng)中斷,并不會清除當前線程中斷狀態(tài)。 所以這里應該使用isInterrupted()實例方法,就可以修復該問題。
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靜態(tài)成員,又因為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); } } 看看上面變態(tài)的程序,一個靜態(tài)變量的初始化由靜態(tài)塊里的線程來初始化,最后的結果怎樣? 當一個線程訪問一個類的某個成員的時候,它會去檢查這個類是否已經(jīng)被初始化,在這一過程中會有以下四種情況: 1、 這個類尚未被初始化 2、 這個類正在被當前線程初始化:這是對初始化的遞歸請求,會直接忽略掉(另,請參考《構造器中靜態(tài)常量的引用問題》一節(jié)) 3、 這個類正在被其他線程而不是當前線程初始化:需等待其他線程初始化完成再使用類的Class對象,而不會兩個線程都會去初始化一遍(如果這樣,那不類會初始化兩遍,這顯示不合理) 4、 這個類已經(jīng)被初始化 當主線程調(diào)用Lazy.main,它會檢查Lazy類是否已經(jīng)被初始化。此時它并沒有被初始化(情況1),所以主線程會記錄下當前正在進行的初始化,并開始對這個類進行初始化。這個過程是:主線程會將initial的值設為false,然后在靜態(tài)塊中創(chuàng)建并啟動一個初始化initial的線程t,該線程的run方法會將initial設為true,然后主線程會等待t線程執(zhí)行完畢,此時,問題就來了。 由于t線程將Lazy.initial設為true之前,它也會去檢查Lazy類是否已經(jīng)被初始化。這時,這個類正在被另外一個線程(mian線程)進行初始化(情況3)。在這種情況下,當前線程,也就是t線程,會等待Class對象直到初始化完成,可惜的是,那個正在進行初始化工作的main線程,也正在等待t線程的運行結束。因為這兩個線程現(xiàn)在正相互等待,形成了死鎖。 修正這個程序的方法就是讓主線程在等待線程前就完成初始化操作: 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類初始化完成后再調(diào)用join方法 try { t.join();// 主線程等待t線程結束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(initial); } } 雖然修正了該程序掛起問題,但如果還有另一線程要訪問Lazy的initial時,則還是很有可能不等initial最后賦值就被使用了。 總之,在類的初始化期間等待某個線程很可能會造成死鎖,要讓類初始化的動作序列盡可能地簡單。
57.繼承內(nèi)部類一般地,要想實例化一個內(nèi)部類,如類Inner1,需要提供一個外圍類的實例給構造器。一般情況下,它是隱式地傳遞給內(nèi)部類的構造器,但是它也是可以以 expression.super(args) 的方式即通過調(diào)用超類的構造器顯式的傳遞。 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的默認構造函數(shù),而不是 * WithInner的父類構造函數(shù),這種特殊的語法只在繼承一個非靜態(tài)內(nèi)部類時才用到, * 表示繼承非靜態(tài)內(nèi)部類時,外圍對象一定要存在,并且只能在 第一行調(diào)用,而且一 * 定要調(diào)用一下。為什么不能直接使用 super()或不直接寫出呢?最主要原因就是每個 * 非靜態(tài)的內(nèi)部類都會與一個外圍類實例對應,這個外圍類實例是運行時傳到內(nèi) * 部類里去的,所以在內(nèi)部類里可以直接使用那個對象(比如Outer.this),但這里 * 是在外部內(nèi)外 ,使用時還是需要存在外圍類實例對象,所以這里就顯示的通過構造 * 器傳遞進來,并且在外圍對象上顯示的調(diào)用一下內(nèi)部類的構造器,這樣就確保了在 * 繼承至一個類部類的情況下 ,外圍對象一類會存在的約束。 */ 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要放置在父類中會百分百機率出現(xiàn) // 放置到子類中就不一定會出現(xiàn)問題了 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一類集合都實現(xiàn)了序列化的writeObject()與readObject()方法。這里錯誤原因是由HashSet的readObject方法引起的。在某些情況下,這個方法會間接地調(diào)用某個未初始化對象的被覆寫的方法。為了組裝正在反序列化的HashSet,HashSet.readObject調(diào)用了HashMap.put方法,而put方法會去調(diào)用鍵的hashCode方法。由于整個對象圖正在被反序列化,并沒有什么可以保證每個鍵在它的hashCode方法被調(diào)用時已經(jīng)被完全初始化了,因為HashSet是在父類中定義的,而在序列化HashSet時子類還沒有開始初始化(這里應該是序列化)子類,所以這就造成了在父類中調(diào)用還沒有初始完成(此時id為0)的被子類覆寫的hashCode方法,導致該對象重新放入hash表格的位置與反序列化前不一樣了。hashCode返回了錯誤的值,相應的鍵值對條目將會放入錯誤的單元格中,當id被初始化為888時,一切都太遲了。 這個程序的說明,包含了HashMap的readObject方法的序列化系統(tǒng)總體上違背了不能從類的構造器或偽構造器(如序列化的readObject)中調(diào)用可覆寫方法的規(guī)則。 如果一個HashSet、Hashtable或HashMap被序列化,那么請確認它們的內(nèi)容沒有直接或間接地引用它們自身,即正在被序列化的對象。 另外,在readObject或readResolve方法中,請避免直接或間接地在正在進行反序列化的對象上調(diào)用任何方法,因為正在反序列化的對象處于不穩(wěn)定狀態(tài)。
59.迷惑的內(nèi)部類public class Twisted { private final String name; Twisted(String name) { this.name = name; } // 私有的不能被繼承,但能被內(nèi)部類直接訪問 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類,所有的本地的、內(nèi)部的、嵌套的長匿名的類都可以毫無限制地訪問彼此的成員。 另一個原因是私有的不能被繼承。
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"; } 現(xiàn)在假設你像下面這樣改變了那個庫類并且重新編譯了這個類,但并不重新編譯客戶端的程序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.打亂數(shù)組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); } // 測試數(shù)組上的每個位置放置的元素是否等概率 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} 如果某個位置上等概率出現(xiàn)這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中提供的會更好,會在效率上獲得最大的受益。
新聞熱點
疑難解答