如下文章來源於程序新視界 ,做者醜胖俠二師兄
雖然隨着Java版本的演變,數組的份量在慢慢減弱,平常使用時大多使用List進行替代。但ArrayList底層依舊採用數組來進行實現,而數組依舊有不少應用的場景。在使用數組的過程當中,你是否彙總過數組的特性及功能,並停下來思考幾個爲何呢?若是沒有,本篇文章將帶領你們從頭梳理一下數組,必定會爲你帶來一些不曾發掘的特性和功能。java
學習數組,咱們最早要知道的就是它是什麼,能作什麼?程序員
數組,就是相同類型的對象或基本類型數據的集合。也能夠理解爲把有限個類型相同的元素按必定順序排列的集合,而後用一個名字命名,用編號區分具體的元素。而這個名字稱爲數組名,編號稱爲下標。面試
數組在內存中是連續存儲的,因此索引速度是很是的快,數組的賦值與修改元素也很簡單。可是數組也有不足的地方,那就是若是想在兩個相鄰元素之間插入新的元素會很是麻煩。算法
另外,數組聲明的時候必須指定數組的長度,而數組的長度是不可變的。在此,數組長度過長會形成內存浪費,長度太短則會形成溢出。編程
上面提到了數組的那麼多缺點,但咱們知道「存在即合理」,下面看看哪些場景適合數組的使用。數組
一、數據比較少,可以肯定長度;存取或修改操做較多,插入和刪除較少的狀況。緩存
二、使用(遍歷)時,常常須要按照序號來進行訪問數據元素或作運算的狀況。編程語言
三、對性能要求較高時,數組是首選。性能
也正是因爲性能較高,因此咱們在閱讀源碼時常常會看到數組的身影。特別是針對基礎類型進行操做,效率提高甚至能夠達到基於List等集合性能的10倍。學習
如下面一段遍歷數組和List求和的場景來作對比。
// 對數組求和 public static int sum(int[] datas) { int sum = 0; for (int data : datas) { sum += data; } return sum; } // 對List求和 public static int sum(List<Integer> datas) { int sum = 0; for (Integer data : datas) { // 拆箱操做 sum += data; } return sum; }
在上述兩個方法中,影響性能的最大地方即是List中的Integer對象的拆箱和裝箱操做,特別是數據量比較大的時候。咱們都知道基礎類型是在棧內存中操做的,而對象是在堆內存中操做的。棧內存的特色是速度快、容量小,堆內存的特色是速度慢、容量大,所以從性能上來說,基本類型的處理佔優點。
有同窗可能會說了有整型緩存池的存在。但整型緩存池容納的是﹣128到127之間的Integer對象,超過這個範圍便須要建立Integer對象了,而超過這個容納範圍基本上是大機率事件。
下面來講說數組的名稱定義,咱們能夠經過兩種形式來進行聲明數組:
int[] a; int b[];
其中後一種格式符合C和C++程序員的習慣,若是你是Java開發人員,建議統一使用前一種。爲何呢?由於前一種從語義上來講更合理,它表示「一個int型數組」。
拓展一下:若是你懂一些其餘編程語言,好比C語言,你會看到相似以下的聲明。
int A[10];
Java中卻不能如此聲明。思考一下爲何?
這個要回到Java的「引用」問題上。咱們在上述代碼中聲明的只是數組的一個引用,JVM會爲該引用分配存儲空間。可是,這個引用並無指向任何對象,也就是說沒有給數組對象自己分配任何空間。只有在數組真正建立時纔會分配空間。所以,編譯器不容許在此指定數組的大小。
數組的建立與初始化有兩種形式:
// 方式一的建立 int[] a = new int[5]; // 方式一的初始化 a[1] = 1; a[2] = 2; a[3] = 3; a[4] = 4; // 方式二(建立+初始化) int[] b = {0, 1, 2, 3, 4};
第一種方式經過new關鍵字建立一個指定長度(爲5)的數組,而後經過數組下標對內容進行逐一初始化。那麼,若是不進行逐一初始化會怎樣?默認會採用int類型的默認值,也就是0進行初始化。
第二種方式,建立與初始化融爲一體,其實也採用了new關鍵字進行建立,只不過是編譯器負責來作,更加方便一些。
拓展一下:咱們能夠經過方式二的形式進行數組的建立和初始化,那麼爲何還提供了int[] a這種基於數組引用的聲明呢?
這是由於在Java中,能夠將一個數組的引用賦值給另一個數組。好比,咱們能夠以下方式使用:
int[] c; int[] b = {0, 1, 2, 3, 4}; c = b;
通過c=b的操做,數組c的引用一樣指向了b。這裏又會出現一個咱們常見的面試題。看看下面代碼打印的結果是什麼:
public static void main(String[] args) { String[] strings = {"a","b","c"}; String string = "abc"; change(strings,string); System.out.println(strings[1]); System.out.println(string.charAt(1)); } public static void change(String[] strings,String string){ strings[1] = "e"; string = "aec"; }
想好答案了吧?如今公佈答案:第一行打印的是「e」,第二行打印的「b」。這與上面所說的數組的引用有密切關聯,數組傳遞進入change方法的是引用,而String類型的參數傳遞的只是值的copy。
這裏咱們再以一張簡單的圖展現一下,數組在內存中存儲的形式。
上圖需注意的是數組使用的存儲空間是連續的。其中建立的對象一般位於堆中,上圖對堆中的數據存儲進行了簡化示意。
在好久以前,面試的時候還出現這樣的面試題:如何獲取數組的長度?
固然,咱們知道該面試題考察的就是經過length屬性獲取數組長度與經過size()方法獲取集合長度的區別。
全部的數組都有一個固定的成員,能夠經過它來獲取數組的長度,這即是length屬性。在使用的過程當中咱們須要注意的是數組的下標是從0開始計算的。所以,咱們在遍歷或修改數組的時候,須要注意數組的下標最大值是length-1,不然,會出現數組越界的問題。
針對數組,Java標準類庫裏特地提供了Arrays類,咱們能夠經過該類提供的方法進行數組的處理。
可經過Arrays.toString()方法對數組的內容進行打印。下面經過示例咱們來對比一下經過toString方法和直接打印的區別。
String[] strings = {"a","b","c"}; System.out.println(strings); System.out.println(Arrays.toString(strings));
打印結果:
[Ljava.lang.String;@36baf30c [a, e, c]
能夠看到,若是直接打印則打印出來的是strings數組的引用,而並非真實的內容。
可經過Arrays.sort()方法對數組進行排序,但對於數組中的元素有必定的要求,要實現Comparable接口。看下面的實例:
String[] sorts = {"c","b","a"}; Arrays.sort(sorts); System.out.println(Arrays.toString(sorts));
打印結果:
[a, b, c]
很明顯已經進行正常排序了。爲何String能夠直接進行排序?那是由於String已經實現了Comparable接口。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
另外,對於數組的排序還有常見的:冒泡排序、快速排序、選擇排序、插入排序、希爾(Shell)排序、堆排序等。面試過程當中的排序每每也是基於數組來進行展開的。感興趣的朋友可拿數組來練習一下排序的算法。
經過Arrays.asList()方法,可將數組轉化爲列表。
String[] sorts = {"程序","新","視界"}; List<String> list = Arrays.asList(sorts); System.out.println(list);
打印結果:
[程序, 新, 視界]
關於asList的源碼以下:
public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
看到asList源碼,你能想到什麼?是否是發現該方法的參數爲可變參數,而且支持數組做爲參數傳入。關於可變參數,下篇文章咱們會詳細講一下,別忘記關注公衆號「程序新視界」學習。
固然,這裏也能夠轉化爲Set集合,但需建立一個Set的實現類(這裏用HashSet),將asList的結果做爲參數傳入:
Set<String> sets = new HashSet<>(Arrays.asList(sorts));
能夠經過Arrays.binarySearch()方法來對數據中的元素進行查找,顧名思義,這裏是經過二分查找法進行查找的。
String[] sorts = {"c","a","b"}; Arrays.sort(sorts); int index = Arrays.binarySearch(sorts,"b"); System.out.println(index); System.out.println(sorts[index]);
打印結果:
1 b
結果中的"1"指的是字符串所在的下標值,經過下標能夠得到對應位置的值。這裏須要注意的是,既然是二分查找法,那麼在查找以前一定須要進行排序,否則二分查找的意義便不存在了。
能夠經過Arrays.copyOf()方法對數組進行復制,其中第一個參數是被複制數組,第二個參數爲新數組的長度,返回的結果爲新的數組。示例以下:
int[] sourceArray = {1, 3, 5, 7, 0}; int[] newArray = Arrays.copyOf(sourceArray, sourceArray.length); System.out.println(Arrays.toString(newArray));
打印結果:
[1, 3, 5, 7, 0]
此時,須要思考一個問題Arrays.copyOf()複製的功能是一個什麼層次的複製。也就說,若是修改新數組的值,是否會影響到原有數組。
先猜想一下,下面看示例代碼:
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));
打印結果:
[1, 8, 5, 7, 0] [1, 3, 5, 7, 0]
結果能說明什麼?說明Arrays.copyOf()的複製功能是建立一個全新的數組及數組元素嗎?NO,NO,NO!
咱們再來看另一個示例,先建立一個User對象,源碼以下:
public class User { private String userNo; public User(String userNo){ this.userNo = userNo; } public String getUserNo() { return userNo; } public void setUserNo(String userNo) { this.userNo = userNo; } }
而後建立數組進行復制操做,複製完成以後對新數組的數據進行修改。
User[] sourceArray = {new User("N1"), new User("N2"),new User("N3")}; User[] newArray = Arrays.copyOf(sourceArray, sourceArray.length); newArray[1].setUserNo("N4"); System.out.println(newArray[1].getUserNo()); System.out.println(sourceArray[1].getUserNo());
打印結果以下:
N4 N4
咱們在代碼中只是修改了新數組中的User的屬性,結果原有數組的值也一樣被修改了。
上面的兩個示例說明數組的copy操做只是一個淺拷貝。這與序列化的淺拷貝徹底相同:基本類型是直接拷貝值,其餘都是拷貝引用地址。
一樣,數組和集合的clone也是如此,一樣是淺拷貝,使用時需多加留意。
關於List是如何實現變長的,你們能夠參考List的源碼進行學習。這裏基於上面提到的Arrays.copyOf()方法的功能來實現動態變長。
實現原理很簡單,就是基於Arrays.copyOf()方法的第二個參數來進行擴容。
相關方法以下:
public static <T> T[] expandCapacity(T[] datas, int newLen) { // 校驗長度值,若是小於0,則爲0 newLen = Math.max(newLen, 0); // 生成一個新數組,並拷貝原值,指定新的數組長度 return Arrays.copyOf(datas, newLen); }
在上述方法中除了校驗部分,核心機制即是利用了Arrays.copyOf()方法來實現一個可變長的數組。
關於數組部分,咱們就講這麼多,其實數組還有多維數組以及經過Arrays.asList()方法轉換爲List以後基於List的更多操做,在這裏咱們就不進行拓展了。感興趣的朋友可自行實踐。