《JAVA解惑》學習筆記

  • 不要用return、break、continue、throw來退出finally語句塊。這樣將直接跳出finally語句塊,從而跳過try語句塊,這並不是我們想要的效果。
  • 關於try-catch
    1. 如果一個catch子句要捕獲一個類型爲E的受檢查異常,而其相對應的try子句卻不能拋出E或其子類異常,這樣編譯時無法通過的。
      try {} catch(IOException e){} // 非法,因爲IOException是檢查異常
    2. 捕獲Exception或Throwable的catch子句是合法的,不管預期相對應的try子句的內容是什麼,哪怕爲空
      try {} carch(Exception e){} // 合法
    3. 一個方法可以拋出的檢查異常集合是它所適用的所有類型聲明要拋出的檢查異常集合的交集
      即:假設接口1聲明方法f() throws A,接口2聲明方法f() throws B,實現類同時實現這兩個接口,必須實現方法f(),但是這裏寫成f()纔對,
      因爲異常要取交集
  • 對於一個未賦值的final域或變量,在try和catch塊中同時爲其賦值時是會報錯的,哪怕邏輯上OK。
    因此在使用final域時,最好的方式是聲明同時賦值
  • System.exit()
    1. 將停止所有的程序線程。
    2. 該方法被調用時,會執行所有關閉掛鉤操作,釋放VM之外的資源
      因此我們如果需要在System.exit()執行後釋放資源,可以調用Runtime.getRuntime().addShutdownHook(Thread t)來添加關閉鉤子
    3. 與之類似的方法
      System.halt() 該方法會直接關閉所有線程,不執行關閉鉤子的動作,建議少用。
  • 在複習一遍實例化過程
    • 分配內存,並將所有域初始化爲零值
    • 進入構造方法,首先按照域聲明順序進行初始化賦值
    • 再遞歸執行構造方法程序體
  • 構造器必須聲明其實例化操作會拋出的所有檢查異常
    • 哪怕這些異常不是在構造器程序體中拋出
    • 典型是的域聲明同時初始化時拋出的異常,需要在構造器拋出
  • finally中的關流問題
    我們經常遇到輸入輸出流需要在finally中,但流的close()方法也會拋出異常。
    • 解決方法是在finally中也拋出異常,但這樣就不美觀了。
    • 好的解決方法是寫一個方法,傳入這些待關閉的流,使用時統一調用即可。一般來說,這些流都繼承了Closeable方法。
  • 對於任何在finally語句塊中拋出的異常,我們都應該當場處理,而不是繼續拋出去
  • 在執行循環時,不能依賴於異常跳出循環,這樣速度會非常慢
  • & | 操作符
    1. 正常情況是位與/或運算符
    2. 兩邊位boolean時,變成了邏輯與/或,只不過沒有短路功能。
  • Class.newInstance()會傳播從無參構造器拋出的所有異常。
    • 比較坑爹的是我們在調用該方法時只能顯式地看到其聲明的兩個異常。對應的無參構造器會拋出什麼異常我們並不知道。
  • Java的異常檢查機制不是虛擬機強制執行的,只是編譯器提供的工具。因此編譯器給出的異常檢查警告一定要認真對待。
  • 探測類丟失
    1. 要想編寫一個能夠探測類丟失的程序,使用反射來引用類,而不要使用常規方法,具體原因可以參見 解惑44
      雖然Java語言規範非常仔細地描述了類初始化是何時發生的,但是類被加載的時機卻是遠遠不可預測的,就像解惑44,居然是驗證時加載了類。
    2. 不要對捕獲NoClassDefFoundError形成依賴,一般來說,捕獲Error及其子類是非常不可取的。
  • Java的重載解析過程
    1. 選取所有可獲得並可應用的方法或構造器
    2. 在第一階段選取的方法或構造器選取最精確的一個
      精確:如果一個方法可以接受傳遞給另一個方法的任何參數,我們就是該方法不如另一個方法精確。
    3. 要想強制要求編譯器選擇一個確定的重載版本,需要將實參轉型爲形參所聲明的類型
  • 每一個靜態域在聲明他的類及其所有子類中共享一份單一的拷貝(這點要尤其注意,之前理解錯誤)
    即:靜態域由聲明它的類及其所有子類所共享
  • 在選擇繼承還是組合的方式時,一定要充分考慮他們的關係是 is-a 還是 has-a,不能自己隨便用
  • 靜態方法
    1. 屬於類,不存在任何動態的分派機制(不會重載),調用時按引用類型決定調用的是哪個方法(編譯器綁定)。
    2. 一定不要通過對象來調用靜態方法,因爲一不小心就會引起問題
  • 小記一筆
    1. 類的靜態域是在類加載後就進行初始化的,初始化順序和聲明順序一致
    2. 實例變量是在創建對象時進行初始化的,順序也和聲明的順序一致
  • instanceof操作符
    1. 當左操作符是null時,運算結果被定義爲false
    2. 右操作符是不允許爲null的
    3. 如果左右操作符都是類,則要求一個類必須是另一個類的子類
      如 new Type() instanceof String, 要求Type必須是String的子類或父類
  • 一個final類型的實例域被賦值前,是有可能被取用的,語法上沒有問題
  • 應注意不要使用循環的類初始化和循環的實例初始化。
    1. 循環的類初始化:初始化靜態域時調用尚未初始化的靜態域
    2. 循環的實例初始化:初始化實例時調用尚未初始化的實例變量
  • 禁止在構造器中、僞構造器中調用重寫方法,因爲這樣可能調用到子類的重寫方法,而此時重寫方法調用的值很可能尚未初始化好。
  • 靜態方法調用的限定表達式是可以計算的,但是它的值將被忽略。如(現實中不要這樣寫)
    1. ((Null)null).greet(); 是可以正常運行的,並不會報錯,其中greet是類Null的靜態方法
    2. null.greet(); 就是不可行的了,因爲null屬於原始類型,不能這樣做,編譯無法通過。
  • 奇怪的問題
    1. 語言規範不允許一個本地變量聲明語句作爲一條語句在for、while循環中重複執行,但是在語句塊中就可以
    2. 舉例
      while(true) int i=0; // 編譯無法通過。
      while(true) {int i=0;} // 編譯通過
  • 計數
    1. 線程安全的計數器,不要忘記使用AutomicLong哦
    2. int計數到溢出最短只需要21秒,long最短需要9x1010秒(按照每秒計數108算),因此計數最好使用long
  • 實例不可變
    1. String、BigDecimal、BigInteger、所有包裝類的實例都是不可變的。
    2. 即,我們不能修改現有實例的值,對這些實例的操作將產生一個新的實例
    3. 舉例:
      • String str = “e”;
        str.replace(‘e’,‘d’); //執行後str的值並不會改變,要想得到改變後的值:str = str.replace(‘e’,‘d’);
      • BigInteger i = new BigInteger(10);
        i.add(new BigInteger2.); //同樣,執行後不會有變化,這一項是比較容易忽略的,要記住。
  • 無論什麼時,只要我們重寫了equals()方法,就一定要同時重寫hashCode()方法
    原因:HashSet會先根據hashCode()值找元素位置,再調用equals()判斷是否相等。
  • 重寫方法時,最好(必須)加上註解Override,這樣可以避免錯誤。
  • 八進制以0(零)開頭,這一點記住了,012代表八進制,12纔是十進制
  • 新API
    1. Arrays.deepToString() 能夠將多維數組的每一項都打印
    2. 整形的包裝類支持通用的位處理操作:如bitCount(),統計被置位的位數
  • Java平臺的每一個主版本都在其類庫中隱藏了一些寶藏,因此研究Java的新特性頁面有很大好處。
    此外,持續研究Java API也是有好處的。
  • 噁心的日期
    1. Calendar的月是從0開始的。
    2. 使用Calendar時,最好隨時翻看API,因爲很多反人類的API會誤導我們。
  • IdentityHashMap:與其他類不同的是,該Map在比較key時,使用的是引用等價性比較,而不是常用的值比較
  • 取餘
    -2%3 = 1 -2 = -13 + 1 -1爲商,1爲餘
    2%3 = 2 1 = 0
    3 + 2 0爲商,2爲餘
  • 負數的絕對值爲負數的情況
    Math.abs():當參數爲MIN_VALUE時,由於整數的二進制數不具有對稱性,因此最小值的絕對值還是爲該值
  • 比較器的缺陷
    1. 使用Comparator時,我們需要重寫int compare(o1, o2)方法,一般直接返回o1-o2這樣的值,但是如果o1-o2發生了溢出, 可能得到相反的效果
    2. 解決方案是不要在重寫的compare中直接相減,更好的方法是比較,然後轉化爲-1、0、1這樣的數
  • 要儘量避免重用平臺類的名字,千萬不要使用java.lang包中的類名,因爲很多地方自然地用到這些類,當我們自己定義了同名類時,一不小心就讓這些地方用到了我們定義的名字,到時候哭死都找不出問題所在。
  • 隱藏
    1. 當子類與父類存在同名實例變量、靜態方法、成員類型時,子類會將可訪問到的父類的變量隱藏起來,但其依然存在,可以通過將子類對象轉爲父類引用進行調用。
    2. 要避免隱藏,因爲這可能導致混亂
      遮掩
    3. 當一個變量和一個類型具有相同的名字,並且它們位於相同作用域時,變量名具有優先權
    4. 按照標準命名規範時,不會遇到這種問題。
      遮蔽
    5. 指的是本地變量、內部類、方法等,因爲與導入的變量、類、方法等同名,而採用本地的變量、內部類、方法的情況,即本地遮蔽了導入
    6. 更普遍地說:一個變量、方法或類型可以分別遮蔽在一個閉合的文本範圍內的具有相同名字的變量、方法、類型。
    7. 更形象地說:一個閉合範圍內,範圍1包含變量A和範圍2,範圍2包含同名變量A,則在範圍2中的A遮蔽了範圍1中範圍2外的A變量
  • 靜態內部類和非靜態內部類
    1. 靜態內部類能夠擁有自己的靜態成員變量和靜態成員方法
      非靜態內部類不具有上述特徵
    2. 靜態內部類不能訪問外部類的非靜態成員和方法
      非靜態內部類可以訪問外部類的非靜態成員和方法
    3. 靜態內部類在創建時不需要依賴外部類的實例
      Outer.Inner inner = new Outer.Inner();
      非靜態內部類創建實例時需要依賴外部類實例
      Outer.Inner inner = outer.new Outer.Inner();
  • 一個包內私有的方法(即方法的訪問權限是default)不能被位於另一個包中的某個方法直接重寫
    這點要注意,內容在解惑70
  • 靜態導入
    1. 當靜態導入的方法和本類中已有方法重名時,本類中方法具有優先權。識別到本地方法後就不會再使用導入的方法了。
    2. 由此看來,靜態導入也會引起一定的問題,要謹慎使用。
  • final
    1. 修飾成員方法,不能重寫
    2. 修飾靜態方法,不能被隱藏
    3. 修飾域,不管是靜態還是成員的,都只限制其值不能被賦值兩次,沒有其它限制
  • 如果兩個重載的方法能夠接收相同的參數,應該使得它們具有相同的行爲。
  • 三目運算符
    1. 當第二個和第三個操作數爲引用類型時,運算結果的類型是它們的最小公共超類
  • join()方法原理:在表示正在被連接的Thread實例上調用wait()方法,wait()方法會釋放線程鎖
  • 跨包訪問:
    1. 訪問位於其它包中的非公共類型的成員是不合法的。這些成員包括包、方法、域等。
      舉例在公共類A中定義default的靜態內部類B,在包外的類C中執行A.B.hashCode()是非法的,因爲B是defalut的,包外不可訪問。
    2. 這種非法問題,在正常時候編譯就能檢查出來,但是在反射時卻只有運行時才能看出效果。
      因此,在使用反射訪問某個類型時,請保證使用的Class對象對應的類型是可被包外訪問的。
  • 一個非靜態的嵌套類的構造器,在編譯時會將一個隱藏的參數作爲它的第一個參數買這個參數表示了它的直接外圍實例。
    利用反射創建嵌套類對象時,就需要我們顯式地傳遞這個參數了。因此不能使用newInstance()來創建對象。取而代之的是用Constructor。
  • 靜態和非靜態內部類的選擇
    內部類中需要使用直接外圍類時,創建非靜態內部類
    其它情況優先創建靜態內部類
  • 應儘量避免使用反射來實例化內部類。
  • PrintStream
    1. System.out、System.err都是PrintStream類型的。
    2. PrintStream可被創建爲自動刷新的,觸發刷新的條件:
      • 一個字節數組被寫入
      • println()方法被調用
      • 換行字符或者字節被寫入
    3. 注意的是,write(int)方法是唯一一個在自動刷新功能開啓的情況下不刷新PrintStream的輸出方法
    4. 對應3.,System.out.write(int)方法就不會自動刷新
  • 子進程
    1. 由於某些本地平臺只提供有限大小的緩衝,所以如果不能迅速地讀取子進程的輸出流,就有可能會導致子進程的阻塞,甚至死鎖。
    2. 解決方案:人爲地排空子進程的輸出流,即獲取子process的輸入流,將流中的內容全部讀出。
  • 創建一個類的實例的方法
    1. 正常new
    2. 反射創建
    3. 序列化後反序列化
    4. 克隆
  • 對於一個實現了Serializable接口的單件類,必須有一個readResolve方法返回其唯一實例。否則,利用序列化反序列化可以創建出新的實例,從而失去單例
    private Object readResolve() {
    return t;
    }
  • Thread.interrupt() :
    Thread.interrupted():測試當前線程是否中斷,同時清除當前線程的中斷狀態。
    Thread.isInterrupted():測試當前線程是否終端,不會清除中斷狀態。
  • Thread的中斷機制
    1. Java的中斷是一種協作機制。也就是說調用線程對象的interrupt方法並不一定就中斷了正在運行的線程,它只是要求線程自己在合適的
      時機中斷自己。每個線程都有一個boolean的中斷狀態(這個狀態不在Thread的屬性上),interrupt方法僅僅只是將該狀態置爲true。
    2. 對正常運行的線程調用interrupt()並不能終止他,只是改變了interrupt標示符
    3. 如果一個方法聲明拋出InterruptedException,表示該方法是可中斷的,比如wait,sleep,join,也就是說可中斷方法會對interrupt調用
      做出響應
    4. Object.wait,Thread.sleep方法,會不斷的輪詢監聽interrupted標誌位,發現其設置爲true後,會停止阻塞並拋出 InterruptedException
      異常。
  • Java在使用一個類時,總是會先檢查一下其是否已經進行了初始化。若沒有,則進行初始化,若發現正在初始化,則等待其初始化是否已經完成。
  • 將int或long轉換爲float;long轉換爲double時,都會導致精度丟失。要注意:使用
  • List<?> 通配符類型
    1. 與List相比,其實一個參數化類型,需要更強的類型檢查。
    2. 編譯器一般不會允許添加除了null以外的任何元素到List<?>中,而List可添加任意元素
    3. 應該避免直接使用原生類型
  • 原生類型、通配符類型、參數化類型是不同的,不能混用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章