Java數組,這一篇文章就真夠了 何謂數組 數組的應用場景 數組的存儲結構 數組的長度 數組的處理 數組的打印 數組轉集合 數組內容查找 小結

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

何謂數組

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

數組,就是相同類型的對象或基本類型數據的集合。也可以理解爲把有限個類型相同的元素按一定順序排列的集合,然後用一個名字命名,用編號區分具體的元素。而這個名字稱爲數組名,編號稱爲下標。

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

另外,數組聲明的時候必須指定數組的長度,而數組的長度是不可變的。在此,數組長度過長會造成內存浪費,長度過短則會造成溢出。

數組的應用場景

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

1、數據比較少,能夠確定長度;存取或修改操作較多,插入和刪除較少的情況。

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的源碼如下:

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的更多操作,在這裏我們就不進行拓展了。感興趣的朋友可自行實踐。

來源:https://www.tuicool.com/articles/Mja2Ab3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章