新聞中心
每天在寫Java程序,其實(shí)里面有一些細(xì)節(jié)大家可能沒怎么注意,這不,有人總結(jié)了一個(gè)我們編程中常見的問題。雖然一般沒有什么大問題,但是最好別這樣做。另外這里提到的很多問題其實(shí)可以通過Findbugs( http://findbugs.sourceforge.net/ )來幫我們進(jìn)行檢查出來。

廣靈網(wǎng)站建設(shè)公司成都創(chuàng)新互聯(lián)公司,廣靈網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為廣靈上千家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站制作要多少錢,請找那個(gè)售后服務(wù)好的廣靈做網(wǎng)站的公司定做!
字符串連接誤用
錯(cuò)誤的寫法:
- String s = "";
- for (Person p : persons) {
- s += ", " + p.getName();
- }
- s = s.substring(2); //remove first comma
正確的寫法:
- StringBuilder sb = new StringBuilder(persons.size() * 16); // well estimated buffer
- for (Person p : persons) {
- if (sb.length() > 0) sb.append(", ");
- sb.append(p.getName);
- }
錯(cuò)誤的使用StringBuffer
錯(cuò)誤的寫法:
- StringBuffer sb = new StringBuffer();
- sb.append("Name: ");
- sb.append(name + '\n');
- sb.append("!");
- ...
- String s = sb.toString();
問題在第三行,append char比String性能要好,另外就是初始化StringBuffer沒有指定size,導(dǎo)致中間append時(shí)可能重新調(diào)整內(nèi)部數(shù)組大小。如果是JDK1.5最好用StringBuilder取代StringBuffer,除非有線程安全的要求。還有一種方式就是可以直接連接字符串。缺點(diǎn)就是無法初始化時(shí)指定長度。
正確的寫法:
- StringBuilder sb = new StringBuilder(100);
- sb.append("Name: ");
- sb.append(name);
- sb.append("\n!");
- String s = sb.toString();
或者這樣寫:
- String s = "Name: " + name + "\n!";
測試字符串相等性
錯(cuò)誤的寫法:
- if (name.compareTo("John") == 0) ...
- if (name == "John") ...
- if (name.equals("John")) ...
- if ("".equals(name)) ...
上面的代碼沒有錯(cuò),但是不夠好。compareTo不夠簡潔,==原義是比較兩個(gè)對象是否一樣。另外比較字符是否為空,最好判斷它的長度。
正確的寫法:
- if ("John".equals(name)) ...
- if (name.length() == 0) ...
- if (name.isEmpty()) ...
數(shù)字轉(zhuǎn)換成字符串
錯(cuò)誤的寫法:
- "" + set.size()
- new Integer(set.size()).toString()
正確的寫法:
- String.valueOf(set.size())
利用不可變對象(Immutable)
錯(cuò)誤的寫法:
- zero = new Integer(0);
- return Boolean.valueOf("true");
正確的寫法:
- zero = Integer.valueOf(0);
- return Boolean.TRUE;
請使用XML解析器
錯(cuò)誤的寫法:
- int start = xml.indexOf("
") + " ".length(); - int end = xml.indexOf("");
- String name = xml.substring(start, end);
正確的寫法:
- SAXBuilder builder = new SAXBuilder(false);
- Document doc = doc = builder.build(new StringReader(xml));
- String name = doc.getRootElement().getChild("name").getText();
請使用JDom組裝XML
錯(cuò)誤的寫法:
- String name = ...
- String attribute = ...
- String xml = "
" - +"
"+ name +" "- +"";
正確的寫法:
- Element root = new Element("root");
- root.setAttribute("att", attribute);
- root.setText(name);
- Document doc = new Documet();
- doc.setRootElement(root);
- XmlOutputter out = new XmlOutputter(Format.getPrettyFormat());
- String xml = out.outputString(root);
XML編碼陷阱
錯(cuò)誤的寫法:
- String xml = FileUtils.readTextFile("my.xml");
因?yàn)閤ml的編碼在文件中指定的,而在讀文件的時(shí)候必須指定編碼。另外一個(gè)問題不能一次就將一個(gè)xml文件用String保存,這樣對內(nèi)存會(huì)造成不必要的浪費(fèi),正確的做法用InputStream來邊讀取邊處理。為了解決編碼的問題, 最好使用XML解析器來處理。
未指定字符編碼
錯(cuò)誤的寫法:
- Reader r = new FileReader(file);
- Writer w = new FileWriter(file);
- Reader r = new InputStreamReader(inputStream);
- Writer w = new OutputStreamWriter(outputStream);
- String s = new String(byteArray); // byteArray is a byte[]
- byte[] a = string.getBytes();
這樣的代碼主要不具有跨平臺(tái)可移植性。因?yàn)椴煌钠脚_(tái)可能使用的是不同的默認(rèn)字符編碼。
正確的寫法:
- Reader r = new InputStreamReader(new FileInputStream(file), "ISO-8859-1");
- Writer w = new OutputStreamWriter(new FileOutputStream(file), "ISO-8859-1");
- Reader r = new InputStreamReader(inputStream, "UTF-8");
- Writer w = new OutputStreamWriter(outputStream, "UTF-8");
- String s = new String(byteArray, "ASCII");
- byte[] a = string.getBytes("ASCII");
未對數(shù)據(jù)流進(jìn)行緩存
錯(cuò)誤的寫法:
- InputStream in = new FileInputStream(file);
- int b;
- while ((b = in.read()) != -1) {
- ...
- }
上面的代碼是一個(gè)byte一個(gè)byte的讀取,導(dǎo)致頻繁的本地JNI文件系統(tǒng)訪問,非常低效,因?yàn)檎{(diào)用本地方法是非常耗時(shí)的。最好用BufferedInputStream包裝一下。曾經(jīng)做過一個(gè)測試,從/dev/zero下讀取1MB,大概花了1s,而用BufferedInputStream包裝之后只需要60ms,性能提高了94%! 這個(gè)也適用于output stream操作以及socket操作。
正確的寫法:
- InputStream in = new BufferedInputStream(new FileInputStream(file));
無限使用heap內(nèi)存
錯(cuò)誤的寫法:
- byte[] pdf = toPdf(file);
這里有一個(gè)前提,就是文件大小不能講JVM的heap撐爆。否則就等著OOM吧,尤其是在高并發(fā)的服務(wù)器端代碼。最好的做法是采用Stream的方式邊讀取邊存儲(chǔ)(本地文件或database)。
正確的寫法:
- File pdf = toPdf(file);
另外,對于服務(wù)器端代碼來說,為了系統(tǒng)的安全,至少需要對文件的大小進(jìn)行限制。
不指定超時(shí)時(shí)間
錯(cuò)誤的代碼:
- Socket socket = ...
- socket.connect(remote);
- InputStream in = socket.getInputStream();
- int i = in.read();
這種情況在工作中已經(jīng)碰到不止一次了。個(gè)人經(jīng)驗(yàn)一般超時(shí)不要超過20s。這里有一個(gè)問題,connect可以指定超時(shí)時(shí)間,但是read無法指定超時(shí)時(shí)間。但是可以設(shè)置阻塞(block)時(shí)間。
正確的寫法:
- Socket socket = ...
- socket.connect(remote, 20000); // fail after 20s
- InputStream in = socket.getInputStream();
- socket.setSoTimeout(15000);
- int i = in.read();
另外,文件的讀取(FileInputStream, FileChannel, FileDescriptor, File)沒法指定超時(shí)時(shí)間, 而且IO操作均涉及到本地方法調(diào)用, 這個(gè)更操作了JVM的控制范圍,在分布式文件系統(tǒng)中,對IO的操作內(nèi)部實(shí)際上是網(wǎng)絡(luò)調(diào)用。一般情況下操作60s的操作都可以認(rèn)為已經(jīng)超時(shí)了。為了解決這些問題,一般采用緩存和異步/消息隊(duì)列處理。
頻繁使用計(jì)時(shí)器
錯(cuò)誤代碼:
- for (...) {
- long t = System.currentTimeMillis();
- long t = System.nanoTime();
- Date d = new Date();
- Calendar c = new GregorianCalendar();
- }
每次new一個(gè)Date或Calendar都會(huì)涉及一次本地調(diào)用來獲取當(dāng)前時(shí)間(盡管這個(gè)本地調(diào)用相對其他本地方法調(diào)用要快)。
如果對時(shí)間不是特別敏感,這里使用了clone方法來新建一個(gè)Date實(shí)例。這樣相對直接new要高效一些。
正確的寫法:
- Date d = new Date();
- for (E entity : entities) {
- entity.doSomething();
- entity.setUpdated((Date) d.clone());
- }
如果循環(huán)操作耗時(shí)較長(超過幾ms),那么可以采用下面的方法,立即創(chuàng)建一個(gè)Timer,然后定期根據(jù)當(dāng)前時(shí)間更新時(shí)間戳,在我的系統(tǒng)上比直接new一個(gè)時(shí)間對象快200倍:
- private volatile long time;
- Timer timer = new Timer(true);
- try {
- time = System.currentTimeMillis();
- timer.scheduleAtFixedRate(new TimerTask() {
- public void run() {
- time = System.currentTimeMillis();
- }
- }, 0L, 10L); // granularity 10ms
- for (E entity : entities) {
- entity.doSomething();
- entity.setUpdated(new Date(time));
- }
- } finally {
- timer.cancel();
- }
捕獲所有的異常
錯(cuò)誤的寫法:
- Query q = ...
- Person p;
- try {
- p = (Person) q.getSingleResult();
- } catch(Exception e) {
- p = null;
- }
這是EJB3的一個(gè)查詢操作,可能出現(xiàn)異常的原因是:結(jié)果不唯一;沒有結(jié)果;數(shù)據(jù)庫無法訪問,而捕獲所有的異常,設(shè)置為null將掩蓋各種異常情況。
正確的寫法:
- Query q = ...
- Person p;
- try {
- p = (Person) q.getSingleResult();
- } catch(NoResultException e) {
- p = null;
- }
忽略所有異常
錯(cuò)誤的寫法:
- try {
- doStuff();
- } catch(Exception e) {
- log.fatal("Could not do stuff");
- }
- doMoreStuff();
這個(gè)代碼有兩個(gè)問題, 一個(gè)是沒有告訴調(diào)用者, 系統(tǒng)調(diào)用出錯(cuò)了. 第二個(gè)是日志沒有出錯(cuò)原因, 很難跟蹤定位問題。
正確的寫法:
- try {
- doStuff();
- } catch(Exception e) {
- throw new MyRuntimeException("Could not do stuff because: "+ e.getMessage, e);
- }
重復(fù)包裝RuntimeException
錯(cuò)誤的寫法:
- try {
- doStuff();
- } catch(Exception e) {
- throw new RuntimeException(e);
- }
正確的寫法:
- try {
- doStuff();
- } catch(RuntimeException e) {
- throw e;
- } catch(Exception e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- try {
- doStuff();
- } catch(IOException e) {
- throw new RuntimeException(e.getMessage(), e);
- } catch(NamingException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
不正確的傳播異常
錯(cuò)誤的寫法:
- try {
- } catch(ParseException e) {
- throw new RuntimeException();
- throw new RuntimeException(e.toString());
- throw new RuntimeException(e.getMessage());
- throw new RuntimeException(e);
- }
主要是沒有正確的將內(nèi)部的錯(cuò)誤信息傳遞給調(diào)用者. 第一個(gè)完全丟掉了內(nèi)部錯(cuò)誤信息, 第二個(gè)錯(cuò)誤信息依賴toString方法, 如果沒有包含最終的嵌套錯(cuò)誤信息, 也會(huì)出現(xiàn)丟失, 而且可讀性差. 第三個(gè)稍微好一些, 第四個(gè)跟第二個(gè)一樣。
正確的寫法:
- try {
- } catch(ParseException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
用日志記錄異常
錯(cuò)誤的寫法:
- try {
- ...
- } catch(ExceptionA e) {
- log.error(e.getMessage(), e);
- throw e;
- } catch(ExceptionB e) {
- log.error(e.getMessage(), e);
- throw e;
- }
一般情況下在日志中記錄異常是不必要的, 除非調(diào)用方?jīng)]有記錄日志。
異常處理不徹底
錯(cuò)誤的寫法:
- try {
- is = new FileInputStream(inFile);
- os = new FileOutputStream(outFile);
- } finally {
- try {
- is.close();
- os.close();
- } catch(IOException e) {
- /* we can't do anything */
- }
- }
is可能close失敗, 導(dǎo)致os沒有close
正確的寫法:
- try {
- is = new FileInputStream(inFile);
- os = new FileOutputStream(outFile);
- } finally {
- try { if (is != null) is.close(); } catch(IOException e) {/* we can't do anything */}
- try { if (os != null) os.close(); } catch(IOException e) {/* we can't do anything */}
- }
捕獲不可能出現(xiàn)的異常
錯(cuò)誤的寫法:
- try {
- ... do risky stuff ...
- } catch(SomeException e) {
- // never happens
- }
- ... do some more ...
正確的寫法:
- try {
- ... do risky stuff ...
- } catch(SomeException e) {
- // never happens hopefully
- throw new IllegalStateException(e.getMessage(), e); // crash early, passing all information
- }
- ... do some more ...
transient的誤用
錯(cuò)誤的寫法:
- public class A implements Serializable {
- private String someState;
- private transient Log log = LogFactory.getLog(getClass());
- public void f() {
- log.debug("enter f");
- ...
- }
- }
這里的本意是不希望Log對象被序列化. 不過這里在反序列化時(shí), 會(huì)因?yàn)閘og未初始化, 導(dǎo)致f()方法拋空指針, 正確的做法是將log定義為靜態(tài)變量或者定位為具備變量。
正確的寫法:
- public class A implements Serializable {
- private String someState;
- private static final Log log = LogFactory.getLog(A.class);
- public void f() {
- log.debug("enter f");
- ...
- }
- }
- public class A implements Serializable {
- private String someState;
- public void f() {
- Log log = LogFactory.getLog(getClass());
- log.debug("enter f");
- ...
- }
- }
不必要的初始化
錯(cuò)誤的寫法:
- public class B {
- private int count = 0;
- private String name = null;
- private boolean important = false;
- }
這里的變量會(huì)在初始化時(shí)使用默認(rèn)值:0, null, false, 因此上面的寫法有些多此一舉。
正確的寫法:
- public class B {
- private int count;
- private String name;
- private boolean important;
- }
最好用靜態(tài)final定義Log變量
- private static final Log log = LogFactory.getLog(MyClass.class);
這樣做的好處有三:
- 可以保證線程安全
- 靜態(tài)或非靜態(tài)代碼都可用
- 不會(huì)影響對象序列化
選擇錯(cuò)誤的類加載器
錯(cuò)誤的代碼:
- Class clazz = Class.forName(name);
- Class clazz = getClass().getClassLoader().loadClass(name);
這里本意是希望用當(dāng)前類來加載希望的對象, 但是這里的getClass()可能拋出異常, 特別在一些受管理的環(huán)境中, 比如應(yīng)用服務(wù)器, web容器, Java WebStart環(huán)境中, 最好的做法是使用當(dāng)前應(yīng)用上下文的類加載器來加載。
正確的寫法:
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- if (cl == null) cl = MyClass.class.getClassLoader(); // fallback
- Class clazz = cl.loadClass(name);
反射使用不當(dāng)
錯(cuò)誤的寫法:
- Class beanClass = ...
- if (beanClass.newInstance() instanceof TestBean) ...
這里的本意是檢查beanClass是否是TestBean或是其子類, 但是創(chuàng)建一個(gè)類實(shí)例可能沒那么簡單, 首先實(shí)例化一個(gè)對象會(huì)帶來一定的消耗, 另外有可能類沒有定義默認(rèn)構(gòu)造函數(shù). 正確的做法是用Class.isAssignableFrom(Class) 方法。
正確的寫法:
- Class beanClass = ...
- if (TestBean.class.isAssignableFrom(beanClass)) ...
不必要的同步
錯(cuò)誤的寫法:
- Collection l = new Vector();
- for (...) {
- l.add(object);
- }
Vector是ArrayList同步版本。
正確的寫法:
- Collection l = new ArrayList();
- for (...) {
- l.add(object);
- }
錯(cuò)誤的選擇List類型
根據(jù)下面的表格數(shù)據(jù)來進(jìn)行選擇
| ArrayList | LinkedList | |
| add (append) | O(1) or ~O(log(n)) if growing | O(1) |
| insert (middle) | O(n) or ~O(n*log(n)) if growing | O(n) |
| remove (middle) | O(n) (always performs complete copy) | O(n) |
| iterate | O(n) | O(n) |
| get by index | O(1) | O(n) |
HashMap size陷阱
錯(cuò)誤的寫法:
- Map map = new HashMap(collection.size());
- for (Object o : collection) {
- map.put(o.key, o.value);
- }
這里可以參考guava的Maps.newHashMapWithExpectedSize的實(shí)現(xiàn). 用戶的本意是希望給HashMap設(shè)置初始值, 避免擴(kuò)容(resize)的開銷. 但是沒有考慮當(dāng)添加的元素?cái)?shù)量達(dá)到HashMap容量的75%時(shí)將出現(xiàn)resize。
正確的寫法:
- Map map = new HashMap(1 + (int) (collection.size() / 0.75));
對Hashtable, HashMap 和 HashSet了解不夠
這里主要需要了解HashMap和Hashtable的內(nèi)部實(shí)現(xiàn)上, 它們都使用Entry包裝來封裝key/value, Entry內(nèi)部除了要保存Key/Value的引用, 還需要保存hash桶中next Entry的應(yīng)用, 因此對內(nèi)存會(huì)有不小的開銷, 而HashSet內(nèi)部實(shí)現(xiàn)其實(shí)就是一個(gè)HashMap. 有時(shí)候IdentityHashMap可以作為一個(gè)不錯(cuò)的替代方案. 它在內(nèi)存使用上更有效(沒有用Entry封裝, 內(nèi)部采用Object[]). 不過需要小心使用. 它的實(shí)現(xiàn)違背了Map接口的定義. 有時(shí)候也可以用ArrayList來替換HashSet.
這一切的根源都是由于JDK內(nèi)部沒有提供一套高效的Map和Set實(shí)現(xiàn)。
對List的誤用
建議下列場景用Array來替代List:
- list長度固定,比如一周中的每一天
- 對list頻繁的遍歷,比如超過1w次
- 需要對數(shù)字進(jìn)行包裝(主要JDK沒有提供基本類型的List)
比如下面的代碼。
錯(cuò)誤的寫法:
- List
codes = new ArrayList (); - codes.add(Integer.valueOf(10));
- codes.add(Integer.valueOf(20));
- codes.add(Integer.valueOf(30));
- codes.add(Integer.valueOf(40));
正確的寫法:
- int[] codes = { 10, 20, 30, 40 };
錯(cuò)誤的寫法:
- // horribly slow and a memory waster if l has a few thousand elements (try it yourself!)
- List
l = ...; - for (int i=0; i < l.size()-1; i++) {
- Mergeable one = l.get(i);
- Iterator
j = l.iterator(i+1); // memory allocation! - while (j.hasNext()) {
- Mergeable other = l.next();
- if (one.canMergeWith(other)) {
- one.merge(other);
- other.remove();
- }
- }
- }
正確的寫法:
- // quite fast and no memory allocation
- Mergeable[] l = ...;
- for (int i=0; i < l.length-1; i++) {
- Mergeable one = l[i];
- for (int j=i+1; j < l.length; j++) {
- Mergeable other = l[j];
- if (one.canMergeWith(other)) {
- one.merge(other);
- l[j] = null;
- }
- }
- }
實(shí)際上Sun也意識(shí)到這一點(diǎn), 因此在JDK中, Collections.sort()就是將一個(gè)List拷貝到一個(gè)數(shù)組中然后調(diào)用Arrays.sort方法來執(zhí)行排序。
用數(shù)組來描述一個(gè)結(jié)構(gòu)
錯(cuò)誤用法:
- /**
- * @returns [1]: Location, [2]: Customer, [3]: Incident
- */
- Object[] getDetails(int id) {...
這里用數(shù)組+文檔的方式來描述一個(gè)方法的返回值. 雖然很簡單, 但是很容易誤用, 正確的做法應(yīng)該是定義個(gè)類。
正確的寫法:
- Details getDetails(int id) {...}
- private class Details {
- public Location location;
- public Customer customer;
- public Incident incident;
- }
對方法過度限制
錯(cuò)誤用法:
- public void notify(Person p) {
- ...
- sendMail(p.getName(), p.getFirstName(), p.getEmail());
- ...
- }
- class PhoneBook {
- String lookup(String employeeId) {
- Employee emp = ...
- return emp.getPhone();
- }
- }
第一個(gè)例子是對方法參數(shù)做了過多的限制, 第二個(gè)例子對方法的返回值做了太多的限制。
正確的寫法:
- public void notify(Person p) {
- ...
- sendMail(p);
- ...
- }
- class EmployeeDirectory {
- Employee lookup(String employeeId) {
- Employee emp = ...
- return emp;
- }
- }
對POJO的setter方法畫蛇添足
錯(cuò)誤的寫法:
- private String name;
- public void setName(String name) {
- this.name = name.trim();
- }
- public void String getName() {
- return this.name;
- }
有時(shí)候我們很討厭字符串首尾出現(xiàn)空格, 所以在setter方法中進(jìn)行了trim處理, 但是這樣做的結(jié)果帶來的副作用會(huì)使getter方法的返回值和setter方法不一致, 如果只是將JavaBean當(dāng)做一個(gè)數(shù)據(jù)容器, 那么最好不要包含任何業(yè)務(wù)邏輯. 而將業(yè)務(wù)邏輯放到專門的業(yè)務(wù)層或者控制層中處理。
正確的做法:
- person.setName(textInput.getText().trim());
日歷對象(Calendar)誤用
錯(cuò)誤的寫法:
- Calendar cal = new GregorianCalender(TimeZone.getTimeZone("Europe/Zurich"));
- cal.setTime(date);
- cal.add(Calendar.HOUR_OF_DAY, 8);
- date = cal.getTime();
這里主要是對date, time, calendar和time zone不了解導(dǎo)致. 而在一個(gè)時(shí)間上增加8小時(shí), 跟time zone沒有任何關(guān)系, 所以沒有必要使用Calendar, 直接用Date對象即可, 而如果是增加天數(shù)的話, 則需要使用Calendar, 因?yàn)椴捎貌煌臅r(shí)令制可能一天的小時(shí)數(shù)是不同的(比如有些DST是23或者25個(gè)小時(shí))
正確的寫法:
- date = new Date(date.getTime() + 8L * 3600L * 1000L); // add 8 hrs
TimeZone的誤用
錯(cuò)誤的寫法:
- Calendar cal = new GregorianCalendar();
- cal.setTime(date);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- Date startOfDay = cal.getTime();
這里有兩個(gè)錯(cuò)誤, 一個(gè)是沒有沒有將毫秒歸零, 不過最大的錯(cuò)誤是沒有指定TimeZone, 不過一般的桌面應(yīng)用沒有問題, 但是如果是服務(wù)器端應(yīng)用則會(huì)有一些問題, 比如同一時(shí)刻在上海和倫敦就不一樣, 因此需要指定的TimeZone.
正確的寫法:
- Calendar cal = new GregorianCalendar(user.getTimeZone());
- cal.setTime(date);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- Date startOfDay = cal.getTime();
時(shí)區(qū)(Time Zone)調(diào)整的誤用
錯(cuò)誤的寫法:
- public static Date convertTz(Date date, TimeZone tz) {
- Calendar cal = Calendar.getInstance();
- cal.setTimeZone(TimeZone.getTimeZone("UTC"));
- cal.setTime(date);
- cal.setTimeZone(tz);
- return cal.getTime();
- }
這個(gè)方法實(shí)際上沒有改變時(shí)間, 輸入和輸出是一樣的. 關(guān)于時(shí)間的問題可以參考這篇文章: http://www.odi.ch/prog/design/datetime.php 這里主要的問題是Date對象并不包含Time Zone信息. 它總是使用UTC(世界統(tǒng)一時(shí)間). 而調(diào)用Calendar的getTime/setTime方法會(huì)自動(dòng)在當(dāng)前時(shí)區(qū)和UTC之間做轉(zhuǎn)換。
Calendar.getInstance()的誤用
錯(cuò)誤的寫法:
- Calendar c = Calendar.getInstance();
- c.set(2009, Calendar.JANUARY, 15);
Calendar.getInstance()依賴local來選擇一個(gè)Calendar實(shí)現(xiàn), 不同實(shí)現(xiàn)的2009年是不同的, 比如有些Calendar實(shí)現(xiàn)就沒有January月份。
正確的寫法:
- Calendar c = new GregorianCalendar(timeZone);
- c.set(2009, Calendar.JANUARY, 15);
Date.setTime()的誤用
錯(cuò)誤的寫法:
- account.changePassword(oldPass, newPass);
- Date lastmod = account.getLastModified();
- lastmod.setTime(System.currentTimeMillis());
在更新密碼之后, 修改一下最后更新時(shí)間, 這里的用法沒有錯(cuò),但是有更好的做法: 直接傳Date對象. 因?yàn)镈ate是Value Object, 不可變的. 如果更新了Date的值, 實(shí)際上是生成一個(gè)新的Date實(shí)例. 這樣其他地方用到的實(shí)際上不在是原來的對象, 這樣可能出現(xiàn)不可預(yù)知的異常. 當(dāng)然這里又涉及到另外一個(gè)OO設(shè)計(jì)的問題, 對外暴露Date實(shí)例本身就是不好的做法(一般的做法是在setter方法中設(shè)置Date引用參數(shù)的clone對象). 另外一種比較好的做法就是直接保存long類型的毫秒數(shù)。
正確的做法:
- account.changePassword(oldPass, newPass);
- account.setLastModified(new Date());
SimpleDateFormat非線程安全誤用
錯(cuò)誤的寫法:
- public class Constants {
- public static final SimpleDateFormat date = new SimpleDateFormat("dd.MM.yyyy");
- }
SimpleDateFormat不是線程安全的. 在多線程并行處理的情況下, 會(huì)得到非預(yù)期的值. 這個(gè)錯(cuò)誤非常普遍! 如果真要在多線程環(huán)境下公用同一個(gè)SimpleDateFormat, 那么做好做好同步(cache flush, lock contention), 但是這樣會(huì)搞得更復(fù)雜, 還不如直接new一個(gè)實(shí)在。
使用全局參數(shù)配置常量類/接口
- public interface Constants {
- String version = "1.0";
- String dateFormat = "dd.MM.yyyy";
- String configFile = ".apprc";
- int maxNameLength = 32;
- String someQuery = "SELECT * FROM ...";
- }
很多應(yīng)用都會(huì)定義這樣一個(gè)全局常量類或接口, 但是為什么這種做法不推薦? 因?yàn)檫@些常量之間基本沒有任何關(guān)聯(lián), 只是因?yàn)楣貌哦x在一起. 但是如果其他組件需要使用這些全局變量, 則必須對該常量類產(chǎn)生依賴, 特別是存在server和遠(yuǎn)程client調(diào)用的場景。
比較好的做法是將這些常量定義在組件內(nèi)部. 或者局限在一個(gè)類庫內(nèi)部。
忽略造型溢出(cast overflow)
錯(cuò)誤的寫法:
- public int getFileSize(File f) {
- long l = f.length();
- return (int) l;
- }
這個(gè)方法的本意是不支持傳遞超過2GB的文件. 最好的做法是對長度進(jìn)行檢查, 溢出時(shí)拋出異常。
正確的寫法:
- public int getFileSize(File f) {
- long l = f.length();
- if (l > Integer.MAX_VALUE) throw new IllegalStateException("int overflow");
- return (int) l;
- }
另一個(gè)溢出bug是cast的對象不對, 比如下面第一個(gè)println. 正確的應(yīng)該是下面的那個(gè)。
- long a = System.currentTimeMillis();
- long b = a + 100;
- System.out.println((int) b-a);
- System.out.println((int) (b-a));
對float和double使用==操作
錯(cuò)誤的寫法:
- for (float f = 10f; f!=0; f-=0.1) {
- System.out.println(f);
- }
上面的浮點(diǎn)數(shù)遞減只會(huì)無限接近0而不會(huì)等于0, 這樣會(huì)導(dǎo)致上面的for進(jìn)入死循環(huán). 通常絕不要對float和double使用==操作. 而采用大于和小于操作. 如果java編譯器能針對這種情況給出警告. 或者在java語言規(guī)范中不支持浮點(diǎn)數(shù)類型的==操作就最好了。
正確的寫法:
- for (float f = 10f; f>0; f-=0.1) {
- System.out.println(f);
- }
用浮點(diǎn)數(shù)來保存money
錯(cuò)誤的寫法:
- float total = 0.0f;
- for (OrderLine line : lines) {
- total += line.price * line.count;
- }
- double a = 1.14 * 75; // 85.5 將表示為 85.4999...
- System.out.println(Math.round(a)); // 輸出值為85
- BigDecimal d = new BigDecimal(1.14); //造成精度丟失
這個(gè)也是一個(gè)老生常談的錯(cuò)誤. 比如計(jì)算100筆訂單, 每筆0.3元, 最終的計(jì)算結(jié)果是29.9999971. 如果將float類型改為double類型, 得到的結(jié)果將是30.000001192092896. 出現(xiàn)這種情況的原因是, 人類和計(jì)算的計(jì)數(shù)方式不同. 人類采用的是十進(jìn)制, 而計(jì)算機(jī)是二進(jìn)制.二進(jìn)制對于計(jì)算機(jī)來說非常好使, 但是對于涉及到精確計(jì)算的場景就會(huì)帶來誤差. 比如銀行金融中的應(yīng)用。
因此絕不要用浮點(diǎn)類型來保存money數(shù)據(jù). 采用浮點(diǎn)數(shù)得到的計(jì)算結(jié)果是不精確的. 即使與int類型做乘法運(yùn)算也會(huì)產(chǎn)生一個(gè)不精確的結(jié)果.那是因?yàn)樵谟枚M(jìn)制存儲(chǔ)一個(gè)浮點(diǎn)數(shù)時(shí)已經(jīng)出現(xiàn)了精度丟失. 最好的做法就是用一個(gè)string或者固定點(diǎn)數(shù)來表示. 為了精確, 這種表示方式需要指定相應(yīng)的精度值.
BigDecimal就滿足了上面所說的需求. 如果在計(jì)算的過程中精度的丟失超出了給定的范圍, 將拋出runtime exception.
正確的寫法:
- BigDecimal total = BigDecimal.ZERO;
- for (OrderLine line : lines) {
- BigDecimal price = new BigDecimal(line.price);
- BigDecimal count = new BigDecimal(line.count);
- total = total.add(price.multiply(count)); // BigDecimal is immutable!
- }
- total = total.setScale(2, RoundingMode.HALF_UP);
- BigDecimal a = (new BigDecimal("1.14")).multiply(new BigDecimal(75)); // 85.5 exact
- a = a.setScale(0, RoundingMode.HALF_UP); // 86
- System.out.println(a); // correct output: 86
- BigDecimal a = new BigDecimal("1.14");
不使用finally塊釋放資源
錯(cuò)誤的寫法:
- public void save(File f) throws IOException {
- OutputStream out = new BufferedOutputStream(new FileOutputStream(f));
- out.write(...);
- out.close();
- }
- public void load(File f) throws IOException {
- InputStream in = new BufferedInputStream(new FileInputStream(f));
- in.read(...);
- in.close();
- }
上面的代碼打開一個(gè)文件輸出流, 操作系統(tǒng)為其分配一個(gè)文件句柄, 但是文件句柄是一種非常稀缺的資源, 必須通過調(diào)用相應(yīng)的close方法來被正確的釋放回收. 而為了保證在異常情況下資源依然能被正確回收, 必須將其放在finally block中. 上面的代碼中使用了BufferedInputStream將file stream包裝成了一個(gè)buffer stream, 這樣將導(dǎo)致在調(diào)用close方法時(shí)才會(huì)將buffer stream寫入磁盤. 如果在close的時(shí)候失敗, 將導(dǎo)致寫入數(shù)據(jù)不完全. 而對于FileInputStream在finally block的close操作這里將直接忽略。
如果BufferedOutputStream.close()方法執(zhí)行順利則萬事大吉, 如果失敗這里有一個(gè)潛在的bug(http://bugs.sun.com/view_bug.do?bug_id=6335274): 在close方法內(nèi)部調(diào)用flush操作的時(shí)候, 如果出現(xiàn)異常, 將直接忽略. 因此為了盡量減少數(shù)據(jù)丟失, 在執(zhí)行close之前顯式的調(diào)用flush操作。
下面的代碼有一個(gè)小小的瑕疵: 如果分配file stream成功, 但是分配buffer stream失敗(OOM這種場景), 將導(dǎo)致文件句柄未被正確
網(wǎng)頁題目:Java編程:常見問題匯總
本文來源:http://fisionsoft.com.cn/article/dhssogc.html


咨詢
建站咨詢
