代碼優(yōu)化是一個非常重要的課題。有些人可能覺得沒用。一些小地方有什么好修改的?對代碼的運行效率有什么影響?下面是我對這個問題的思考。就像海里的鯨魚,它吃一只小蝦有用嗎?沒什么用,但是蝦吃多了,鯨魚就喂了。代碼優(yōu)化也是如此。如果項目的目標是盡快上線,沒有bug,那么這個時候,代碼的細節(jié)可以忽略不計。但是,如果有足夠的時間來開發(fā)和維護代碼,那么每一個可以優(yōu)化的細節(jié)都必須考慮。小優(yōu)化點的積累一定會提高代碼的運行效率。
代碼優(yōu)化的目標是:
,減少代碼的大小
,提高代碼運行的效率
代碼優(yōu)化的細節(jié)
在Java核心API中,有很多應用final的例子,比如java.lang.String,整個類都是final。為類指定最終修飾符可以防止類被繼承,為方法指定最終修飾符可以防止方法被重寫。如果某個類被指定為final,則該類的所有方法都是final。Java編譯器會尋找機會內聯(lián)所有的final方法,內聯(lián)對提高Java運行效率有重要作用。有關詳細信息,請參見Java運行時優(yōu)化。這可以將性能平均提高10% .
,盡量重用對象
,尤其是字符串對象的使用。發(fā)生字符串連接時,應改為使用StringBuilder/StringBuffer。由于Java虛擬機不僅要花時間生成對象,將來還可能要花時間對這些對象進行垃圾收集和處理,所以生成過多的對象會對程序的性能產生很大的影響。
,調用方法時傳遞的參數(shù)盡可能使用局部變量
,調用中創(chuàng)建的臨時變量都保存在堆棧中,速度更快。其他變量,比如靜態(tài)變量,實例變量,都是在堆里創(chuàng)建的,比較慢。此外,當方法運行時,在堆棧中創(chuàng)建的變量將消失,并且不需要額外的垃圾收集。
,及時關閉流
在Java編程的過程中,連接數(shù)據(jù)庫和操作I/O流時一定要小心。用完后及時關閉釋放資源。因為對這些大型對象的操作會造成系統(tǒng)的較大開銷,稍有不慎就會導致嚴重的后果。
,盡量減少變量的重復計算
明確一個概念。即使一個方法中只有一條語句,也是有消耗的,包括創(chuàng)建堆棧幀、調用方法時保護場景、調用方法時還原場景等。例如,下面的操作:
for(int I =;我& ltlist . size();i++)
{}
建議替換為:
for (int i =,int length = list . size();我& lt長度;i++)
{}
這樣在list.size()比較大的時候,會減少很多消耗。
盡量采用懶加載的策略,也就是只在需要的時候才創(chuàng)建。
例如:
string str = & #;AAA & #;;如果(i ==)
{
list . add(str);
}
建議替換為:
如果(i ==)
{
string str = & #;AAA & #;;
list . add(str);
}
小心使用異常。
對異常表現(xiàn)不利。當拋出異常時,應該首先創(chuàng)建一個新對象。Throwable接口的構造函數(shù)調用名為fillInStackTrace()的本地同步方法,fillInStackTrace()方法檢查堆棧并收集調用跟蹤信息。每當拋出異常時,Java虛擬機必須調整調用堆棧,因為在處理過程中創(chuàng)建了一個新對象。異常只能用于錯誤處理,不應該用于控制程序流。
不要在循環(huán)中使用try…catch…應該放在外層。
除非萬不得已。如果你無緣無故寫這個,只要你的領導是學長,強迫癥,巴成都會罵你寫這個垃圾代碼。
如果可以估計要添加的內容的長度,請在底部指定數(shù)組中實現(xiàn)的集合和工具類的初始長度。
如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等。以StringBuilder為例:
()StringBuilder() //默認分配字符的空間。
()StringBuilder(int size) //默認情況下分配大小為字符的空間。
()StringBuilder(String str)//characters+str . length()默認情況下分配字符空格。
可以設置類的初始化容量(不僅僅是上面的StringBuilder),這樣可以明顯提高性能。例如,StringBuilder,length表示當前StringBuilder可以容納的字符數(shù)。因為當StringBuilder達到一個大容量的時候,它會把容量增加到現(xiàn)在的兩倍。每當StringBuilder達到其最大容量時,它都必須創(chuàng)建一個新的字符數(shù)組,并將舊字符數(shù)組的內容復制到新字符數(shù)組中——這是一個非常消耗性能的操作。想象一下,如果可以估計字符數(shù)組中的一個大輪廓存儲字符而不指定長度,并且冪越接近,不考慮每次擴展的加法,那么:
()在的基礎上,申請一個size的字符數(shù)組相當于一次申請一個size的字符數(shù)組。如果您可以在開始時指定一個字符數(shù)組的大小,它將節(jié)省一倍以上的空間。
()將原始字符復制到新的字符數(shù)組中。
這樣就浪費了內存空間,降低了代碼運行效率。所以在底層為數(shù)組實現(xiàn)的集合和工具類設置一個合理的初始化容量并沒有錯,會帶來立竿見影的效果。但是注意像HashMap這樣的集合是作為數(shù)組+鏈表實現(xiàn)的。不要將初始大小設置為與您的估計大小相同,因為只有一個對象連接到一個表的可能性幾乎為零。建議將初始大小設置為的n次方。如果有一個估計元素,可以將其設置為new HashMap()或new HashMap()。
復制大量數(shù)據(jù)時,請使用System.arraycopy()命令。
乘法和除法使用移位運算。
例如:
for(val =;val & lt;val +=)
{
a = val *;
b = val/;
}
使用移位操作可以大大提高性能,因為在計算機的底層,對齊的操作更加方便快捷,所以建議修改為:
for(val =;val & lt;val +=)
{
a = val & lt& gt;
}
雖然shift運算速度很快,但可能會使代碼難以理解,所以最好添加相應的注釋。
,不要一直在循環(huán)中創(chuàng)建對象引用
例如:
for(int I =;我& lt=計數(shù);i++)
{
Object obj = new Object();
}
這種做法會導致內存中存在count對象引用。如果計數(shù)非常大,它將消耗內存。建議改為:
Object obj = nullfor(int I =;我& lt=計數(shù);i++){ obj = new Object();}
這樣內存中只有一個對象引用,每次創(chuàng)建新的Object()時,對象引用都指向不同的對象,但內存中只有一個,這樣就大大節(jié)省了內存空間。
出于效率和類型檢查的考慮,盡量使用array,只有在無法確定數(shù)組大小的情況下才使用ArrayList。
盡量用HashMap,ArrayList,StringBuilder。除非線程安全需要,否則不建議使用Hashtable、Vector和StringBuffer。由于使用了同步機制,后三種方法會導致性能開銷。
不要將數(shù)組聲明為public static final。
因為這是沒有意義的,它只是將引用定義為static final,數(shù)組的內容仍然可以隨意更改。將數(shù)組聲明為public是一個安全漏洞,這意味著數(shù)組可以被外部類更改。
嘗試在正確的地方使用單個案例。
使用單例可以減輕加載負擔,縮短加載時間,提高加載效率,但并不是所有的地方都適合單例。簡單來說,單身族主要適合以下三個方面:
()控制資源的使用,通過線程同步控制資源的并發(fā)訪問。
()控制實例的生成,達到節(jié)約資源的目的。
()控制數(shù)據(jù)的共享,使多個不相關的進程或線程能夠在不建立直接關聯(lián)的情況下進行通信。
盡量避免隨意使用靜態(tài)變量。
要知道,當一個對象被定義為static的變量引用時,gc通常不會回收這個對象占用的堆內存,比如:
公共A類
{
私有靜態(tài)B B = new B();
}
此時靜態(tài)變量B的生命周期與類A相同,如果不卸載類A,引用B指向的B對象將常駐內存,直到程序終止。
及時清除不必要的會話。
為了清除不活動的會話,許多應用服務器都有默認的會話超時,通常是幾分鐘。當應用服務器需要保存更多的會話時,如果內存不足,操作系統(tǒng)會將一些數(shù)據(jù)轉移到磁盤,應用服務器可能會根據(jù)MRU(最近使用比較頻繁)算法將一些不活動的會話轉儲到磁盤,甚至會拋出內存不足的異常。如果要將會話轉儲到磁盤,則必須首先對其進行序列化。在大規(guī)模集群中,序列化對象的成本很高。因此,當不再需要會話時,應該及時調用HttpSession的invalid()方法來清除會話。
實現(xiàn)RandomAccess接口的集合(如ArrayList)應該由一個通用的for循環(huán)而不是foreach循環(huán)來遍歷。
這是JDK向用戶推薦的。JDK API對RandomAccess接口的解釋是:實現(xiàn)RandomAccess接口是用來表明它支持快速隨機訪問。該接口的主要目的是允許通用算法改變它們的行為,以便在應用于隨機或連續(xù)訪問列表時能夠提供良好的性能。實際體驗表明,如果隨機訪問實現(xiàn)RandomAccess接口的類實例,使用普通for循環(huán)的效率會高于使用foreach循環(huán)的效率;反之,如果是順序訪問,使用迭代器會更高效。您可以使用類似于下面的代碼來做出判斷:
if(列出隨機訪問的實例)
{ for(int I =;我& ltlist . size();i++){}
}否則{
iterator iterator = list . iterable();while(iterator . has next()){ iterator . next()}
}
foreach循環(huán)的底層實現(xiàn)原理是迭代器。參見Java語法sugar:變長參數(shù)和foreach循環(huán)原理。所以后半句“反過來,如果是順序訪問,用迭代器會更有效率”的意思就是用foreach循環(huán)遍歷那些順序訪問的類實例。
,用同步代碼塊代替同步方法
這個在《多線程模塊中的同步鎖方法塊》一文中已經講得很清楚了。除非可以確定需要同步整個方法,否則盡量使用同步代碼塊,避免同步那些不需要同步的代碼,影響代碼執(zhí)行的效率。
,將常量聲明為static final,并用大寫
命名,這樣可以在編譯時將這些內容放入常量池,運行時可以避免生成常量的值。另外,用大寫字母命名常量的名字也可以很容易區(qū)分常量和變量
,不創(chuàng)建一些不用的對象,不導入一些不用的類
,沒有意義。如果代碼中出現(xiàn)“不使用局部變量I的值”和“從不使用導入java.util”,請刪除這些無用的內容
,避免在程序運行過程中使用反射
。有關信息,請參考反射。正是反射Java為用戶提供了非常強大的功能。功能強大往往意味著效率低下。不建議在程序運行過程中使用反射機制,尤其是方法的invoke方法。如果真的有必要,建議的做法是通過反射實例化一個對象,在項目啟動時放入內存——用戶只關心在與對等體交互時獲得更快的響應速度,而不關心對等體啟動項目需要多長時間。
,數(shù)據(jù)庫連接池和線程池
都是用來重用對象的。前者可以避免頻繁地打開和關閉連接,后者可以避免頻繁地創(chuàng)建和銷毀線程
,使用緩沖的iostream進行IO操作
[即BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream,可以大大提高IO效率
。ArrayList用于更多順序插入和隨機訪問的場景,使用LinkedList
用于中間刪除和插入更多元素的場景。如果你了解ArrayList和LinkedList的原理,你就知道
。不要讓公共方法有太多的形參
,也就是公共方法是外部提供的方法。如果給這些方法的參數(shù)太多,主要有兩個缺點:
,違背了面向對象的編程思想。Java強調一切都是對象,參數(shù)太多不符合面向對象的編程思想
,參數(shù)太多必然增加方法調用的錯誤概率
[/h]比如我們用JDBC寫一個insertStudentInfo方法,有一個學生信息字段要插入到學生表中,這個參數(shù)可以封裝在一個實體類中。作為insert方法的參數(shù)
,字符串變量和字符串常量等于,把字符串常量寫在前面
是常用的伎倆。如果有以下代碼& #;;
if(str . equals(& #;)){
& #;
}
建議修改為:
string str = & #;;
if(& #;。equals(str))
{
& #;
}
這主要是為了避免空指針異常
。要知道java里的if (i ==)和if (== i)是沒有區(qū)別的,但是從閱讀習慣上講,建議用前者。
在C/C++中,“if (i ==)”判斷條件成立,而且是基于NAND的,表示假,否定表示真。如果有這樣的代碼:
int I =;
if(I = =)
{
& #;
} else {
& #;
}
C/C++判斷" I = = " = "不成立,所以用,即false表示。但是如果:
int I =;if(I =){ & #;} else { & #;}
萬一程序員不小心把“if (i ==)”寫成了“if (i =)”,那就有問題了。如果I賦給If,if確定里面的內容不為真,返回值為真,但是當I為清時,比較值為假,應該返回。這種情況在C/C++開發(fā)中很可能發(fā)生,會導致一些無法理解的錯誤。所以為了避免開發(fā)者在if語句中不正確的賦值操作,建議將if語句寫成:
int I =;if(= = I){ & #;} else { & #;}
這樣即使開發(fā)者不小心寫了“= i”,C/C++編譯器也能第一時間查出來,因為我們可以把I賦給變量,而不是常量。
但是在Java中,C/C++的“if (i =)”語法是不可能的,因為一旦寫了這個語法,Java就會編譯出錯誤“類型不匹配:無法從int轉換成boolean”。不過,雖然Java中的“if (i ==)”和“if (== i)”在語義上沒有區(qū)別,但從閱讀習慣上來說,還是建議使用前者比較好。
,不要在數(shù)組上使用toString()方法
看看在數(shù)組上使用toString()打印出來的是什么:
public static void main(string[]args)[/br/
system . out . println(is . toString());
}
結果是:
[I@af
意在打印出數(shù)組的內容,但也有可能因為數(shù)組引用為空而導致空指針異常。雖然對數(shù)組toString()沒有意義,但是可以打印出set toString()的內容,因為set AbstractCollections的父類覆蓋了對象的toString()方法。
,不要強制向下轉換超出范圍的基本數(shù)據(jù)類型
這樣永遠得不到想要的結果:
public static void main(String[]args)
{
long L = L;
int I =(int)l;
system . out . println(I);
}
我們可能期望得到其中的一些,但結果是:
解釋。Java long是一個字節(jié)位,所以在計算機中的表示應該是:
一個int類型的數(shù)據(jù)是一個字節(jié)位,從低位取的上述二進制數(shù)據(jù)串的第一位是:
[/]從這個例子中,我們可以順便得到兩個結論:
。integer的默認數(shù)據(jù)類型是int,long l = L,超出了int的范圍,所以末尾有一個L,表示是一個long數(shù)。對了,浮點型的默認類型是double,所以定義float的時候應該寫成" " float f =。f "
,然后再寫一句“int ii = l+I;”將會報告一個錯誤,因為long+int是一個long,不能賦給int
,公共集合類中未使用的數(shù)據(jù)必須及時移除
如果一個集合類是公共的(也就是說,它不是方法中的一個屬性),那么這個集合中的元素不會被自動釋放,因為總是有引用指向它。因此,如果公共集合中的一些數(shù)據(jù)沒有被使用或刪除,就會導致公共集合不斷增長,從而使系統(tǒng)存在內存泄漏的隱患。
,將基本數(shù)據(jù)類型轉換為字符串,基本數(shù)據(jù)類型。toString()是比較快的方式,String.valueOf (data)次之,data+" "比較慢
把基本數(shù)據(jù)類型轉換成通用數(shù)據(jù)類型有三種方式,我有一個整型數(shù)據(jù)I。
public static void main(String[]args)
{
int loop time =;
Integer I =;long start time = system . current time millis();for(int j =;j & lt循環(huán)時間;j++)
{
string str = string . value of(I);
}
system . out . println(& #;string . value of():& #;+(system . current time millis()& #;start time)+& #;ms & #;);
start time = system . current time millis();for(int j =;j & lt循環(huán)時間;j++)
{
string str = I . tostring();
}
system . out . println(& #;integer . tostring():& #;+(system . current time millis()& #;start time)+& #;ms & #;);
start time = system . current time millis();for(int j =;j & lt循環(huán)時間;j++)
{
string str = I+& #;;
}
system . out . println(& #;I+/& #;/:+(system . current time millis()& #;start time)+& #;ms & #;);
}
運行結果是:
string . value of():ms integer . tostring():ms I+& #;:毫秒
因此,將來將基本數(shù)據(jù)類型轉換為字符串時,首選toString()方法。至于為什么,很簡單:
在String.valueOf()方法的底部,調用Integer.toString()方法,但是在調用之前判斷會被短路。
,Integer.toString()方法就不說了,直接調用。
i+" "的底層由StringBuilder實現(xiàn),通過append方法拼接,然后通過toString()方法獲取字符串。
三者相比,明顯是快、二、慢。
使用更高效的方式遍歷地圖。
有許多方法可以遍歷地圖。通常,我們在場景中需要的是遍歷地圖中的鍵和值,所以推薦且高效的方式是:
公共靜態(tài)void main(String[] args)
{
HashMap hm = new HashMap();
hm . put(& #;, );
Set & lt;地圖。Entry & gtentry set = hm . entry set();
迭代器& lt地圖。Entry & gtITER = entry set . iterator();while(ITER . has next())
{
Map。entry entry = ITER . next();
system . out . println(entry . getkey()+& #;/t & #;+entry . getvalue());
}
}
如果您只想遍歷此映射的鍵值,請使用" Set keySet = hm . keySet();"會比較合適
,資源的close()建議單獨操作
。意思是比如我有這個代碼:
try {
XXX . close .
yyy . close();
}catch(異常e)
{
& #;
}
建議修改為:
try { XXX . close();}catch(異常e){ & #;}試試{ yyy . close();}catch(異常e){ & #;}
雖然有些麻煩,但是可以避免資源泄露。我們認為,如果沒有修改代碼,萬一XXX.close()拋出異常,那么它就在cath塊中,YYY.close()不會被執(zhí)行,所以YYY的這個資源不會被回收,一直被占用。如果這樣的代碼比較多,可能會造成資源句柄的泄露。改成以下寫法后,保證無論如何XXX和YYY都會很親近。
作者:徐州百都網(wǎng)絡 | 來源: | 發(fā)布于:2022-04-03 10:59:33