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