以下文章來源于程序新視界 ,作者丑胖俠二師兄
雖然隨著Java版本的演變,數(shù)組的分量在慢慢減弱,日常使用時大多使用List進(jìn)行替代。但ArrayList底層依舊采用數(shù)組來進(jìn)行實(shí)現(xiàn),而數(shù)組依舊有很多應(yīng)用的場景。在使用數(shù)組的過程中,你是否匯總過數(shù)組的特性及功能,并停下來思考幾個為什么呢?如果沒有,本篇文章將帶領(lǐng)大家從頭梳理一下數(shù)組,一定會為你帶來一些未曾發(fā)掘的特性和功能。
何謂數(shù)組
學(xué)習(xí)數(shù)組,我們最先要知道的就是它是什么,能做什么?
數(shù)組,就是相同類型的對象或基本類型數(shù)據(jù)的集合。也可以理解為把有限個類型相同的元素按一定順序排列的集合,然后用一個名字命名,用編號區(qū)分具體的元素。而這個名字稱為數(shù)組名,編號稱為下標(biāo)。
數(shù)組在內(nèi)存中是連續(xù)存儲的,所以索引速度是非常的快,數(shù)組的賦值與修改元素也很簡單。但是數(shù)組也有不足的地方,那就是如果想在兩個相鄰元素之間插入新的元素會非常麻煩。
另外,數(shù)組聲明的時候必須指定數(shù)組的長度,而數(shù)組的長度是不可變的。在此,數(shù)組長度過長會造成內(nèi)存浪費(fèi),長度過短則會造成溢出。
數(shù)組的應(yīng)用場景
上面提到了數(shù)組的那么多缺點(diǎn),但我們知道“存在即合理”,下面看看哪些場景適合數(shù)組的使用。
1、數(shù)據(jù)比較少,能夠確定長度;存取或修改操作較多,插入和刪除較少的情況。
2、使用(遍歷)時,經(jīng)常需要按照序號來進(jìn)行訪問數(shù)據(jù)元素或做運(yùn)算的情況。
3、對性能要求較高時,數(shù)組是首選。
也正是由于性能較高,所以我們在閱讀源碼時經(jīng)常會看到數(shù)組的身影。特別是針對基礎(chǔ)類型進(jìn)行操作,效率提升甚至可以達(dá)到基于List等集合性能的10倍。
以下面一段遍歷數(shù)組和List求和的場景來做對比。??????
// 對數(shù)組求和publicstaticintsum(int[] datas){intsum =0;for(intdata : datas) {? ? sum += data;? }returnsum;}// 對List求和publicstaticintsum(List<Integer> datas){intsum =0;for(Integer data : datas) {// 拆箱操作sum += data;? }returnsum;}
在上述兩個方法中,影響性能的最大地方便是List中的Integer對象的拆箱和裝箱操作,特別是數(shù)據(jù)量比較大的時候。我們都知道基礎(chǔ)類型是在棧內(nèi)存中操作的,而對象是在堆內(nèi)存中操作的。棧內(nèi)存的特點(diǎn)是速度快、容量小,堆內(nèi)存的特點(diǎn)是速度慢、容量大,因此從性能上來講,基本類型的處理占優(yōu)勢。
有同學(xué)可能會說了有整型緩存池的存在。但整型緩存池容納的是﹣128到127之間的Integer對象,超過這個范圍便需要創(chuàng)建Integer對象了,而超過這個容納范圍基本上是大概率事件。
數(shù)據(jù)變量定義
下面來說說數(shù)組的名稱定義,我們可以通過兩種形式來進(jìn)行聲明數(shù)組:
int[]a;intb[];
其中后一種格式符合C和C++程序員的習(xí)慣,如果你是Java開發(fā)人員,建議統(tǒng)一使用前一種。為什么呢?因?yàn)榍耙环N從語義上來說更合理,它表示“一個int型數(shù)組”。
拓展一下:如果你懂一些其他編程語言,比如C語言,你會看到類似如下的聲明。
int?A[10];
Java中卻不能如此聲明。思考一下為什么?
這個要回到Java的“引用”問題上。我們在上述代碼中聲明的只是數(shù)組的一個引用,JVM會為該引用分配存儲空間。但是,這個引用并沒有指向任何對象,也就是說沒有給數(shù)組對象本身分配任何空間。只有在數(shù)組真正創(chuàng)建時才會分配空間。因此,編譯器不允許在此指定數(shù)組的大小。
數(shù)組的創(chuàng)建與初始化
數(shù)組的創(chuàng)建與初始化有兩種形式:
//方式一的創(chuàng)建int[]a=newint[5];//方式一的初始化a[1]=1;a[2]=2;a[3]=3;a[4]=4;//方式二(創(chuàng)建+初始化)int[]b={0,1,2,3,4};
第一種方式通過new關(guān)鍵字創(chuàng)建一個指定長度(為5)的數(shù)組,然后通過數(shù)組下標(biāo)對內(nèi)容進(jìn)行逐一初始化。那么,如果不進(jìn)行逐一初始化會怎樣?默認(rèn)會采用int類型的默認(rèn)值,也就是0進(jìn)行初始化。
第二種方式,創(chuàng)建與初始化融為一體,其實(shí)也采用了new關(guān)鍵字進(jìn)行創(chuàng)建,只不過是編譯器負(fù)責(zé)來做,更加方便一些。
拓展一下:我們可以通過方式二的形式進(jìn)行數(shù)組的創(chuàng)建和初始化,那么為什么還提供了int[] a這種基于數(shù)組引用的聲明呢?
這是因?yàn)樵贘ava中,可以將一個數(shù)組的引用賦值給另外一個數(shù)組。比如,我們可以如下方式使用:
int[]c;int[]b={0,1,2,3,4};c=b;
經(jīng)過c=b的操作,數(shù)組c的引用同樣指向了b。這里又會出現(xiàn)一個我們常見的面試題??纯聪旅娲a打印的結(jié)果是什么:
publicstaticvoidmain(String[] args) {String[] strings = {"a","b","c"};Stringstring="abc";? ? change(strings,string);? ? System.out.println(strings[1]);? ? System.out.println(string.charAt(1));}publicstaticvoidchange(String[] strings,Stringstring){? ? strings[1] ="e";string="aec";}
想好答案了吧?現(xiàn)在公布答案:第一行打印的是“e”,第二行打印的“b”。這與上面所說的數(shù)組的引用有密切關(guān)聯(lián),數(shù)組傳遞進(jìn)入change方法的是引用,而String類型的參數(shù)傳遞的只是值的copy。
數(shù)組的存儲結(jié)構(gòu)
這里我們再以一張簡單的圖展示一下,數(shù)組在內(nèi)存中存儲的形式。
?
上圖需注意的是數(shù)組使用的存儲空間是連續(xù)的。其中創(chuàng)建的對象通常位于堆中,上圖對堆中的數(shù)據(jù)存儲進(jìn)行了簡化示意。
數(shù)組的長度
在很久之前,面試的時候還出現(xiàn)這樣的面試題:如何獲取數(shù)組的長度?
當(dāng)然,我們知道該面試題考察的就是通過length屬性獲取數(shù)組長度與通過size()方法獲取集合長度的區(qū)別。
所有的數(shù)組都有一個固定的成員,可以通過它來獲取數(shù)組的長度,這便是length屬性。在使用的過程中我們需要注意的是數(shù)組的下標(biāo)是從0開始計算的。因此,我們在遍歷或修改數(shù)組的時候,需要注意數(shù)組的下標(biāo)最大值是length-1,否則,會出現(xiàn)數(shù)組越界的問題。
數(shù)組的處理
針對數(shù)組,Java標(biāo)準(zhǔn)類庫里特意提供了Arrays類,我們可以通過該類提供的方法進(jìn)行數(shù)組的處理。
數(shù)組的打印
可通過Arrays.toString()方法對數(shù)組的內(nèi)容進(jìn)行打印。下面通過示例我們來對比一下通過toString方法和直接打印的區(qū)別。???????
String[] strings = {"a","b","c"};System.out.println(strings);System.out.println(Arrays.toString(strings));
打印結(jié)果:
[Ljava.lang.String;@36baf30c[a, e, c]
可以看到,如果直接打印則打印出來的是strings數(shù)組的引用,而并不是真實(shí)的內(nèi)容。
數(shù)組的排序
可通過Arrays.sort()方法對數(shù)組進(jìn)行排序,但對于數(shù)組中的元素有一定的要求,要實(shí)現(xiàn)Comparable接口??聪旅娴膶?shí)例:???????
String[] sorts = {"c","b","a"};Arrays.sort(sorts);System.out.println(Arrays.toString(sorts));
打印結(jié)果:
[a, b, c]
很明顯已經(jīng)進(jìn)行正常排序了。為什么String可以直接進(jìn)行排序?那是因?yàn)镾tring已經(jīng)實(shí)現(xiàn)了Comparable接口。???????
publicfinalclassStringimplementsjava.io.Serializable,Comparable,CharSequence{}
另外,對于數(shù)組的排序還有常見的:冒泡排序、快速排序、選擇排序、插入排序、希爾(Shell)排序、堆排序等。面試過程中的排序往往也是基于數(shù)組來進(jìn)行展開的。感興趣的朋友可拿數(shù)組來練習(xí)一下排序的算法。
數(shù)組轉(zhuǎn)集合
通過Arrays.asList()方法,可將數(shù)組轉(zhuǎn)化為列表。
String[] sorts = {"程序","新","視界"};List list = Arrays.asList(sorts);System.out.println(list);
打印結(jié)果:
[程序, 新, 視界]
關(guān)于asList的源碼如下:
publicstaticListasList(T... a){returnnewArrayList<>(a);}
看到asList源碼,你能想到什么?是不是發(fā)現(xiàn)該方法的參數(shù)為可變參數(shù),并且支持?jǐn)?shù)組作為參數(shù)傳入。關(guān)于可變參數(shù),下篇文章我們會詳細(xì)講一下,別忘記關(guān)注公眾號“程序新視界”學(xué)習(xí)。
當(dāng)然,這里也可以轉(zhuǎn)化為Set集合,但需創(chuàng)建一個Set的實(shí)現(xiàn)類(這里用HashSet),將asList的結(jié)果作為參數(shù)傳入:
Set?sets?=?new?HashSet<>(Arrays.asList(sorts));
數(shù)組內(nèi)容查找
可以通過Arrays.binarySearch()方法來對數(shù)據(jù)中的元素進(jìn)行查找,顧名思義,這里是通過二分查找法進(jìn)行查找的。
String[] sorts = {"c","a","b"};Arrays.sort(sorts);intindex= Arrays.binarySearch(sorts,"b");System.out.println(index);System.out.println(sorts[index]);
打印結(jié)果:
1b
結(jié)果中的"1"指的是字符串所在的下標(biāo)值,通過下標(biāo)可以獲得對應(yīng)位置的值。這里需要注意的是,既然是二分查找法,那么在查找之前必定需要進(jìn)行排序,不然二分查找的意義便不存在了。
數(shù)組的拷貝
可以通過Arrays.copyOf()方法對數(shù)組進(jìn)行復(fù)制,其中第一個參數(shù)是被復(fù)制數(shù)組,第二個參數(shù)為新數(shù)組的長度,返回的結(jié)果為新的數(shù)組。示例如下:???????
int[]sourceArray={1,3,5,7,0};int[]newArray=Arrays.copyOf(sourceArray,sourceArray.length);System.out.println(Arrays.toString(newArray));
打印結(jié)果:
[1,?3,?5,?7,?0]
此時,需要思考一個問題Arrays.copyOf()復(fù)制的功能是一個什么層次的復(fù)制。也就說,如果修改新數(shù)組的值,是否會影響到原有數(shù)組。
先猜測一下,下面看示例代碼:???????
int[]sourceArray={1,3,5,7,0};int[]newArray=Arrays.copyOf(sourceArray,sourceArray.length);newArray[1]=8;System.out.println(Arrays.toString(newArray));System.out.println(Arrays.toString(sourceArray));
打印結(jié)果:
[1,8,5,7,0][1,3,5,7,0]
結(jié)果能說明什么?說明Arrays.copyOf()的復(fù)制功能是創(chuàng)建一個全新的數(shù)組及數(shù)組元素嗎?NO,NO,NO!
我們再來看另外一個示例,先創(chuàng)建一個User對象,源碼如下:???????
publicclassUser{privateString userNo;publicUser(String userNo){this.userNo = userNo;? }publicStringgetUserNo(){returnuserNo;? }publicvoidsetUserNo(String userNo){this.userNo = userNo;? }}
然后創(chuàng)建數(shù)組進(jìn)行復(fù)制操作,復(fù)制完成之后對新數(shù)組的數(shù)據(jù)進(jìn)行修改。???????
User[] sourceArray = {newUser("N1"),newUser("N2"),newUser("N3")};User[] newArray = Arrays.copyOf(sourceArray, sourceArray.length);newArray[1].setUserNo("N4");System.out.println(newArray[1].getUserNo());System.out.println(sourceArray[1].getUserNo());
打印結(jié)果如下:
N4N4
我們在代碼中只是修改了新數(shù)組中的User的屬性,結(jié)果原有數(shù)組的值也同樣被修改了。
上面的兩個示例說明數(shù)組的copy操作只是一個淺拷貝。這與序列化的淺拷貝完全相同:基本類型是直接拷貝值,其他都是拷貝引用地址。
同樣,數(shù)組和集合的clone也是如此,同樣是淺拷貝,使用時需多加留意。
基于數(shù)組淺拷貝實(shí)現(xiàn)變長數(shù)組
關(guān)于List是如何實(shí)現(xiàn)變長的,大家可以參考List的源碼進(jìn)行學(xué)習(xí)。這里基于上面提到的Arrays.copyOf()方法的功能來實(shí)現(xiàn)動態(tài)變長。
實(shí)現(xiàn)原理很簡單,就是基于Arrays.copyOf()方法的第二個參數(shù)來進(jìn)行擴(kuò)容。
相關(guān)方法如下:???????
publicstaticT[]expandCapacity(T[] datas,intnewLen){// 校驗(yàn)長度值,如果小于0,則為0newLen = Math.max(newLen,0);// 生成一個新數(shù)組,并拷貝原值,指定新的數(shù)組長度returnArrays.copyOf(datas, newLen);}
在上述方法中除了校驗(yàn)部分,核心機(jī)制便是利用了Arrays.copyOf()方法來實(shí)現(xiàn)一個可變長的數(shù)組。
小結(jié)
關(guān)于數(shù)組部分,我們就講這么多,其實(shí)數(shù)組還有多維數(shù)組以及通過Arrays.asList()方法轉(zhuǎn)換為List之后基于List的更多操作,在這里我們就不進(jìn)行拓展了。感興趣的朋友可自行實(shí)踐。