Java編碼易疏忽的十個(gè)問題

2012-09-04 18:37:00來源:InfoQ中文站作者:

在Java編碼中,我們?nèi)菀追敢恍╁e(cuò)誤,也容易疏忽一些問題,因此筆者對日常編碼中曾遇到的一些經(jīng)典情形歸納整理成文,以共同探討。

在Java編碼中,我們?nèi)菀追敢恍╁e(cuò)誤,也容易疏忽一些問題,因此筆者對日常編碼中曾遇到的一些經(jīng)典情形歸納整理成文,以共同探討。

1. 糾結(jié)的同名

現(xiàn)象

很多類的命名相同(例如:常見于異常、常量、日志等類),導(dǎo)致在import時(shí),有時(shí)候張冠李戴,這種錯(cuò)誤有時(shí)候很隱蔽。因?yàn)橥念惞δ芤差愃,所以IDE不會提示warn。

解決

寫完代碼時(shí),掃視下import部分,看看有沒有不熟悉的。替換成正確導(dǎo)入后,要注意下注釋是否也作相應(yīng)修改。

啟示

命名盡量避開重復(fù)名,特別要避開與JDK中的類重名,否則容易導(dǎo)入錯(cuò),同時(shí)存在大量重名類,在查找時(shí),也需要更多的辨別時(shí)間。

2. 想當(dāng)然的API

現(xiàn)象

有時(shí)候調(diào)用API時(shí),會想當(dāng)然的通過名字直接自信滿滿地調(diào)用,導(dǎo)致很驚訝的一些錯(cuò)誤:

示例一:flag是true?

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

可能老是false。

示例二:這是去年的今天嗎(今年是2012年)?結(jié)果還是2012年:

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

下面的才是去年:

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

解決辦法

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

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

啟示

名字起的更詳細(xì)點(diǎn),注釋更清楚點(diǎn),不要不經(jīng)了解、測試就想當(dāng)然的用一些API,如果時(shí)間有限,用自己最為熟悉的API。

3. 有時(shí)候溢出并不難

現(xiàn)象

有時(shí)候溢出并不難,雖然不常復(fù)現(xiàn):

示例一:

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

x是多少?竟然是-2147483648,明明加上1之后還是long的范圍。類似的經(jīng)常出現(xiàn)在時(shí)間計(jì)算:

數(shù)字1×數(shù)字2×數(shù)字3… 

示例二:

在檢查是否為正數(shù)的參數(shù)校驗(yàn)中,為了避免重載,選用參數(shù)number, 于是下面代碼結(jié)果小于0,也是因?yàn)橐绯鰧?dǎo)致:

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

解決

  1. 讓第一個(gè)操作數(shù)是long型,例如加上L或者l(不建議小寫字母l,因?yàn)楹蛿?shù)字1太相似了);
  2. 不確定時(shí),還是使用重載吧,即使用doubleValue(),當(dāng)參數(shù)是BigDecimal參數(shù)時(shí),也不能解決問題。

啟示

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

4. 日志跑哪了?

現(xiàn)象

有時(shí)候覺得log都打了,怎么找不到?

示例一:沒有stack trace!

 } catch (Exception ex) {
    log.error(ex);
 }

示例二:找不到log!

} catch (ConfigurationException e) {
    e.printStackTrace();
}

解決

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

啟示

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

5. 遺忘的volatile

現(xiàn)象

在DCL模式中,總是忘記加一個(gè)Volatile。

private static CacheImpl instance;  //lose volatile
public static CacheImpl getInstance() {
    if (instance == null) {
        synchronized (CacheImpl.class) {
            if (instance == null) {
                instance = new CacheImpl (); 
            }
        }
    }
    return instance;
}

解決

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

啟示

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

6. 不要影響彼此

現(xiàn)象

在釋放多個(gè)IO資源時(shí),都會拋出IOException ,于是可能為了省事如此寫:

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();
    }
}

假設(shè)bos關(guān)閉失敗,bis還能關(guān)閉嗎?當(dāng)然不能!

解決辦法

