最近在想自己編程時是否注意過代碼的效率問題,得出的答案是:沒有。代碼只是實現了功能,至於效率高不高沒怎么關注,這應該是JAVA程序員進階的時候需要考慮的問題,不再是單純的實現功能,也不是完全依賴GC而不關注內存中發生了什么,而要考慮到代碼的性能。下面是網上找的一篇關於JAVA代碼優化的文章,覺得不錯,就轉載了。這里面設計到了JAVA基礎和J2EE方面的優化建議,有時間會整理一下,現在先轉載。 原文地址是:http://blog.163.com/user_zhaopeng/blog/static/16602270820122105731329/ 1、 盡量指定類的final修飾符 帶有final修飾符的類是不可派生的。 如果指定一個類為final,則該類所有的方法都是final。Java編譯器會尋找機會內聯(inline)所有的 final方法(這和具體的編譯器實現有關)。此舉能夠使性能平均提高50% 。 2、 盡量重用對象。 特別是String 對象的使用中,出現字符串連接情況時應用StringBuffer 代替。由於系統不僅要花時間生成對象,以后可能還需花時間對這些對象進行垃圾回收和處理。因此,生成過多的對象將會給程序的性能帶來很大的影響。 3、 盡量使用局部變量,調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧(Stack)中,速度較快。 其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢。另外,依賴於具體的編譯器/JVM,局部變量還可能得到進一步優化。 4、 不要重復初始化變量 默認情況下,調用類的構造函數時, Java會把變量初始化成確定的值:所有的對象被設置成null,整數變量(byte、short、int、long)設置成0,float和 double變量設置成0.0,邏輯值設置成false。當一個類從另一個類派生時,這一點尤其應該注意,因為用new關鍵詞創建一個對象時,構造函數鏈 中的所有構造函數都會被自動調用。 5、 在JAVA + ORACLE 的應用系統開發中,java中內嵌的SQL語句盡量使用大寫的形式,以減輕ORACLE解析器的解析負擔。 6、 I/O操作中需要及時釋放資源 Java 編程過程中,進行數據庫連接、I/O流操作時務必小心,在使用完畢后,即使關閉以釋放資源。 因為對這些大對象的操作會造成系統大的開銷,稍有不慎,會導致嚴重的后果。 7、 保證過期對象的及時回收 由於JVM的有其自身的GC機制,不需要程序開發者的過多考慮,從一定程度上減輕了開發者負擔,但同時也遺漏了隱患,過分的創建對象會消耗系統的大量內 存,嚴重時會導致內存泄露,因此,保證過期對象的及時回收具有重要意義。 JVM回收垃圾的條件是:對象不在被引用;然而,JVM的GC並非十分的機智,即使對象滿足了垃圾回收的條件也不一定會被立即回收。所以,建議我們在對象使用完畢,應手動置成null。 8、 在使用同步機制時,應盡量使用方法同步代替代碼塊同步。 9、 盡量減少對變量的重復計算 10、盡量采用lazy loading 的策略,即在需要的時候才開始創建。 11、慎用異常 異常對性能不利。拋出異常首先要創建一個新的對象。Throwable接口的構造函數調用名為fillInStackTrace()的本地 (Native)方法,fillInStackTrace()方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,VM就必須調整調用堆棧,因為在處理過 程中創建了一個新的對象。 異常只能用於錯誤處理,不應該用來控制程序流程。 12、不要在循環中使用: Try { } catch() { } 應把其放置在最外層。 13、StringBuffer 的使用: StringBuffer表示了可變的、可寫的字符串。 有三個構造方法 : StringBuffer (); //默認分配16個字符的空間 StringBuffer (int size); //分配size個字符的空間 StringBuffer (String str); //分配16個字符+str.length()個字符空間 你可以通過StringBuffer的構造函數來設定它的初始化容量,這樣可以明顯地提升性能。 這里提到的構造函數是StringBuffer(int length),length參數表示當前的StringBuffer能保持的字符數量。你也可以使用ensureCapacity(int minimumcapacity)方法在StringBuffer對象創建之后設置它的容量。首先我們看看StringBuffer的缺省行為,然后再找 出一條更好的提升性能的途徑。 StringBuffer在內部維護一個字符數組,當你使用缺省的構造函數來創建StringBuffer對象的時候,因為沒有設置初始化字符長 度,StringBuffer的容量被初始化為16個字符,也就是說缺省容量就是16個字符。當StringBuffer達到最大容量的時候,它會將自身 容量增加到當前的2倍再加2,也就是(2*舊值+2)。如果你使用缺省值,初始化之后接着往里面追加字符,在你追加到第16個字符的時候它會將容量增加到 34(2*16+2),當追加到34個字符的時候就會將容量增加到70(2*34+2)。無論何事只要StringBuffer到達它的最大容量它就不得 不創建一個新的字符數組然后重新將舊字符和新字符都拷貝一遍――這也太昂貴了點。所以總是給StringBuffer設置一個合理的初始化容量值是錯不了 的,這樣會帶來立竿見影的性能增益。StringBuffer初始化過程的調整的作用由此可見一斑。所以,使用一個合適的容量值來初始化 StringBuffer永遠都是一個最佳的建議。 14、合理的使用Java類 java.util.Vector。 簡單地說,一個Vector就是一個java.lang.Object實例的數組。Vector與數組相似,它的元素可以通過整數形式的索引訪問。但是,Vector類型的對象在創建之后,對象的大小能夠根據元素的增加或者刪除而擴展、縮小。請考慮下面這個向Vector加入元素的例子: Object bj = new Object(); Vector v = new Vector(100000); for(int I=0; I<100000; I++) { v.add(0,obj); } 除非有絕對充足的理由要求每次都把新元素插入到Vector的前面,否則上面的代碼對性能不利。在默認構造函數中,Vector的初始存儲能力 是10個元素,如果新元素加入時存儲能力不足,則以后存儲能力每次加倍。Vector類就對象StringBuffer類一樣,每次擴展存儲能力時,所有 現有的元素都要復制到新的存儲空間之中。下面的代碼片段要比前面的例子快幾個數量級: Object bj = new Object(); Vector v = new Vector(100000); for(int I=0; I<100000; I++) { v.add(obj); } 同樣的規則也適用於Vector類的remove()方法。由於Vector中各個元素之間不能含有“空隙”,刪除除最后一個元素之外的任意其 他元素都導致被刪除元素之后的元素向前移動。也就是說,從Vector刪除最后一個元素要比刪除第一個元素“開銷”低好幾倍。 假設要從前面的Vector刪除所有元素,我們可以使用這種代碼: for(int I=0; I<100000; I++) { v.remove(0); } 但是,與下面的代碼相比,前面的代碼要慢幾個數量級: for(int I=0; I<100000; I++) { v.remove(v.size()-1); } 從Vector類型的對象v刪除所有元素的最好方法是: v.removeAllElements(); 假設Vector類型的對象v包含字符串“Hello”。考慮下面的代碼,它要從這個Vector中刪除“Hello”字符串: String s = “Hello”; int i = v.indexOf(s); if(I != -1) v.remove(s); 這些代碼看起來沒什么錯誤,但它同樣對性能不利。在這段代碼中,indexOf()方法對v進行順序搜索尋找字符串 “Hello”,remove(s)方法也要進行同樣的順序搜索。改進之后的版本是: String s = “Hello”; int i = v.indexOf(s); if(I != -1) v.remove(i); 這個版本中我們直接在remove()方法中給出待刪除元素的精確索引位置,從而避免了第二次搜索。一個更好的版本是: String s = “Hello”; v.remove(s); 最后,我們再來看一個有關Vector類的代碼片段: for(int I=0; I++;I < v.length) 如果v包含100,000個元素,這個代碼片段將調用v.size()方法100,000次。雖然size方法是一個簡單的方法,但它仍舊需要 一次方法調用的開銷,至少JVM需要為它配置以及清除堆棧環境。在這里,for循環內部的代碼不會以任何方式修改Vector類型對象v的大小,因此上面 的代碼最好改寫成下面這種形式: int size = v.size(); for(int I=0; I++;I<size) 雖然這是一個簡單的改動,但它仍舊贏得了性能。畢竟,每一個CPU周期都是寶貴的。 15、當復制大量數據時,使用System.arraycopy()命令。 16、代碼重構:增強代碼的可讀性。 17、不用new關鍵詞創建類的實例 用new關鍵詞創建類的實例時,構造函數鏈中的所有構造函數都會被自動調用。但如果一個對象實現了Cloneable接口,我們可以調用它的 clone()方法。clone()方法不會調用任何類構造函數。 在使用設計模式(Design Pattern)的場合,如果用Factory模式創建對象,則改用clone()方法創建新的對象實例非常簡單。例如,下面是Factory模式的一個 典型實現: public static Credit getNewCredit() { return new Credit(); } 改進后的代碼使用clone()方法,如下所示: private static Credit BaseCredit = new Credit(); public static Credit getNewCredit() { return (Credit) BaseCredit.clone(); } 上面的思路對於數組處理同樣很有用。 18、乘法和除法,用移位操作替代乘法操作可以極大地提高性能。 19、在JSP頁面中關閉無用的會話。 一個常見的誤解是以為session在有客戶端訪問時就被創建,然而事實是直到某server端程序調用 HttpServletRequest.getSession(true)這樣的語句時才被創建,注意如果JSP沒有顯示的使用 <%@pagesession=”false”%> 關閉session,則JSP文件在編譯成Servlet時將會自動加上這樣一條語句HttpSession session = HttpServletRequest.getSession(true);這也是JSP中隱含的session對象的來歷。由於session會消耗內 存資源,因此,如果不打算使用session,應該在所有的JSP中關閉它。 對於那些無需跟蹤會話狀態的頁面,關閉自動創建的會話可以節省一些資源。使用如下page指令:<%@ page session=”false”%> 20、JDBC與I/O 如果應用程序需要訪問一個規模很大的數據集,則應當考慮使用塊提取方式。默認情況下,JDBC每次提取32行數據。舉例來說,假設我們要遍歷一個5000 行的記錄集,JDBC必須調用數據庫157次才能提取到全部數據。如果把塊大小改成512,則調用數據庫的次數將減少到10次。 21、Servlet與內存使用 許多開發者隨意地把大量信息保存到用戶會話之中。一些時候,保存在會話中的對象沒有及時地被垃圾回收機制回收。從性能上看,典型的症狀是用戶感到系統周期 性地變慢,卻又不能把原因歸於任何一個具體的組件。如果監視JVM的堆空間,它的表現是內存占用不正常地大起大落。 解決這類內存問題主要有二種辦法。第一種辦法是,在所有作用范圍為會話的Bean中實現HttpSessionBindingListener接口。這 樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean使用的資源。 另外一種辦法就是盡快地把會話作廢。大多數應用服務器都有設置會話作廢間隔時間的選項。另外,也可以用編程的方式調用會話的 setMaxInactiveInterval()方法,該方法用來設定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計。 22、使用緩沖標記 一些應用服務器加入了面向JSP的緩沖標記功能。例如,BEA的WebLogic Server從6.0版本開始支持這個功能,Open Symphony工程也同樣支持這個功能。JSP緩沖標記既能夠緩沖頁面片斷,也能夠緩沖整個頁面。當JSP頁面執行時,如果目標片斷已經在緩沖之中,則 生成該片斷的代碼就不用再執行。頁面級緩沖捕獲對指定URL的請求,並緩沖整個結果頁面。對於購物籃、目錄以及門戶網站的主頁來說,這個功能極其有用。對 於這類應用,頁面級緩沖能夠保存頁面執行的結果,供后繼請求使用。 23、選擇合適的引用機制 在典型的JSP應用系統中,頁頭、頁腳部分往往被抽取出來,然后根據需要引入頁頭、頁腳。當前,在JSP頁面中引入外部資源的方法主要有兩 種:include指令,以及include動作。 include指令:例如<%@ include file=”copyright.html” %>。該指令在編譯時引入指定的資源。在編譯之前,帶有include指令的頁面和指定的資源被合並成一個文件。被引用的外部資源在編譯時就確定, 比運行時才確定資源更高效。 include動作:例如<jsp:include page=”copyright.jsp” />。該動作引入指定頁面執行后生成的結果。由於它在運行時完成,因此對輸出結果的控制更加靈活。但時,只有當被引用的內容頻繁地改變時,或者在對 主頁面的請求沒有出現之前,被引用的頁面無法確定時,使用include動作才合算。 24、及時清除不再需要的會話 為了清除不再活動的會話,許多應用服務器都有默認的會話超時時間,一般為30分鍾。當應用服務器需要保存更多會話時,如果內存容量不足,操作系統會把部分 內存數據轉移到磁盤,應用服務器也可能根據“最近最頻繁使用”(Most Recently Used)算法把部分不活躍的會話轉儲到磁盤,甚至可能拋出“內存不足”異常。在大規模系統中,串行化會話的代價是很昂貴的。當會話不再需要時,應當及時 調用HttpSession.invalidate()方法清除會話。HttpSession.invalidate()方法通常可以在應用的退出頁面調 用。 25、不要將數組聲明為:public static final 。 26、HashMap的遍歷效率討論 經常遇到對HashMap中的key和value值對的遍歷操作,有如下兩種方法:Map<String, String[]> paraMap = new HashMap<String, String[]>(); …………….//第一個循環 Set<String> appFieldDefIds = paraMap.keySet(); for (String appFieldDefId : appFieldDefIds) { String[] values = paraMap.get(appFieldDefId); …… } //第二個循環 for(Entry<String, String[]> entry : paraMap.entrySet()){ String appFieldDefId = entry.getKey(); String[] values = entry.getValue(); ……. } 第一種實現明顯的效率不如第二種實現。 分析如下 Set<String> appFieldDefIds = paraMap.keySet(); 是先從HashMap中取得keySet 代碼如下: public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } private class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } } 其實就是返回一個私有類KeySet, 它是從AbstractSet繼承而來,實現了Set接口。 再來看看for/in循環的語法 for(declaration : expression) statement 在執行階段被翻譯成如下各式 for(Iterator<E> #i = (expression).iterator(); #i.hashNext();){ declaration = #i.next(); statement } 因此在第一個for語句for (String appFieldDefId : appFieldDefIds) 中調用了HashMap.keySet().iterator() 而這個方法調用了newKeyIterator() Iterator<K> newKeyIterator() { return new KeyIterator(); } private class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } } 所以在for中還是調用了 在第二個循環for(Entry<String, String[]> entry : paraMap.entrySet())中使用的Iterator是如下的一個內部類 private class EntryIterator extends HashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } } 此時第一個循環得到key,第二個循環得到HashMap的Entry效率就是從循環里面體現出來的第二個循環此致可以直接取key和value值 而第一個循環還是得再利用HashMap的get(Object key)來取value值現在看看HashMap的get(Object key)方法 public V get(Object key) { Object k = maskNull(key); int hash = hash(k); int i = indexFor(hash, table.length); //Entry[] table Entry<K,V> e = table; while (true) { if (e == null) return null; if (e.hash == hash && eq(k, e.key)) return e.value; e = e.next; } } 其實就是再次利用Hash值取出相應的Entry做比較得到結果,所以使用第一中循環相當於兩次進入HashMap的Entry 中而第二個循環取得Entry的值之后直接取key和value,效率比第一個循環高。其實按照Map的概念來看也應該是用第二個循環好一點,它本 來就是key和value的值對,將key和value分開操作在這里不是個好選擇。 27、array(數組) 和 ArryList的使用 array([]):最高效;但是其容量固定且無法動態改變; ArrayList:容量可動態增長;但犧牲效率; 基於效率和類型檢驗,應盡可能使用array,無法確定數組大小時才使用ArrayList! ArrayList是Array的復雜版本 ArrayList內部封裝了一個Object類型的數組,從一般的意義來說,它和數組沒有本質的差別,甚至於ArrayList的許多方法,如 Index、IndexOf、Contains、Sort等都是在內部數組的基礎上直接調用Array的對應方法。 ArrayList存入對象時,拋棄類型信息,所有對象屏蔽為Object,編譯時不檢查類型,但是運行時會報錯。 注:jdk5中加入了對泛型的支持,已經可以在使用ArrayList時進行類型檢查。 從這一點上看來,ArrayList與數組的區別主要就是由於動態增容的效率問題了 28、盡量使用HashMap 和ArrayList ,除非必要,否則不推薦使用HashTable和Vector ,后者由於使用同步機制,而導致了性能的開銷。