這篇將作為我這一段時(shí)間以來(lái)結(jié)合項(xiàng)目、學(xué)習(xí)對(duì)于自我提升的過(guò)程中的一個(gè)技術(shù)性的經(jīng)驗(yàn)總結(jié)??挣饥。
現(xiàn)在大多數(shù)公司招人看中項(xiàng)目經(jīng)驗(yàn)、工作經(jīng)驗(yàn)扔枫,我也承認(rèn)這些也確實(shí)非常重要。但是我時(shí)常會(huì)想短荐,是否有一條捷徑,可以讓我們更快的強(qiáng)大起來(lái)忍宋、更快的走向成功?我也不確定讶踪,我也在摸索著前行...但我相信,站在巨人們的肩旁上乳讥,借鑒他們探索出的東西,加上自己的那一份努力云石,至少可以讓自己少走一些彎路。
為什么會(huì)寫(xiě)這篇程序優(yōu)化的總結(jié)汹忠,主要是面向如今這個(gè)大數(shù)據(jù)驅(qū)動(dòng)的時(shí)代。如果沒(méi)有大批量的數(shù)據(jù)讓你去處理宽菜、如果沒(méi)有大量業(yè)務(wù)需要你去書(shū)寫(xiě),那么有必要去在乎那零點(diǎn)幾毫秒的優(yōu)化或是復(fù)用那些代碼嗎铅乡?
1. 選用恰當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)
去重操作是我們?nèi)粘?huì)遇到的一個(gè)問(wèn)題。我無(wú)數(shù)次遇到去重的問(wèn)題的時(shí)候阵幸,可能大家想的都一樣: 臥槽,也太簡(jiǎn)單了挚赊,給我兩秒鐘...然后做了如下操作:
ArrayList<String> arr = new ArrayList<String>(Arrays.asList(......)); //待去重?cái)?shù)組
ArrayList<String> arrTemp = new ArrayList<String>();
for(String str: arr) {
if (!arrTemp.contains(str)) {
arrTemp.add(str);
// 處理操作
}
}
這是正常人的思路吧,使用一個(gè)臨時(shí)數(shù)組來(lái)存放新的元素荠割,然后便利的時(shí)候判斷是否包含。嗯...功能實(shí)現(xiàn)了,但是這個(gè)待遍歷的數(shù)組變成了幾十萬(wàn)條數(shù)據(jù)...然后我按下了 run
跑程序并等待程序的結(jié)束纽帖,然后我在程序finish
之前陷入了長(zhǎng)時(shí)間的沉思......然后到處找呀找举反,查閱資料和博客,最后發(fā)現(xiàn)了一種效率非常贊的數(shù)據(jù)結(jié)構(gòu) - HashSet火鼻, 我非常激動(dòng),MD太辛苦了找了辣磨久終于找到好的辦法了融撞,這個(gè)時(shí)候之前的程序運(yùn)行結(jié)束了......
先想想為什么這種數(shù)組的方式會(huì)很慢。在我們每一次循環(huán)中粗蔚,我們都會(huì)使用contains()
去遍歷整個(gè)arrTemp
,隨著數(shù)組越來(lái)越長(zhǎng)致扯,速度會(huì)越來(lái)越慢。接下來(lái)看看 HashSet 的存儲(chǔ)結(jié)構(gòu)示意圖:
??在HashSet中抖僵,每一個(gè)
key
都對(duì)應(yīng)著一個(gè)hashcode
缘揪,因此通過(guò)計(jì)算可以先找到是在數(shù)組中 0~15 的哪一個(gè)位置,然后再去尋找這條鏈上有沒(méi)有這個(gè)對(duì)應(yīng)的key
值找筝。這么一來(lái)在速率上明顯快了不上。
ArrayList<String> arr = new ArrayList<String>(Arrays.asList(......)); //待去重?cái)?shù)組
HashSet set = new HashSet();
for (String str : arr) {
if (set.add(str)) {
// 沒(méi)有重復(fù)元素
} else {
// 存在重復(fù)元素...
}
}
換了一種數(shù)據(jù)結(jié)構(gòu)袖裕,在程序的執(zhí)行效率上有了很大的改觀。所以說(shuō)沐祷,選擇一種好的數(shù)據(jù)結(jié)構(gòu)是很重要的~
2. 養(yǎng)成復(fù)用對(duì)象的好習(xí)慣
在程序的運(yùn)行過(guò)程中攒岛,JVM堆上存放著活對(duì)象和程序無(wú)法再次引用的垃圾對(duì)象,當(dāng)堆空間的空閑部分低于某個(gè)比例或者無(wú)法滿足為新對(duì)象分配空間時(shí)灾锯,JVM都會(huì)觸發(fā)一次垃圾收集(Garbage Collect,GC),觸發(fā)gc時(shí)吵聪,JVM會(huì)暫停所有的用戶線程。gc觸發(fā)太過(guò)頻繁或是時(shí)間過(guò)長(zhǎng)帽蝶,都會(huì)導(dǎo)致程序性能大大下降。
所以說(shuō)励稳,遇到此類問(wèn)題我們應(yīng)該怎么辦呢囱井? 我們需要盡量去重用對(duì)象,以減少gc的觸發(fā)幾率庞呕。比如說(shuō)像這樣:
String str = "";
for(...) {
str = ....;
str ......
}
順便說(shuō)一下,不只是對(duì)象的創(chuàng)建住练,循環(huán)里面最好別放一些沒(méi)必要的操作,特別是耗時(shí)的操作澎羞。
3. 提高字符串拼接的效率
字符串的拼接是我們寫(xiě)程序的時(shí)候最最常見(jiàn)的情況了,我相信大家肯定都遇到過(guò)顺呕,并且以后也會(huì)繼續(xù)和它打交道。對(duì)于文件IO讀寫(xiě)而言括饶,大家一定對(duì)于這句話并不陌生:
str += stringBuffer.readLine() + "\n";
我們?cè)谧x取文件的我時(shí)候,經(jīng)常會(huì)按行來(lái)讀取启盛,然后將其內(nèi)容進(jìn)行依次追加,最后將內(nèi)容結(jié)果寫(xiě)出僵闯。
如果在循環(huán)次數(shù)不多的情況下藤滥,并且對(duì)效率的要求不高的話鳖粟,為了圖方便這么些沒(méi)問(wèn)題拙绊。但是泳秀,如果循環(huán)次數(shù)非常多的話榄攀,這種方式做字符串的拼接效率實(shí)在太低了。這里有一種更好的方式:使用 StringBuilder
檩赢,我們來(lái)看一個(gè)效率的對(duì)比結(jié)果吧。
首先我們假設(shè)有一個(gè)100000次的循環(huán)漠畜,每一次里面都會(huì)將內(nèi)容"aaa123"追加到結(jié)果中坞靶。
long start = System.currentTimeMillis();
int size = 100000;
String result = "";
for (int i = 0; i < size; i++) {
result += "aaa123";
}
long end = System.currentTimeMillis();
System.out.println(end - start + "ms");
然后再來(lái)看一下改成StringBuilder
之后的代碼和運(yùn)行結(jié)果:
long start = System.currentTimeMillis();
int size = 100000;
String result = "";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < size; i++) {
builder.append("aaa123");
}
result = builder.toString();
long end = System.currentTimeMillis();
System.out.println(end - start + "ms");
這效率簡(jiǎn)直高了幾個(gè)數(shù)量級(jí)了...為什么StringBuilder
可以做到在這樣多的循環(huán)次下達(dá)到這么高的效率呢彰阴?我大致解釋下...
對(duì)于使用'' + ''號(hào)來(lái)追加的方式而言,String對(duì)象在賦值以后是沒(méi)法改變的尿这,所以這種使用'' + ''號(hào)的追加原理其實(shí)每一次都重新創(chuàng)建了一個(gè)String 對(duì)象,然后對(duì)其賦值射众。這樣不斷新建對(duì)象,就像之前我們所講的那樣叨橱,會(huì)頻繁觸發(fā)gc,而且會(huì)隨著內(nèi)容越來(lái)越多使得創(chuàng)建String對(duì)象的速度越來(lái)越慢愉舔。
然而對(duì)于StringBuilder
的 append
追加方式,是將string 轉(zhuǎn)換為一個(gè)char[] 數(shù)組伙菜,然后通過(guò)擴(kuò)展數(shù)組的方式將追加內(nèi)容放入原始數(shù)組中轩缤。
4. for循環(huán)的正確使用姿勢(shì)
對(duì)于for循環(huán)的使用,我的建議是:
- 遍歷數(shù)組使用基于索引的循環(huán)方式
- 遍歷集合使用基于迭代器的循環(huán)方式
具體的怎么去使用贩绕,我在這里就不做過(guò)多說(shuō)明了火的。
主要講講怎么進(jìn)行循環(huán)的優(yōu)化吧。
(1) ''外小內(nèi)大''原則
for (int i = 0; i < 1000000 ; i++ ) {
for (int j = 0; j < 1000 ; j++ ) {
for (int k = 0 ; k < 10; k++) {
}
}
}
這是一個(gè)典型的循環(huán)嵌套的結(jié)構(gòu)淑倾,但是呢for循環(huán)應(yīng)該是要滿足一個(gè)''外小內(nèi)大''的模式馏鹤。這是我們改良后的代碼:
for (int i = 0; i < 10 ; i++ ) {
for (int j = 0; j < 1000 ; j++ ) {
for (int k = 0 ; k < 1000000; k++) {
}
}
}
結(jié)果很明顯,運(yùn)行效率上面快了不少踊淳。但是這還不是最完美的做法~
int i,j,k;
for (i = 0; i < 10 ; i++ ) {
for (j = 0; j < 1000 ; j++ ) {
for (k = 0 ; k < 1000000; k++) {
}
}
}
嗯假瞬,這樣差不多就比較完美了??
(2)避免沒(méi)必要的操作
還是先來(lái)看一段原始代碼:
for(int i = 0; i < list.size(); i++) {
i = i * a * b;
}
這段代碼有兩個(gè)問(wèn)題:第一個(gè)問(wèn)題是每次循環(huán)都會(huì)進(jìn)行一次 list.size()
的計(jì)算陕靠,第二個(gè)問(wèn)題是每次都會(huì)進(jìn)行 a * b
的計(jì)算。這些都是只需要進(jìn)行一次的操作脱茉,沒(méi)必要每次都進(jìn)行剪芥。
優(yōu)化后的代碼如下:
int c = a * b;
int len = list.size();
for(int i = 0; i < len; i++) {
i = i * c;
}