閒聊
一時無聊,在簡書開一個文集,專門記錄一些技術上的偏門知識,少用但有用的知識。也不知道該取什麼名字,就叫轉角遇到愛吧。
有豐富開發經驗的人一定都有體會,真正在項目開發中,實現功能的時間其實並不長,大量的時間是浪費在瞭解決一些稀奇古怪的問題上,有很多冷門、少見的技術我們不知道,知道後才發現這麼神奇,就拿Android開發來說,你用300行代碼實現一個效果,可能一個神奇的屬性就搞定了。而這些冷門的知識我們不常用,用過一次之後很快又忘記了,下次遇到同樣的問題又要Google很久。這就是我決定寫這個系列的原因,把一些冷門的知識彙集起來,方便自己和大家查詢。
1、魔鬼死循環
boolean flag = false;
for (int i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i++) {
flag = !flag;
}
System.out.println(flag);
大多數人第一反應就是:flag初始爲false,執行奇數次就是true,偶數次就是false,所以問題就是for循環執行奇數次還是偶數次。
如果你是這麼想的,那你就上當了,這段代碼的執行結果是死循環。因爲Integer.MAX_VALUE + 1 = Integer.MIN_VALUE。Java中Integer類型值域範圍一旦超過就會回頭,變成最小值,是不是恍然大悟。
System.out.println(Integer.MIN_VALUE);
System.out.println(Integer.MAX_VALUE + 1);
// 執行結果
-2147483648
-2147483648
2、Java7.0的try-with-resources
在 Java7之前,可以使用 finally 塊來確保資源被關閉,不管 try 語句正常地完成或是發生意外。
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
Java7之後,使用try-with-resources語句,當try退出時,會自動調用res.close()方法。不管代碼塊如何退出,只要之前已經被創建出來,它們都會被關閉。那什麼樣的資源會被關閉呢?任何實現了 java.lang.AutoCloseable的對象, 包括所有實現了 java.io.Closeable的對象, 都可以用作一個資源。
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
可以在一個 try-with-resources 語句中聲明一個或多個資源。資源的 close 方法調用順序與它們的創建順序相反。
try (
File file = new File("");
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
) {
// do someting
}
3、數字下劃線分割
我們在顯示大數金額時,通常會用逗號分隔符,便於清晰讀出數值。Java7之後,對於數字也支持下劃線分割。
long one_million = 1_000_000_000;
System.out.println(one_million);
// 輸出結果
1000000000
4、Math.abs()絕對值
你猜下面的代碼打印結果是什麼?
int i = Math.abs(Integer.MIN_VALUE);
System.out.println(i)
結果還是Integer.MIN_VALUE,喫驚吧!
Math.abs()並不是一定會給你返回正數,原因很簡單,就是Integer的值域問題,Integer的最大值是2147483647,最小值是-2147483648,絕對值就是2147483648,還記得前面的那個魔鬼死循環嗎?
Integer.MAX_VALUE + 1 = Integer.MIN_VALUE;就是這麼無語。
5、ArrayList擴容
正常創建一個ArrayList你會這麼寫:
ArrayList a = new ArrayList();
對應ArrayList的無參構造函數:
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
這裏有一個細節,ArrayList 底層採用 Object 類型的數組實現,當使用無參構造方法時,ArrayList 底層會生成一個長度爲 10 的 Object 類型數組,當向 ArrayList 添加對象時,計數加 1,並計算容量是否適當,當存儲的元素個數超過容量時,就會新建一個數組,新數組的長度是原來的1.5倍,然後把原來數組的內容拷貝大新數組。
注意,這個複製操作是非常傷性能的,如果 ArrayList 很大,執行數百次擴容,那麼就會進行更多次數的新數組分配操作,以及更多次數的舊數組回收操作。於是你就會發現性能越來越差,但是又不知道爲什麼。
正因爲如此,所以纔有了第二個構造函數,傳入一個指定值,作爲初始數組的大小。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
}
比如,你預期你的ArrayList至少會保存100個對象,那麼你就使用第二個構造函數,傳入100。這樣,前100次添加都不會有數組的拷貝操作。
ArrayList a = new ArrayList(100);
總結:如果你能預期ArrayList會保存大量的數據,那麼請使用第二個構造函數,傳入一個合適的值作爲初始容量,儘可能避免大量的性能消耗。
6、單例模式
說起單例模式,相信大多數人都能徒手寫出來,畢竟是最簡單的設計模式,所以我主要講一下4種單例寫法的遞進關係。
1、基礎形式
這是最簡單的形式,申明靜態實例的時候直接創建對象。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
這中寫法的缺點很明顯,在初始化類的時候就創建了對象,如果我們沒有用到這個單例,那就是一種浪費。所以我們需要改進。
2、懶惰形式
比起基礎形式,這種形式的好處就是可以在需要的時候初始化實例。
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
雖然解決了延遲初始化的問題,但是還有個明顯的問題就是線程不安全,所以還需要繼續優化。
3、方法鎖形式
在方法上使用synchronized關鍵字就可以處理多個線程同時訪問的問題。每個類實例對應一個線程鎖, synchronized 修飾的方法必須獲得調用該方法的類實例的鎖方能執行, 否則所屬線程阻塞。方法一旦執行, 就獨佔該鎖,直到從該方法返回時纔將鎖釋放。此後被阻塞的線程方能獲得該鎖, 重新進入可執行狀態。
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
4、Class鎖形式
上面的寫法雖然是線程安全的,但是每次調用 getInstance() 方法都需要進行線程鎖定判斷,在多線程高併發訪問環境中,將會導致系統性能下降。事實上,不僅效率很低,99%情況下不需要線程鎖定判斷。這個時候,我們可以通過雙重校驗鎖的方式進行處理。換句話說,利用雙重校驗鎖,第一次檢查是否實例已經創建,如果還沒創建,再進行同步的方式創建單例對象。
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}