雖然拋出的是同一個(gè)異常,但是還是各自捕獲各的為好。否則第一個(gè)失敗,后一個(gè)面就沒有機(jī)會去釋放資源了。

啟示

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

7. 用斷言取代參數(shù)校驗(yàn)

現(xiàn)象

如題所提,作為防御式編程常用的方式:斷言,寫在產(chǎn)品代碼中做參數(shù)校驗(yàn)等。例如:

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

解決

換成正常的統(tǒng)一的參數(shù)校驗(yàn)方法。因?yàn)閿嘌阅J(rèn)是關(guān)閉的,所以起不起作用完全在于配置,如果采用默認(rèn)配置,經(jīng)歷了eventList != null結(jié)果還沒有起到作用,徒勞無功。

啟示

有的時(shí)候,代碼起不起作用,不僅在于用例,還在于配置,例如斷言是否啟用、log級別等,要結(jié)合真實(shí)環(huán)境做有用編碼。

8. 用戶認(rèn)知負(fù)擔(dān)有時(shí)候很重

現(xiàn)象

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

示例一:

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)

示例二:

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);

示例三:

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

解決

  1. 保持參數(shù)傳遞順序;
  2. remove變成了delete,顯得突兀了點(diǎn), 統(tǒng)一表達(dá)更好;
  3. 保持表達(dá),少縮寫也會看起來流暢點(diǎn)。

啟示

在編碼過程中,不管是參數(shù)的順序還是命名都盡量統(tǒng)一,這樣用戶的認(rèn)知負(fù)擔(dān)會很少,不要要用戶容易犯錯(cuò)或迷惑。例如用枚舉代替string從而不讓用戶迷惑到底傳什么string, 諸如此類。

9. 忽視日志記錄時(shí)機(jī)、級別

現(xiàn)象

存在下面兩則示例:

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

catch (SocketException e)
{
    LOG.error("server error", e);
    throw new ConnectionException(e.getMessage(), e);
}   

示例二:記什么級別日志?

在用戶登錄系統(tǒng)中,每次失敗登錄:

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

解決

  1. 移除日志記錄:在遇到需要re-throw的異常時(shí),如果每個(gè)人都按照先記錄后throw的方式去處理,那么對一個(gè)錯(cuò)誤會記錄太多的日志,所以不推薦如此做;但是如果re-throw出去的exception沒有帶完整的trace( 即cause),那么最好還是記錄下。
  2. 如果惡意登錄,那系統(tǒng)內(nèi)部會出現(xiàn)太多WARN,從而讓管理員誤以為是代碼錯(cuò)誤?梢苑答佊脩粢藻e(cuò)誤,但是不要記錄用戶錯(cuò)誤的行為,除非想達(dá)到控制的目的。

啟示

日志改不改記?記成什么級別?如何記?這些都是問題,一定要根據(jù)具體情況,需要考慮:

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

10. 忘設(shè)初始容量

現(xiàn)象

在JAVA中,我們常用Collection中的Map做Cache,但是我們經(jīng)常會遺忘設(shè)置初始容量。

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

解決

初始容量的影響有多大?拿LinkedHashMap來說,初始容量如果不設(shè)置默認(rèn)是16,超過16×LOAD_FACTOR,會resize(2 * table.length),擴(kuò)大2倍:采用 Entry[] newTable = new Entry[newCapacity]; transfer(newTable),即整個(gè)數(shù)組Copy, 那么對于一個(gè)需要做大容量CACHE來說,從16變成一個(gè)很大的數(shù)量,需要做多少次數(shù)組復(fù)制可想而知。如果初始容量就設(shè)置很大,自然會減少resize, 不過可能會擔(dān)心,初始容量設(shè)置很大時(shí),沒有Cache內(nèi)容仍然會占用過大體積。其實(shí)可以參考以下表格簡單計(jì)算下, 初始時(shí)還沒有cache內(nèi)容, 每個(gè)對象僅僅是4字節(jié)引用而已。

  • 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的過程,如果數(shù)據(jù)量很大,就不能忽視初始容量可以考慮設(shè)置下,否則不僅有頻繁的 resize還容易浪費(fèi)容量。

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

關(guān)鍵詞:Java

贊助商鏈接: