转载--编写高质量代码:改善Java程序的151个建议(第5章:数组和集合___建议60~64)
閱讀目錄
- 建議60:性能考慮,數(shù)組是首選
- 建議61:若有必要,使用變長數(shù)組
- 建議62:警惕數(shù)組的淺拷貝
- 建議63:在明確的場(chǎng)景下,為集合指定初始容量
- 建議64:多種最值算法,適時(shí)選擇
噢,它明白了,河水既沒有牛伯伯說的那么淺,也沒有小松鼠說的那么深,只有親自試過才知道。
---寓言故事《小馬過河》
數(shù)據(jù)處理是每種語言必備的功能,Java更甚之,數(shù)據(jù)集可以允許重復(fù),也可以不允許重復(fù),可以允許null存在,也可以不允許null存在,可以自動(dòng)排序,也可以不自動(dòng)排序,可以是阻塞式的,也可以是非阻塞式的,可以是棧,也可以是隊(duì)列......
本章將圍繞我們使用最多的三個(gè)數(shù)據(jù)集合(數(shù)組,ArrayList和HashMap)來闡述在開發(fā)過程中要注意的事項(xiàng),并由此延伸至Set、Quene、Stack等集合。
回到頂部建議60:性能考慮,數(shù)組是首選
數(shù)組在實(shí)際的系統(tǒng)開發(fā)中用的越來越少了,我們通常只有在閱讀一些開源項(xiàng)目時(shí)才會(huì)看到它們的身影,在Java中它確實(shí)沒有List、Set、Map這些集合類用起來方便,但是在基本類型處理方面,數(shù)組還是占優(yōu)勢(shì)的,而且集合類的底層也都是通過數(shù)組實(shí)現(xiàn)的,比如對(duì)一數(shù)據(jù)集求和這樣的計(jì)算:
1 //對(duì)數(shù)組求和 2 public static int sum(int datas[]) { 3 int sum = 0; 4 for (int i = 0; i < datas.length; i++) { 5 sum += datas[i]; 6 } 7 return sum; 8 }對(duì)一個(gè)int類型 的數(shù)組求和,取出所有數(shù)組元素并相加,此算法中如果是基本類型則使用數(shù)組效率是最高的,使用集合則效率次之。再看使用List求和:
1 // 對(duì)列表求和計(jì)算 2 public static int sum(List<Integer> datas) { 3 int sum = 0; 4 for (int i = 0; i < datas.size(); i++) { 5 sum += datas.get(i); 6 } 7 return sum; 8 }注意看sum +=?datas.get(i);這行代碼,這里其實(shí)已經(jīng)做了一個(gè)拆箱動(dòng)作,Integer對(duì)象通過intValue方法自動(dòng)轉(zhuǎn)換成了一個(gè)int基本類型,對(duì)于性能瀕于臨界的系統(tǒng)來說該方案是比較危險(xiǎn)的,特別是大數(shù)量的時(shí)候,首先,在初始化List數(shù)組時(shí)要進(jìn)行裝箱操作,把一個(gè)int類型包裝成一個(gè)Integer對(duì)象,雖然有整型池在,但不在整型池范圍內(nèi)的都會(huì)產(chǎn)生一個(gè)新的Integer對(duì)象,而且眾所周知,基本類型是在棧內(nèi)存中操作的,而對(duì)象是堆內(nèi)存中操作的,棧內(nèi)存的特點(diǎn)是:速度快,容量小;堆內(nèi)存的特點(diǎn)是:速度慢,容量大(從性能上講,基本類型的處理占優(yōu)勢(shì))。其次,在進(jìn)行求和運(yùn)算時(shí)(或者其它遍歷計(jì)算)時(shí)要做拆箱動(dòng)作,因此無謂的性能消耗也就產(chǎn)生了。在實(shí)際測(cè)試中發(fā)現(xiàn):對(duì)基本類型進(jìn)行求和運(yùn)算時(shí),數(shù)組的效率是集合的10倍。
注意:性能要求較高的場(chǎng)景中使用數(shù)組代替集合。
回到頂部建議61:若有必要,使用變長數(shù)組
Java中的數(shù)組是定長的,一旦經(jīng)過初始化聲明就不可改變長度,這在實(shí)際使用中非常不方便,比如要對(duì)班級(jí)學(xué)生的信息進(jìn)行統(tǒng)計(jì),因?yàn)槲覀儾恢酪粋€(gè)班級(jí)會(huì)有多少學(xué)生(隨時(shí)都可能會(huì)有學(xué)生入學(xué)、退學(xué)或轉(zhuǎn)學(xué)),所以需要一個(gè)足夠大的數(shù)組來容納所有的學(xué)生,但問題是多大才算足夠大?20年前一臺(tái)臺(tái)式機(jī)64MB的內(nèi)存已經(jīng)很牛了,現(xiàn)在要是沒有2GB的內(nèi)存(現(xiàn)在這個(gè)都太小了)你都不好意思跟別人交流計(jì)算機(jī)的配置,所以呀,這個(gè)足夠大是相對(duì)于當(dāng)時(shí)的場(chǎng)景而言的,隨著環(huán)境的變化,"足夠大"也可能會(huì)轉(zhuǎn)變成"足夠小",然后就會(huì)出現(xiàn)超出數(shù)組最大容量的情況,那該如何解決呢?事實(shí)上,可以通過對(duì)數(shù)組擴(kuò)容"婉轉(zhuǎn)" 地解決該問題,代碼如下:
1 public static <T> T[] expandCapacity(T[] datas, int newLen) { 2 // 不能是負(fù)值 3 newLen = newLen < 0 ? 0 : newLen; 4 // 生成一個(gè)新數(shù)組,并拷貝原值 5 return Arrays.copyOf(datas, newLen); 6 }上述代碼采用的是Arrays數(shù)組工具類的copyOf方法,產(chǎn)生了一個(gè)newLen長度的新數(shù)組,并把原有的值拷貝了進(jìn)去,之后就可以對(duì)超長的元素進(jìn)行賦值了(依據(jù)類型的不同分別賦值0、false或null),使用方法如下:
public class Client61 {public static void main(String[] args) {//一個(gè)班級(jí)最多容納60個(gè)學(xué)生Stu [] stuNums= new Stu[60];//stuNums初始化......//偶爾一個(gè)班級(jí)可以容納80人,數(shù)組加長stuNums=expandCapacity(stuNums,80);/* 重新初始化超過限額的20人...... */}public static <T> T[] expandCapacity(T[] datas, int newLen) {// 不能是負(fù)值newLen = newLen < 0 ? 0 : newLen;// 生成一個(gè)新數(shù)組,并拷貝原值return Arrays.copyOf(datas, newLen);} } class Stu{}通過這樣的處理方式,曲折的解決了數(shù)組的變長問題,其實(shí),集合的長度自動(dòng)維護(hù)功能的原理與此類似。在實(shí)際開發(fā)中,如果確實(shí)需要變長的數(shù)據(jù)集,數(shù)組也是在考慮范圍之內(nèi)的,不能因固定長度而將其否定之。
回到頂部建議62:警惕數(shù)組的淺拷貝
? 有這樣一個(gè)例子,第一個(gè)箱子里有赤橙黃綠青藍(lán)紫7色氣球,現(xiàn)在希望在第二個(gè)箱子中也放入7個(gè)氣球,其中最后一個(gè)氣球改為藍(lán)色,也就是赤橙黃綠青藍(lán)藍(lán)7個(gè)氣球,那我們很容易就會(huì)想到第二個(gè)箱子中的氣球可以通過拷貝第一個(gè)箱子中的氣球來實(shí)現(xiàn),畢竟有6個(gè)氣球是一樣的嘛,來看實(shí)現(xiàn)代碼:
1 import java.util.Arrays;2 import org.apache.commons.lang.builder.ToStringBuilder;3 4 public class Client62 {5 public static void main(String[] args) {6 // 氣球數(shù)量7 int ballonNum = 7;8 // 第一個(gè)箱子9 Balloon[] box1 = new Balloon[ballonNum]; 10 // 初始化第一個(gè)箱子中的氣球 11 for (int i = 0; i < ballonNum; i++) { 12 box1[i] = new Balloon(Color.values()[i], i); 13 } 14 // 第二個(gè)箱子的氣球是拷貝第一個(gè)箱子里的 15 Balloon[] box2 = Arrays.copyOf(box1, box1.length); 16 // 修改最后一個(gè)氣球顏色 17 box2[6].setColor(Color.Blue); 18 // 打印出第一個(gè)箱子中的氣球顏色 19 for (Balloon b : box1) { 20 System.out.println(b); 21 } 22 23 } 24 } 25 26 // 氣球顏色 27 enum Color { 28 Red, Orange, Yellow, Green, Indigo, Blue, Violet 29 } 30 31 // 氣球 32 class Balloon { 33 // 編號(hào) 34 private int id; 35 // 顏色 36 private Color color; 37 38 public Balloon(Color _color, int _id) { 39 color = _color; 40 id = _id; 41 } 42 43 public int getId() { 44 return id; 45 } 46 47 public void setId(int id) { 48 this.id = id; 49 } 50 51 public Color getColor() { 52 return color; 53 } 54 55 public void setColor(Color color) { 56 this.color = color; 57 } 58 59 @Override 60 public String toString() { 61 //apache-common-lang包下的ToStringBuilder重寫toString方法 62 return new ToStringBuilder(this).append("編號(hào)", id).append("顏色", color).toString(); 63 } 64 65 }第二個(gè)箱子里最后一個(gè)氣球的顏色毫無疑問是被修改為藍(lán)色了,不過我們是通過拷貝第一個(gè)箱子里的氣球然后再修改的方式來實(shí)現(xiàn)的,那會(huì)對(duì)第一個(gè)箱子的氣球顏色有影響嗎?我們看看輸出結(jié)果:
最后一個(gè)氣球顏色竟然也被修改了,我們只是希望修改第二個(gè)箱子的氣球啊,這是為何?這是典型的淺拷貝(Shallow ?Clone)問題,以前第一章序列化時(shí)講過,但是這里與之有一點(diǎn)不同:數(shù)組中的元素沒有實(shí)現(xiàn)Serializable接口。
確實(shí)如此,通過copyOf方法產(chǎn)生的數(shù)組是一個(gè)淺拷貝,這與序列化的淺拷貝完全相同:基本類型是直接拷貝值,其它都是拷貝引用地址。需要說明的是,數(shù)組的clone方法也是與此相同的,同樣是淺拷貝,而且集合的clone方法也都是淺拷貝,這就需要大家在拷貝時(shí)多留心了。
問題找到了,解決辦法也很簡單,遍歷box1的每個(gè)元素,重新生成一個(gè)氣球(Balloon)對(duì)象,并放置到box2數(shù)組中,代碼比較簡單,不再贅述。
該方法用的最多的地方是在使用集合(如List),進(jìn)行業(yè)務(wù)處理時(shí),比如發(fā)覺需要拷貝集合中的元素,可集合沒有提供拷貝方法,如果自己寫會(huì)很麻煩,所以干脆使用List.toArray方法轉(zhuǎn)換成數(shù)組,然后通過Arrays.copyOf拷貝,再轉(zhuǎn)換回集合,簡單便捷!但是,非常遺憾的是,這里我們又撞到淺拷貝的槍口上了,雖然很多時(shí)候淺拷貝可以解決業(yè)務(wù)問題,但更多時(shí)候會(huì)留下隱患,我們需要提防又提防。
回到頂部建議63:在明確的場(chǎng)景下,為集合指定初始容量
? 我們經(jīng)常使用ArrayList、Vector、HashMap等集合,一般都是直接用new跟上類名聲明出一個(gè)集合來,然后使用add、remove等方法進(jìn)行操作,而且因?yàn)樗亲詣?dòng)管理長度的,所以不用我們特別費(fèi)心超長的問題,這確實(shí)是一個(gè)非常好的優(yōu)點(diǎn),但也有我們必須要注意的事項(xiàng)。
下面以ArrayList為例深入了解一下Java是如何實(shí)現(xiàn)長度的動(dòng)態(tài)管理的,先從add方法的閱讀開始,代碼(JDK7)如下:
1 public boolean add(E e) { 2 //擴(kuò)展長度 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 //追加元素 5 elementData[size++] = e; 6 return true; 7 }我們知道ArrayList是一個(gè)大小可變的數(shù)組,但它在底層使用的是數(shù)組存儲(chǔ)(也就是elementData變量),而且數(shù)組長度是定長的,要實(shí)現(xiàn)動(dòng)態(tài)長度必然要進(jìn)行長度的擴(kuò)展,ensureCapacityInternal方法提供了此功能,代碼如下:
private void ensureCapacityInternal(int minCapacity) {//修改計(jì)數(shù)器modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}private void grow(int minCapacity) {// overflow-conscious code//上次(原始)定義的數(shù)組長度int oldCapacity = elementData.length;//新長度為原始長度+原始長度右移一位 ==>原始長度的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://數(shù)組拷貝,生成新數(shù)組elementData = Arrays.copyOf(elementData, newCapacity);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}大概分析一下這些源碼,這個(gè)源碼還是JDK7之前的版本上做了優(yōu)化處理的。先說一下第一個(gè)方法ensureCapacityIntenal,?方法名的英文大致意思是“確保內(nèi)部容量”,這里要說明,size表示的是現(xiàn)有的元素個(gè)數(shù),并非ArrayList的容量,容量應(yīng)該是數(shù)組 elementData的長度。參數(shù)minCapacity是需要檢查的最小容量,即方法的功能就是確保elementData的長度不小于 minCapacity,如果不夠,則調(diào)用grow增加容量。容量的增長也算結(jié)構(gòu)性變動(dòng),所以modCount需要加1。
grow方法:先對(duì)容量擴(kuò)大1.5倍,這里oldCapacity >> 1是二進(jìn)制操作右移,相當(dāng)于除以2,如果不知道這個(gè)面壁去吧。接再來把新的臨時(shí)容量(還沒正式改變?nèi)萘?#xff0c;應(yīng)該叫預(yù)期容量)和實(shí)際需要的最小容量比較,如果還不滿 足,則把臨時(shí)容量改成需要的最小容量值。在判斷容量是否超過MAX_ARRAY_SIZE的值,MAX_ARRAY_SIZE值為 Integer.MAX_VALUE - 8,比int的最大值小8,不知道設(shè)計(jì)初衷是什么,可能方便判斷吧。如果已經(jīng)超過,調(diào)用hugeCapacity方法檢查容量的int值是不是已經(jīng)溢出。一般很 少用到int最大值的情況,那么多數(shù)據(jù)也不會(huì)用ArrayList來做容器了,估計(jì)沒機(jī)會(huì)見到hugeCapacity運(yùn)行一次了。最后確定了新的容量,就使用Arrays.copyOf方法來生成新的數(shù)組,copyOf也已經(jīng)完成了將就的數(shù)據(jù)拷貝到新數(shù)組的工作。
回歸正題,大家注意看數(shù)組長度的計(jì)算方法,并不是增加一個(gè)元素,elementData的長度就加1,而是在達(dá)到elementData長度的臨界點(diǎn)時(shí),才將elementData擴(kuò)容1.5倍,這樣實(shí)現(xiàn)避免了多次copyOf方法的性能開銷,否則每增加一個(gè)元素都要擴(kuò)容一次,那性能會(huì)更差。不知道大家有沒有這樣一個(gè)疑問,為啥要擴(kuò)容1.5倍,而不是2.5,倍、3.5倍呢?其實(shí)我也這么想過,原因是一次擴(kuò)容太大,占用的內(nèi)存就越大,浪費(fèi)的內(nèi)存也就越多(1.5倍擴(kuò)容,最多浪費(fèi)33%的數(shù)組空間,而2.5倍則最多消耗60%的內(nèi)存),而一次擴(kuò)容太小,則需要多次對(duì)數(shù)組重新分配內(nèi)存,性能消耗嚴(yán)重,經(jīng)過測(cè)試驗(yàn)證,擴(kuò)容1.5倍既滿足了性能要求,也減少了內(nèi)存消耗。
現(xiàn)在我們知道了ArrayList的擴(kuò)容原則,那還有一個(gè)問題:elementData的默認(rèn)長度是多少呢?答案是10,如果我們使用默認(rèn)方式聲明ArrayList,如new ArrayList(),則elementData的初始長度是10,我們看看ArrayList的三個(gè)構(gòu)造函數(shù)。
//無參構(gòu)造 public ArrayList() {this(10);}//構(gòu)造一個(gè)具有指定初始容量的空列表。 public ArrayList(int initialCapacity) {super();if (initialCapacity < 0)throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);this.elementData = new Object[initialCapacity];}//構(gòu)造一個(gè)包含指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排列的 public ArrayList(Collection<? extends E> c) {elementData = c.toArray();size = elementData.length;// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);}ArrayList():默認(rèn)構(gòu)造函數(shù),提供初始容量為10的空列表。
???? ArrayList(int initialCapacity):構(gòu)造一個(gè)具有指定初始容量的空列表。
???? ArrayList(Collection<? extends?E> c):構(gòu)造一個(gè)包含指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排列的。
? 從這里我們可以看出,如果不設(shè)置初始容量,系統(tǒng)會(huì)按照1.5倍的規(guī)則擴(kuò)容,每次擴(kuò)容都是一次數(shù)組的拷貝,如果數(shù)據(jù)量大,這樣的拷貝會(huì)非常消耗資源,而且效率非常低下。所以,我們?nèi)绻酪粋€(gè)ArrayList的可能長度,然后對(duì)ArrayList設(shè)置一個(gè)初始容量則可以顯著提高系統(tǒng)性能。
其它的集合如Vector和ArrayList類似,只是擴(kuò)容的倍數(shù)不同而已,Vector擴(kuò)容2倍,大家有興趣的話可以看看Vector,HashMap的JDK源碼。
回到頂部建議64:多種最值算法,適時(shí)選擇
? 對(duì)一批數(shù)據(jù)進(jìn)行排序,然后找出其中的最大值或最小值,這是基本的數(shù)據(jù)結(jié)構(gòu)知識(shí)。在Java中我們可以通過編寫算法的方式,也可以通過數(shù)組先排序再取值的方式來實(shí)現(xiàn),下面以求最大值為例,解釋一下多種算法:
(1)、自行實(shí)現(xiàn),快速查找最大值
先看看用快速查找法取最大值的算法,代碼如下:
1 public static int max(int[] data) { 2 int max = data[0]; 3 for (int i : data) { 4 max = max > i ? max : i; 5 } 6 return max; 7 }這是我們經(jīng)常使用的最大值算法,也是速度最快的算法。它不要求排序,只要遍歷一遍數(shù)組即可找出最大值。
(2)、先排序,后取值
對(duì)于求最大值,也可以采用先排序后取值的方式,代碼如下:
1 public static int max(int[] data) { 2 Arrays.sort(data); 3 return data[data.length - 1]; 4 }從效率上講,當(dāng)然是自己寫快速查找法更快一些了,只用遍歷一遍就可以計(jì)算出最大值,但在實(shí)際測(cè)試中發(fā)現(xiàn),如果數(shù)組量少于10000,兩個(gè)基本上沒有區(qū)別,但在同一個(gè)毫秒級(jí)別里,此時(shí)就可以不用自己寫算法了,直接使用數(shù)組先排序后取值的方式。
如果數(shù)組元素超過10000,就需要依據(jù)實(shí)際情況來考慮:自己實(shí)現(xiàn),可以提高性能;先排序后取值,簡單,通俗易懂。排除性能上的差異,兩者都可以選擇,甚至后者更方便一些,也更容易想到。
現(xiàn)在問題來了,在代碼中為什么先使用data.clone拷貝再排序呢?那是因?yàn)閿?shù)組也是一個(gè)對(duì)象,不拷貝就改變了原有的數(shù)組元素的順序嗎?除非數(shù)組元素的順序無關(guān)緊要。那如果要查找僅次于最大值的元素(也就是老二),該如何處理呢?要注意,數(shù)組的元素時(shí)可以重復(fù)的,最大值可能是多個(gè),所以單單一個(gè)排序然后取倒數(shù)第二個(gè)元素時(shí)解決不了問題的。
此時(shí),就需要一個(gè)特殊的排序算法了,先要剔除重復(fù)數(shù)據(jù),然后再排序,當(dāng)然,自己寫算法也可以實(shí)現(xiàn),但是集合類已經(jīng)提供了非常好的方法,要是再使用自己寫算法就顯得有點(diǎn)重復(fù)造輪子了。數(shù)組不能剔除重復(fù)數(shù)據(jù),但Set集合卻是可以的,而且Set的子類TreeSet還能自動(dòng)排序,代碼如下:
1 public static int getSecond(Integer[] data) { 2 //轉(zhuǎn)換為列表 3 List<Integer> dataList = Arrays.asList(data); 4 //轉(zhuǎn)換為TreeSet,剔除重復(fù)元素并升序排列 5 TreeSet<Integer> ts = new TreeSet<Integer>(dataList); 6 //取得比最大值小的最大值,也就是老二了 7 return ts.lower(ts.last()); 8 }剔除重復(fù)元素并升序排列,這都是由TreeSet類實(shí)現(xiàn)的,然后可再使用lower方法尋找小于最大值的值,大家看,上面的程序非常簡單吧?那如果是我們自己編寫代碼會(huì)怎么樣呢?那至少要遍歷數(shù)組兩遍才能計(jì)算出老二的值,代碼復(fù)雜度將大大提升。因此在實(shí)際應(yīng)用中求最值,包括最大值、最小值、倒數(shù)第二小值等,使用集合是最簡單的方式,當(dāng)然從性能方面來考慮,數(shù)組才是最好的選擇。
注意:最值計(jì)算時(shí)使用集合最簡單,使用數(shù)組性能最優(yōu)。
作者:阿赫瓦里 出處:http://www.cnblogs.com/selene/ 本文以學(xué)習(xí)、研究和分享為主,版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,如果文中有不妥或者錯(cuò)誤的地方還望大神您不吝指出。如果覺得本文對(duì)您有所幫助不如【推薦】一下吧!如果你有更好的建議,不如留言一起討論,共同進(jìn)步!此外,大家也可以支持一下自家蘋果, 再次感謝您耐心的讀完本篇文章。轉(zhuǎn)載于:https://www.cnblogs.com/LH923613603/p/7163707.html
總結(jié)
以上是生活随笔為你收集整理的转载--编写高质量代码:改善Java程序的151个建议(第5章:数组和集合___建议60~64)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: u盘拷东西拷满显示0字节要格式化怎么办
- 下一篇: 不能在 UTF8 和 UCS2 之间转换