首頁>技術>

雖然隨著Java版本的演變,陣列的分量在慢慢減弱,日常使用時大多使用List進行替代。但ArrayList底層依舊採用陣列來進行實現,而陣列依舊有很多應用的場景。在使用陣列的過程中,你是否彙總過陣列的特性及功能,並停下來思考幾個為什麼呢?如果沒有,本篇文章將帶領大家從頭梳理一下陣列,一定會為你帶來一些未曾發掘的特性和功能。

何謂陣列

學習陣列,我們最先要知道的就是它是什麼,能做什麼?

陣列,就是相同型別的物件或基本型別資料的集合。也可以理解為把有限個型別相同的元素按一定順序排列的集合,然後用一個名字命名,用編號區分具體的元素。而這個名字稱為陣列名,編號稱為下標。

陣列在記憶體中是連續儲存的,所以索引速度是非常的快,陣列的賦值與修改元素也很簡單。但是陣列也有不足的地方,那就是如果想在兩個相鄰元素之間插入新的元素會非常麻煩。

另外,陣列宣告的時候必須指定陣列的長度,而陣列的長度是不可變的。在此,陣列長度過長會造成記憶體浪費,長度過短則會造成溢位。

陣列的應用場景

上面提到了陣列的那麼多缺點,但我們知道“存在即合理”,下面看看哪些場景適合陣列的使用。

2、使用(遍歷)時,經常需要按照序號來進行訪問資料元素或做運算的情況。

3、對效能要求較高時,陣列是首選。

也正是由於效能較高,所以我們在閱讀原始碼時經常會看到陣列的身影。特別是針對基礎型別進行操作,效率提升甚至可以達到基於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的原始碼如下:

當然,這裡也可以轉化為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]);

列印結果:

1b

結果中的"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());

列印結果如下:

N4N4

我們在程式碼中只是修改了新陣列中的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的更多操作,在這裡我們就不進行拓展了。感興趣的朋友可自行實踐。

20
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • dotnet 跨平臺構建應用 for linux