本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
數組是存儲多個同類型元素的基本數據結構,數組中的元素在內存連續存放,能夠經過數組下標直接定位任意元素,相比咱們在後續章節介紹的其餘容器,效率很是高。java
數組操做是計算機程序中的常見基本操做,Java中有一個類Arrays,包含一些對數組操做的靜態方法,本節主要就來討論這些方法,咱們先來看怎麼用,而後再來看它們的實現原理。學習Arrays的用法,咱們就能夠避免從新發明輪子,直接使用,學習它的實現原理,咱們就能夠在須要的時候,本身實現它不具有的功能。算法
Arrays的toString方法能夠方便的輸出一個數組的字符串形式,方便查看,它有九個重載的方法,包括八種基本類型數組和一個對象類型數組,這裏列舉兩個:apache
public static String toString(int[] a) public static String toString(Object[] a) 複製代碼
例如:編程
int[] arr = {9,8,3,4};
System.out.println(Arrays.toString(arr));
String[] strArr = {"hello", "world"};
System.out.println(Arrays.toString(strArr));
複製代碼
輸出爲:設計模式
[9, 8, 3, 4]
[hello, world]
複製代碼
若是不使用Arrays.toString,直接輸出數組自身,即代碼改成:數組
int[] arr = {9,8,3,4};
System.out.println(arr);
String[] strArr = {"hello", "world"};
System.out.println(strArr);
複製代碼
則輸出會變爲以下所示:bash
[I@1224b90
[Ljava.lang.String;@728edb84
複製代碼
這個輸出就難以閱讀了,@後面的數字表示的是內存的地址。微信
排序是一個很是常見的操做,同toString同樣,對每種基本類型的數組,Arrays都有sort方法(boolean除外),如:數據結構
public static void sort(int[] a) public static void sort(double[] a) 複製代碼
排序按照從小到大升序排,看個例子:
int[] arr = {4, 9, 3, 6, 10};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
複製代碼
輸出爲:
[3, 4, 6, 9, 10]
複製代碼
數組已經排好序了。
sort還能夠接受兩個參數,對指定範圍內的元素進行排序,如:
public static void sort(int[] a, int fromIndex, int toIndex) 複製代碼
包括fromIndex位置的元素,不包括toIndex位置的元素,如:
int[] arr = {4, 9, 3, 6, 10};
Arrays.sort(arr,0,3);
System.out.println(Arrays.toString(arr));
複製代碼
輸出爲:
[3, 4, 9, 6, 10]
複製代碼
只對前三個元素排序。
除了基本類型,sort還能夠直接接受對象類型,但對象須要實現Comparable接口。
public static void sort(Object[] a) public static void sort(Object[] a, int fromIndex, int toIndex) 複製代碼
咱們看個String數組的例子:
String[] arr = {"hello","world", "Break","abc"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
複製代碼
輸出爲:
[Break, abc, hello, world]
複製代碼
"Break"之因此排在最前面,是由於大寫字母比小寫字母都小。那若是排序的時候但願忽略大小寫呢?
sort還有另外兩個重載方法,能夠接受一個比較器做爲參數:(若是用掘金app看,可能會有亂碼,是掘金bug,能夠經過掘金PC版查看,或者關注個人微信公衆號"老馬說編程"回覆31查看)
public static <T> void sort(T[] a, Comparator<? super T> c) public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c) 複製代碼
方法聲明中的T表示泛型,泛型咱們在後續章節再介紹,這裏表示的是,這個方法能夠支持全部對象類型,只要傳遞這個類型對應的比較器就能夠了。Comparator就是比較器,它是一個接口,定義是:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
複製代碼
最主要的是compare這個方法,它比較兩個對象,返回一個表示比較結果的值,-1表示o1小於o2,0表示相等,1表示o1大於o2。
排序是經過比較來實現的,sort方法在排序的過程當中,須要對對象進行比較的時候,就調用比較器的compare方法。
String類有一個public靜態成員,表示忽略大小寫的比較器:
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
複製代碼
咱們經過這個比較器再來對上面的String數組排序:
String[] arr = {"hello","world", "Break","abc"};
Arrays.sort(arr, String.CASE_INSENSITIVE_ORDER);
System.out.println(Arrays.toString(arr));
複製代碼
這樣,大小寫就忽略了,輸出變爲了:
[abc, Break, hello, world]
複製代碼
爲進一步理解Comparator,咱們來看下String的這個比較器的主要實現代碼:
private static class CaseInsensitiveComparator implements Comparator<String> {
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
}
複製代碼
代碼比較直接,就不解釋了。
sort默認都是從小到大排序,若是但願按照從大到小排呢?對於對象類型,能夠指定一個不一樣的Comparator,能夠用匿名內部類來實現Comparator,好比能夠這樣:
String[] arr = {"hello","world", "Break","abc"};
Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareToIgnoreCase(o1);
}
});
System.out.println(Arrays.toString(arr));
複製代碼
程序輸出爲:
[world, hello, Break, abc]
複製代碼
以上代碼使用一個匿名內部類實現Comparator接口,返回o2與o1進行忽略大小寫比較的結果,這樣就能實現,忽略大小寫,且按從大到小排序。爲何o2與o1比就逆序了呢?由於默認狀況下,是o1與o2比。
Collections類中有兩個靜態方法,能夠返回逆序的Comparator,如
public static <T> Comparator<T> reverseOrder() public static <T> Comparator<T> reverseOrder(Comparator<T> cmp) 複製代碼
關於Collections類,咱們在後續章節再詳細介紹。
這樣,上面字符串忽略大小寫逆序排序的代碼能夠改成:
String[] arr = {"hello","world", "Break","abc"};
Arrays.sort(arr, Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
System.out.println(Arrays.toString(arr));
複製代碼
傳遞比較器Comparator給sort方法,體現了程序設計中一種重要的思惟方式,將不變和變化相分離,排序的基本步驟和算法是不變的,但按什麼排序是變化的,sort方法將不變的算法設計爲主體邏輯,而將變化的排序方式設計爲參數,容許調用者動態指定,這也是一種常見的設計模式,它有一個名字,叫策略模式,不一樣的排序方式就是不一樣的策略。
Arrays包含不少與sort對應的查找方法,能夠在已排序的數組中進行二分查找,所謂二分查找就是從中間開始找,若是小於中間元素,則在前半部分找,不然在後半部分找,每比較一次,要麼找到,要麼將查找範圍縮小一半,因此查找效率很是高。
二分查找既能夠針對基本類型數組,也能夠針對對象數組,對對象數組,也能夠傳遞Comparator,也均可以指定查找範圍,以下所示:
針對int數組
public static int binarySearch(int[] a, int key) public static int binarySearch(int[] a, int fromIndex, int toIndex, int key) 複製代碼
針對對象數組
public static int binarySearch(Object[] a, Object key) 複製代碼
自定義比較器
public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c) 複製代碼
若是能找到,binarySearch返回找到的元素索引,好比說:
int[] arr = {3,5,7,13,21};
System.out.println(Arrays.binarySearch(arr, 13));
複製代碼
輸出爲3。若是沒找到,返回一個負數,這個負數等於:-(插入點+1),插入點表示,若是在這個位置插入沒找到的元素,能夠保持原數組有序,好比說:
int[] arr = {3,5,7,13,21};
System.out.println(Arrays.binarySearch(arr, 11));
複製代碼
輸出爲-4,表示插入點爲3,若是在3這個索引位置處插入11,能夠保持數組有序,即數組會變爲:{3,5,7,11,13,21}
須要注意的是,binarySearch針對的必須是已排序數組,若是指定了Comparator,須要和排序時指定的Comparator保持一致,另外,若是數組中有多個匹配的元素,則返回哪個是不肯定的。
與toString同樣,也有多種重載形式,如:
public static long[] copyOf(long[] original, int newLength)
public static <T> T[] copyOf(T[] original, int newLength)
複製代碼
後面那個是泛型用法,這裏表示的是,這個方法能夠支持全部對象類型,參數是什麼數組類型,返回結果就是什麼數組類型。
newLength表示新數組的長度,若是大於原數組,則後面的元素值設爲默認值。回顧一下默認值,對於數值類型,值爲0,對於boolean,值爲false,對於char,值爲'\0',對於對象,值爲null。
來看個例子:
String[] arr = {"hello", "world"};
String[] newArr = Arrays.copyOf(arr, 3);
System.out.println(Arrays.toString(newArr));
複製代碼
輸出爲:
[hello, world, null]
複製代碼
除了copyOf方法,Arrays中還有copyOfRange方法,以支持拷貝指定範圍的元素,如:
public static int[] copyOfRange(int[] original, int from, int to)
複製代碼
from表示要拷貝的第一個元素的索引,新數組的長度爲to-from,to能夠大於原數組的長度,若是大於,與copyOf相似,多出的位置設爲默認值。
來看個例子:
int[] arr = {0,1,3,5,7,13,19};
int[] subArr1 = Arrays.copyOfRange(arr,2,5);
int[] subArr2 = Arrays.copyOfRange(arr,5,10);
System.out.println(Arrays.toString(subArr1));
System.out.println(Arrays.toString(subArr2));
複製代碼
輸出爲:
[3, 5, 7]
[13, 19, 0, 0, 0]
複製代碼
subArr1是正常的子數組,subArr2拷貝時to大於原數組長度,後面的值設爲了0。
支持基本類型和對象類型,以下所示:
public static boolean equals(boolean[] a, boolean[] a2) public static boolean equals(double[] a, double[] a2) public static boolean equals(Object[] a, Object[] a2) 複製代碼
只有數組長度相同,且每一個元素都相同,才返回true,不然返回false。對於對象,相同是指equals返回true。
Arrays包含不少fill方法,能夠給數組中的每一個元素設置一個相同的值:
public static void fill(int[] a, int val) 複製代碼
也能夠給數組中一個給定範圍的每一個元素設置一個相同的值:
public static void fill(int[] a, int fromIndex, int toIndex, int val) 複製代碼
好比說:
int[] arr = {3,5,7,13,21};
Arrays.fill(arr,2,4,0);
System.out.println(Arrays.toString(arr));
複製代碼
將索引從2(含2)到4(不含4)的元素設置爲0,輸出爲:
[3, 5, 0, 0, 21]
複製代碼
針對數組,計算一個數組的哈希值:
public static int hashCode(int a[]) 複製代碼
計算hashCode的算法和String是相似的,咱們看下代碼:
public static int hashCode(int a[]) {
if (a == null)
return 0;
int result = 1;
for (int element : a)
result = 31 * result + element;
return result;
}
複製代碼
回顧一下,String計算hashCode的算法也是相似的,數組中的每一個元素都影響hash值,位置不一樣,影響也不一樣,使用31一方面產生的哈希值更分散,另外一方面計算效率也比較高。
以前咱們介紹的數組都是一維的,數組還能夠是多維的,先來看二維數組,好比:
int[][] arr = new int[2][3];
for(int i=0;i<arr.length;i++){
for(int j=0;j<arr[i].length;j++){
arr[i][j] = i+j;
}
}
複製代碼
arr就是一個二維數組,第一維長度爲2,第二維長度爲3,相似於一個長方形矩陣,或者相似於一個表格,第一維表示行,第二維表示列。arr[i]表示第i行,它自己仍是一個數組,arr[i][j]表示第i行中的第j個元素。
除了二維,數組還能夠是三維、四維等,但通常而言,不多用到三維以上的數組,有幾維,就有幾個[],好比說,一個三維數組的聲明爲:
int[][][] arr = new int[10][10][10];
複製代碼
在建立數組時,除了第一維的長度須要指定外,其餘維的長度不須要指定,甚至,第一維中,每一個元素的第二維的長度能夠不同,看個例子:
int[][] arr = new int[2][];
arr[0] = new int[3];
arr[1] = new int[5];
複製代碼
arr是一個二維數組,第一維的長度爲2,第一個元素的第二維長度爲3,而第二個爲5。
多維數組究竟是什麼呢?其實,能夠認爲,多維數組只是一個假象,只有一維數組,只是數組中的每一個元素還能夠是一個數組,這樣就造成二維數組,若是其中每一個元素還都是一個數組,那就是三維數組。
Arrays中的toString,equals,hashCode都有對應的針對多維數組的方法:
public static String deepToString(Object[] a) public static boolean deepEquals(Object[] a1, Object[] a2) public static int deepHashCode(Object a[]) 複製代碼
這些deepXXX方法,都會判斷參數中的元素是否也爲數組,若是是,會遞歸進行操做。
看個例子:
int[][] arr = new int[][]{
{0,1},
{2,3,4},
{5,6,7,8}
};
System.out.println(Arrays.deepToString(arr));
複製代碼
輸出爲:
[[0, 1], [2, 3, 4], [5, 6, 7, 8]]
複製代碼
下面,咱們來看以上方法的實現原理。
hashCode的實現咱們已經介紹了,fill和equals的實現都很簡單,循環操做而已,就不贅述了。
toString的實現也很簡單,利用了StringBuilder,咱們列下代碼,但不作解釋了。
public static String toString(int[] a) {
if (a == null)
return "null";
int iMax = a.length - 1;
if (iMax == -1)
return "[]";
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(a[i]);
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
複製代碼
copyOf和copyOfRange利用了 System.arraycopy,邏輯也很簡單,咱們也只是簡單列下代碼:
public static int[] copyOfRange(int[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
int[] copy = new int[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
複製代碼
二分查找binarySearch的代碼也比較直接,主要代碼以下:
private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex, T key, Comparator<? super T> c) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
T midVal = a[mid];
int cmp = c.compare(midVal, key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
複製代碼
有兩個標誌low和high,表示查找範圍,在while循環中,與中間值進行對比,大於則在後半部分找(提升low),不然在前半部分找(下降high)。
最後,咱們來看排序方法sort,與前面這些簡單直接的方法相比,sort要複雜的多,排序是計算機程序中一個很是重要的方面,幾十年來,計算機科學家和工程師們對此進行了大量的研究,設計實現了各類各樣的算法和實現,進行了大量的優化。通常而言,沒有一個所謂最好的算法,不一樣算法每每有不一樣的適用場合。
那Arrays的sort是如何實現的呢?
對於基本類型的數組,Java採用的算法是雙樞軸快速排序(Dual-Pivot Quicksort),這個算法是Java 1.7引入的,在此以前,Java採用的算法是普通的快速排序,雙樞軸快速排序是對快速排序的優化,新算法的實現代碼位於類java.util.DualPivotQuicksort中。
對於對象類型,Java採用的算法是TimSort, TimSort也是在Java 1.7引入的,在此以前,Java採用的是歸併排序,TimSort其實是對歸併排序的一系列優化,TimSort的實現代碼位於類java.util.TimSort中。
在這些排序算法中,若是數組長度比較小,它們還會採用效率更高的插入排序。
爲何基本類型和對象類型的算法不同呢?排序算法有一個穩定性的概念,所謂穩定性就是對值相同的元素,若是排序前和排序後,算法能夠保證它們的相對順序不變,那算法就是穩定的,不然就是不穩定的。
快速排序更快,但不穩定,而歸併排序是穩定的。對於基本類型,值相同就是徹底相同,因此穩定不穩定沒有關係。但對於對象類型,相同只是比較結果同樣,它們仍是不一樣的對象,其餘實例變量也不見得同樣,穩定不穩定可能就頗有關係了,因此採用歸併排序。
這些算法的實現是比較複雜的,所幸的是,Java給咱們提供了很好的實現,絕大多數狀況下,咱們會用就能夠了。
其實,Arrays中包含的數組方法是比較少的,不少經常使用的操做沒有,好比,Arrays的binarySearch只能針對已排序數組進行查找,那沒有排序的數組怎麼方便查找呢?
Apache有一個開源包(http://commons.apache.org/proper/commons-lang/),裏面有一個類ArrayUtils (位於包org.apache.commons.lang3),裏面實現了更多的經常使用數組操做,這裏列舉一些,與Arrays相似,每一個操做都有不少重載方法,咱們只列舉一個。
public static void reverse(final int[] array) 複製代碼
對於基本類型數組,Arrays的sort只能從小到大排,若是但願從大到小,能夠在排序後,使用reverse進行翻轉。
//從頭日後找
public static int indexOf(final int[] array, final int valueToFind) //從尾部往前找 public static int lastIndexOf(final int[] array, final int valueToFind) //檢查是否包含元素 public static boolean contains(final int[] array, final int valueToFind) 複製代碼
由於數組長度是固定的,刪除是經過建立新數組,而後拷貝除刪除元素外的其餘元素來實現的。
//刪除指定位置的元素
public static int[] remove(final int[] array, final int index)
//刪除多個指定位置的元素
public static int[] removeAll(final int[] array, final int... indices)
//刪除值爲element的元素,只刪除第一個
public static boolean[] removeElement(final boolean[] array, final boolean element)
複製代碼
同刪除同樣,由於數組長度是固定的,添加是經過建立新數組,而後拷貝原數組內容和新元素來實現的。
//添加一個元素
public static int[] add(final int[] array, final int element)
//在指定位置添加一個元素
public static int[] add(final int[] array, final int index, final int element)
//合併兩個數組
public static int[] addAll(final int[] array1, final int... array2)
複製代碼
判斷數組是不是已排序的
public static boolean isSorted(int[] array) 複製代碼
本節咱們分析了Arrays類,介紹了其用法,以及基本實現原理,同時,咱們介紹了多維數組以及Apache中的ArrayUtils類。對於帶Comparator參數的排序方法,咱們提到,這是一種思惟和設計模式,值得學習。
數組是計算機程序中的基本數據結構,Arrays類以及ArrayUtils類封裝了關於數組的常見操做,使用這些方法吧!
下一節,咱們來看計算機程序中,另外一種常見的操做,就是對日期的操做。
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。