Java編碼易疏忽的十個問題

在Java編碼中,我們容易犯一些錯誤,也容易疏忽一些問題,因此筆者對日常編碼中曾遇到的一些經典情形歸納整理成文,以共同探討。

1. 糾結的同名

  現象

  很多類的命名相同(例如:常見於異常、常量、日誌等類),導致在import時,有時候張冠李戴,這種錯誤有時候很隱蔽。因爲往往同名的類功能也類似,所以IDE不會提示warn。

  解決

  寫完代碼時,掃視下import部分,看看有沒有不熟悉的。替換成正確導入後,要注意下注釋是否也作相應修改。

  啓示

  命名儘量避開重複名,特別要避開與JDK中的類重名,否則容易導入錯,同時存在大量重名類,在查找時,也需要更多的辨別時間。

2. 想當然的API

  現象

  有時候調用API時,會想當然的通過名字直接自信滿滿地調用,導致很驚訝的一些錯誤:

  示例一:flag是true?

1
boolean flag = Boolean.getBoolean("true");

  可能老是false。

  示例二:這是去年的今天嗎(今年是2012年,不考慮閏年)?結果還是2012年:

1
2
Calendar calendar = GregorianCalendar.getInstance();
calendar.roll(Calendar.DAY_OF_YEAR, -365);

  下面的纔是去年:

1
calendar.add(Calendar.DAY_OF_YEAR, -365);

  解決辦法

  問自己幾個問題,這個方法我很熟悉嗎?有沒有類似的API? 區別是什麼?就示例一而言,需要區別的如下:

1
Boolean.valueOf(b) VS Boolean.parseBoolean(b) VS Boolean.getBoolean(b);

  啓示

  名字起的更詳細點,註釋更清楚點,不要不經瞭解、測試就想當然的用一些API,如果時間有限,用自己最爲熟悉的API。

3. 有時候溢出並不難

  現象

  有時候溢出並不難,雖然不常復現:

  示例一:

1
2
long x=Integer.MAX_VALUE+1;
System.out.println(x);

  x是多少?竟然是-2147483648,明明加上1之後還是long的範圍。類似的經常出現在時間計算:

  數字1×數字2×數字3…

  示例二:

  在檢查是否爲正數的參數校驗中,爲了避免重載,選用參數number, 於是下面代碼結果小於0,也是因爲溢出導致:

1
2
Number i=Long.MAX_VALUE;
System.out.println(i.intValue()>0);

  解決

  1. 讓第一個操作數是long型,例如加上L或者l(不建議小寫字母l,因爲和數字1太相似了);
  2. 不確定時,還是使用重載吧,即使用doubleValue(),當參數是BigDecimal參數時,也不能解決問題。

  啓示

  對數字運用要保持敏感:涉及數字計算就要考慮溢出;涉及除法就要考慮被除數是0;實在容納不下了可以考慮BigDecimal之類。

4. 日誌跑哪了?

  現象

  有時候覺得log都打了,怎麼找不到?

  示例一:沒有stack trace!

1
2
3
} catch (Exception ex) {
   log.error(ex);
}

  示例二:找不到log!

1
2
3
} catch (ConfigurationException e) {
    e.printStackTrace();
}

  解決

  1. 替換成log.error(ex.getMessage(),ex);
  2. 換成普通的log4j吧,而不是System.out。

  啓示

  1. API定義應該避免讓人犯錯,如果多加個重載的log.error(Exception)自然沒有錯誤發生
  2. 在產品代碼中,使用的一些方法要考慮是否有效,使用e.printStackTrace()要想下終端(Console)在哪。

5. 遺忘的volatile

  現象

  在DCL模式中,總是忘記加一個Volatile。

1
2
3
4
5
6
7
8
9
10
11
private static CacheImpl instance;  //lose volatile
public static CacheImpl getInstance() {
    if (instance == null) {
        synchronized (CacheImpl.class) {
            if (instance == null) {
                instance = new CacheImpl ();
            }
        }
    }
    return instance;
}

  解決

  毋庸置疑,加上一個吧,synchronized 鎖的是一塊代碼(整個方法或某個代碼塊),保證的是這”塊“代碼的可見性及原子性,但是instance == null第一次判斷時不再範圍內的。所以可能讀出的是過期的null。

  啓示

  我們總是覺得某些低概率的事件很難發生,例如某個時間併發的可能性、某個異常拋出的可能性,所以不加控制,但是如果可以,還是按照前人的“最佳實踐”來寫代碼吧。至少不用過多解釋爲啥另闢蹊徑。

6. 不要影響彼此

  現象

  在釋放多個IO資源時,都會拋出IOException ,於是可能爲了省事如此寫:

1
2
3
4
5
6
7
8
9
10
public static void inputToOutput(InputStream is, OutputStream os,
           boolean isClose) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(is, 1024);
    BufferedOutputStream bos = new BufferedOutputStream(os, 1024); 
    ….
    if (isClose) {
       bos.close();
       bis.close();
    }
}

  假設bos關閉失敗,bis還能關閉嗎?當然不能!

  解決辦法

  雖然拋出的是同一個異常,但是還是各自捕獲各的爲好。否則第一個失敗,後一個面就沒有機會去釋放資源了。

  啓示

  代碼/模塊之間可能存在依賴,要充分識別對相互的依賴。

7. 用斷言取代參數校驗

  現象

  如題所提,作爲防禦式編程常用的方式:斷言,寫在產品代碼中做參數校驗等。例如:

1
2
3
private void send(List< Event> eventList)  {
    assert eventList != null;
}

  解決

  換成正常的統一的參數校驗方法。因爲斷言默認是關閉的,所以起不起作用完全在於配置,如果採用默認配置,經歷了eventList != null結果還沒有起到作用,徒勞無功。

  啓示

  有的時候,代碼起不起作用,不僅在於用例,還在於配置,例如斷言是否啓用、log級別等,要結合真實環境做有用編碼。

8. 用戶認知負擔有時候很重

  現象

  先來比較三組例子,看看那些看着更順暢?

  示例一:

1
2
3
4
5
6
public void caller(int a, String b, float c, String d) {
    methodOne(d, z, b);
    methodTwo(b, c, d);
}
public void methodOne(String d, float z, String b) 
public void methodTwo(String b, float c, String d)

  示例二:

1
2
3
4
5
public boolean remove(String key, long timeout) {
             Future< Boolean> future = memcachedClient.delete(key);
public boolean delete(String key, long timeout) {
             Future< Boolean> future = memcachedClient.delete(key);

  示例三:

1
2
public static String getDigest(String filePath, DigestAlgorithm algorithm)
public static String getDigest(String filePath, DigestAlgorithm digestAlgorithm)

  解決

  1. 保持參數傳遞順序;
  2. remove變成了delete,顯得突兀了點, 統一表達更好;
  3. 保持表達,少縮寫也會看起來流暢點。

  啓示

  在編碼過程中,不管是參數的順序還是命名都儘量統一,這樣用戶的認知負擔會很少,不要要用戶容易犯錯或迷惑。例如用枚舉代替string從而不讓用戶迷惑到底傳什麼string, 諸如此類。

9. 忽視日誌記錄時機、級別

  現象

  存在下面兩則示例:

  示例一:該不該記錄日誌?

1
2
3
4
5
catch (SocketException e)
{
    LOG.error("server error", e);
    throw new ConnectionException(e.getMessage(), e);
}  

  示例二:記什麼級別日誌?

  在用戶登錄系統中,每次失敗登錄:

1
LOG.warn("Failed to login by "+username+");

  解決

  1. 移除日誌記錄:在遇到需要re-throw的異常時,如果每個人都按照先記錄後throw的方式去處理,那麼對一個錯誤會記錄太多的日誌,所以不推薦如此做;但是如果re-throw出去的exception沒有帶完整的trace( 即cause),那麼最好還是記錄下。
  2. 如果惡意登錄,那系統內部會出現太多WARN,從而讓管理員誤以爲是代碼錯誤。可以反饋用戶以錯誤,但是不要記錄用戶錯誤的行爲,除非想達到控制的目的。

  啓示

  日誌改不改記?記成什麼級別?如何記?這些都是問題,一定要根據具體情況,需要考慮:

  1. 是用戶行爲錯誤還是代碼錯誤?
  2. 記錄下來的日誌,能否能給別人在不造成過多的干擾前提下提供有用的信息以快速定位問題。

10. 忘設初始容量

  現象

  在JAVA中,我們常用Collection中的Map做Cache,但是我們經常會遺忘設置初始容量。

1
cache = new LRULinkedHashMap< K, V>(maxCapacity);

  解決

  初始容量的影響有多大?拿LinkedHashMap來說,初始容量如果不設置默認是16,超過16×LOAD_FACTOR,會resize(2 * table.length),擴大2倍:採用 Entry[] newTable = new Entry[newCapacity]; transfer(newTable),即整個數組Copy, 那麼對於一個需要做大容量CACHE來說,從16變成一個很大的數量,需要做多少次數組複製可想而知。如果初始容量就設置很大,自然會減少resize, 不過可能會擔心,初始容量設置很大時,沒有Cache內容仍然會佔用過大體積。其實可以參考以下表格簡單計算下, 初始時還沒有cache內容, 每個對象僅僅是4字節引用而已。

  • memory for reference fields (4 bytes each);
  • memory for primitive fields
Java type Bytes required
boolean 1
byte  
char 2
short  
int 4
float  
long 8
double  

  啓示

  不僅是map, 還有stringBuffer等,都有容量resize的過程,如果數據量很大,就不能忽視初始容量可以考慮設置下,否則不僅有頻繁的 resize還容易浪費容量。

  在Java編程中,除了上面枚舉的一些容易忽視的問題,日常實踐中還存在很多。相信通過不斷的總結和努力,可以將我們的程序完美呈現給讀者。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章