java中的Arrays這個(gè)工具類你真的會(huì)用嗎(一文秒懂)
Java源碼系列三-工具類Arrays
今天分享java的源碼的第三彈,Arrays這個(gè)工具類的源碼。因?yàn)榻谠趶?fù)習(xí)數(shù)據(jù)結(jié)構(gòu),了解到Arrays里面的排序算法和二分查找等的實(shí)現(xiàn),收益匪淺,決定研讀一下Arrays這個(gè)類的源碼。不足之處,歡迎在評(píng)論區(qū)交流和指正。
1.認(rèn)識(shí)Arrays這個(gè)類:
首先它在java的utils包下,屬于Java Collections Framework中的一員。它的初衷就是一個(gè)工具類,封裝了操縱數(shù)組的各種方法,比如排序,二分查找,數(shù)組的拷貝等等。滿足了我們?nèi)粘?duì)數(shù)組操做的基本需求,了解它的底層實(shí)現(xiàn),不僅能幫助我們更好的使用它,而且還能培養(yǎng)我們更好的代碼的思維。
2.構(gòu)造方法
因?yàn)槭且粋€(gè)工具類,所以它的構(gòu)造方法定義為私有的,且所有的實(shí)現(xiàn)方法都是靜態(tài)方法。也就是說(shuō)這個(gè)類不能被實(shí)例化,通俗的講,就是不能new。只能通過類名來(lái)直接調(diào)用方法(反射除外)。這樣做的目的是強(qiáng)化該類不可實(shí)列化的能力,突出該類作為工具類的根本職能。源碼如下:
// Suppresses default constructor, ensuring non-instantiability.
private Arrays()
{
}
3.常用方法的解析
3.1快速插入集合元素的方法asList(T... a):
基本使用:
/**
* 數(shù)組轉(zhuǎn)化為集合
*/
@Test
public void toArrayTest(){
List<Integer> list = Arrays.asList(2,4,5,6,6);
for (Integer integer : list) {
System.out.print(integer+" ");
}
}
輸出結(jié)果:
2 4 5 6 6
看一下源碼:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
// ArrayList的構(gòu)造方法和屬性
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
這個(gè)方法的實(shí)現(xiàn)比較簡(jiǎn)單,就是調(diào)用ArrayList的構(gòu)造方法,并且參數(shù)是一個(gè)數(shù)組,也就是將我們要構(gòu)造的數(shù)傳入到ArrayList的構(gòu)造方法中去,進(jìn)行實(shí)例化。
3.2.二分查找的方法
Arrays類中的二分查找八種基本類型都有涉及,但都是方法的重載。其實(shí)現(xiàn)原理都是一樣,這里以int類型為例,進(jìn)行說(shuō)明。
基本使用:
@Test
public void binarySearchTest(){
int[] arrays = {1,4,6,7,9,3};
// 查找元素為7的下標(biāo)值
int result = Arrays.binarySearch(arrays,7);
System.out.println(result);
}
結(jié)果:
3
這個(gè)方法主要涉及的一下三個(gè)方法:
// 我們常用的方法
public static int binarySearch(int[] a, int key) {
return binarySearch0(a, 0, a.length, key);
}
/*
參數(shù)說(shuō)明如下: a 待查找的數(shù)組
fromIndex 查找的開始位置
toIndex 查找的結(jié)束位置
key 查找的目標(biāo)值
*/
public static int binarySearch(int[] a, int fromIndex, int toIndex,
int key) {
// 進(jìn)行異常檢查
rangeCheck(a.length, fromIndex, toIndex);
return binarySearch0(a, fromIndex, toIndex, key);
}
// Like public version, but without range checks.
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
// 找出查找范圍的中間值
int mid = (low + high) >>> 1;
int midVal = a[mid];
// 進(jìn)行比較
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
當(dāng)然實(shí)現(xiàn)的核心方法還是上述私有方法binarySearch0()這個(gè)方法,實(shí)現(xiàn)的邏輯也不復(fù)雜。
第一步就是聲明兩個(gè)變量存儲(chǔ)查找區(qū)域的開始和結(jié)束。
第二步 循環(huán),比較,不斷的縮小比較的范圍,直到找到數(shù)組中的值和目標(biāo)值相同,返回下標(biāo),如果沒有找到就返回一個(gè)負(fù)數(shù)也就是下面的這l兩行代碼:
return mid; // key found return -(low + 1); // key not found.
我認(rèn)為:這個(gè)二分法實(shí)現(xiàn)的亮點(diǎn)就在于求中間值的移位運(yùn)算:
int mid = (low + high) >>> 1;
有人就納悶了,為什么還要使用移位運(yùn)算,除法不行嗎?主要還是為了性能考量。因?yàn)橐莆贿\(yùn)算占兩個(gè)機(jī)器周期,而乘除法占四個(gè)運(yùn)算周期,所以移位運(yùn)算的速度肯定比乘除法的運(yùn)算速度快很多,計(jì)算量小了可能區(qū)別不大,但是計(jì)算量很大,就區(qū)別很明顯了。
3.3 數(shù)組的拷貝
@Test
public void testCopyArrange(){
// 原數(shù)組
int [] srcArray = {11,2,244,5,6,54};
// 拷貝原數(shù)組長(zhǎng)度為3的部分
int[] descArray = Arrays.copyOf(srcArray,3);
System.out.println(Arrays.toString(descArray));
}
輸出結(jié)果:
[11, 2, 244]
源碼分析:
/* 參數(shù)說(shuō)明:
original 原數(shù)組
newLength 拷貝的數(shù)組長(zhǎng)度
*/
public static int[] copyOf(int[] original, int newLength) {
// 聲明一個(gè)新數(shù)組的長(zhǎng)度,存儲(chǔ)拷貝后的數(shù)組
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
分析: 主要還是調(diào)用了本地的方法arraycopy完成數(shù)組的指定長(zhǎng)度拷貝,可以看到源碼并沒有對(duì)數(shù)組的長(zhǎng)度進(jìn)行檢查,主要是arraycopy()這個(gè)方法時(shí)使了Math.min()方法,保證了你聲明的長(zhǎng)度在一個(gè)安全的范圍之內(nèi),如果你拷貝的長(zhǎng)度超出了數(shù)組的長(zhǎng)度,就默認(rèn)拷貝整個(gè)數(shù)組。至于native修飾的方法的使用,可以看看這里。
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
當(dāng)然如果需要拷貝數(shù)組指定的區(qū)間 ,可以使用Arrays的copyOfRange(int[] original, int from, int to) 實(shí)現(xiàn)原理和arraycopy()方法的原理類似:
@Test
public void testCopy(){
int [] srcArray = {11,2,244,5,6,54};
// 拷貝指定范圍的數(shù)組
int[] descArray = Arrays.copyOfRange(srcArray,0,3);
System.out.println(Arrays.toString(descArray));
}
輸出結(jié)果:
[11, 2, 244]
注: copyOfRange(int[] original, int from, int to)中的參數(shù)to是不包含在拷貝的結(jié)果中的,上述的例子,就只能拷貝到索引為2的元素,不包含索引為3的元素,這點(diǎn)需要注意。
3.4 equals方法
主要重寫了Object類的equals方法,用來(lái)比較兩個(gè)數(shù)組內(nèi)容是否相等,也就是他們中的元素是否相等。
基本用法:
@Test
public void equalTest(){
int[] array ={1,2,3,4};
int[] result ={1,2,3,4};
System.out.println(Arrays.equals(array,result));
System.out.println(array == result);
}
結(jié)果:
true
false
看源碼之前,有必要講一下重寫了equals方法之后,兩個(gè)對(duì)象比較的是值,也就是他們的內(nèi)容,這點(diǎn)非常的重要。重寫equals方法的注意事項(xiàng)可以移步這里。
源碼如下:
public static boolean equals(int[] a, int[] a2) {
// 基于地址的比較
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
// 基于長(zhǎng)度的比較
if (a2.length != length)
return false;
// 比較每個(gè)元素是否相等
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
}
源碼說(shuō)明如下:
源碼判斷了四次,分別是首地址比較,是否為空,以及長(zhǎng)度的比較,最后對(duì)于數(shù)組的各個(gè)元素進(jìn)行比較。
有必要說(shuō)明下第一個(gè)判斷,也就是首地址的比較。當(dāng)我們聲明一個(gè)數(shù)組變量時(shí),這個(gè)變量就代表數(shù)組的首地址,看下面這個(gè)代碼:
@Test
public void equalTest(){
int[] array ={1,2,3,4};
System.out.println(array);
}
結(jié)果:
[I@4f2410ac // [代表數(shù)組 I代表整數(shù) @分隔符 后邊內(nèi)存地址十六進(jìn)制
這表示的是一個(gè)地址。還是因?yàn)樵诼暶饕粋€(gè)數(shù)組時(shí),會(huì)在堆里面創(chuàng)建一塊內(nèi)存區(qū)域,但是這塊內(nèi)存區(qū)域相對(duì)于堆來(lái)說(shuō)可能很小,不好找。為了方便查找,所以將數(shù)組內(nèi)存中的首地址表示出來(lái)。虛擬機(jī)將地址傳給變量名array。這也是引用類型,傳的是地址,也就是理解成array指向內(nèi)存地址(類似于家庭的地址),每次運(yùn)行可能地址都不一樣,因?yàn)樘摂M機(jī)開辟的內(nèi)存空間可能不一樣。
理解了這個(gè),那么a==a2就好理解了,如果兩個(gè)數(shù)組內(nèi)存地址都相同,那么兩個(gè)數(shù)組的肯定是相等的。
還有我認(rèn)為程序?qū)懙谋容^好的地方就是源碼中對(duì)數(shù)組每個(gè)元素的比較,也就是下面這段代碼;
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
使用a[i] != a2[i] 作為判斷條件,就可以減少比較次數(shù),提高了性能。試想一下如果這里是相等的比較,那每次都要遍歷整個(gè)數(shù)組,如果數(shù)據(jù)量大了,無(wú)疑在性能上會(huì)慢很多。又一次感嘆到源碼的魅力。
3.5 排序相關(guān)的方法sort()和parallelSort()
Arrays 這個(gè)類中主要涉及了兩種類型的排序方法串行 sort()和并行parallelSort()這兩個(gè)方法,當(dāng)然對(duì)象的排序和基本類型的排序也不太一樣。這里還是以int[]類型的為例。進(jìn)行說(shuō)明。
首先比較兩個(gè)方法的性能:
public final int UPPER_LIMIT = 0xffffff;
final int ROUNDS = 10;
final int INCREMENT = 5;
final int INIT_SIZE = 1000;
@Test
public void sortAndParallelSortTest(){
// 構(gòu)造不同容量的集合
for (int capacity = INIT_SIZE; capacity < UPPER_LIMIT ; capacity*= INCREMENT) {
ArrayList<Integer> list = new ArrayList<>(capacity);
for (int j = 0; j < capacity; j++) {
list.add((int) (Math.random()*capacity));
}
double avgTimeOfParallelSort = 0;
double avgTimeOfSort = 0;
for (int j = 0; j <= ROUNDS ; j++) {
// 每次排序都打亂順序
Collections.shuffle(list);
Integer[] arr1 = list.toArray(new Integer[capacity]);
Integer[] arr2 = arr1.clone();
avgTimeOfParallelSort += counter(arr1,true);
avgTimeOfSort += counter(arr2, false);
}
// 輸出結(jié)果
output(capacity,avgTimeOfParallelSort/ROUNDS,avgTimeOfSort/ROUNDS);
}
}
private void output(int capacity, double v, double v1) {
System.out.println("=======================測(cè)試排序的時(shí)間=========");
System.out.println("Capacity"+capacity);
System.out.println("ParallelSort"+v);
System.out.println("Sort"+v1);
System.out.println("比較快的排序是:"+(v < v1 ? "ParallelSort":"Sort"));
}
// 計(jì)算消耗的時(shí)間
private double counter(Integer[] arr1, boolean b) {
long begin,end;
begin = System.nanoTime();
if(b){
Arrays.parallelSort(arr1);
}else{
Arrays.parallelSort(arr1);
}
end = System.nanoTime();
return BigDecimal.valueOf(end-begin,9).doubleValue();
}
部分的測(cè)試的結(jié)果:
=======================測(cè)試排序的時(shí)間=========
Capacity1000
ParallelSort6.284099999999999E-4
Sort5.599599999999999E-4
比較快的排序是:Sort
=======================測(cè)試排序的時(shí)間=========
Capacity5000
ParallelSort0.00163599
Sort0.0018313699999999995
比較快的排序是:ParallelSort
可以看到在數(shù)據(jù)量比較小的情況下,使用sort()方法更快,一旦過了一個(gè)閾值,就是ParallelSort()這個(gè)方法性能好。這個(gè)閾值是多少呢。
我們先看一下parallelSort的源碼:
public static void parallelSort(int[] a) {
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);
else
new ArraysParallelSortHelpers.FJInt.Sorter
(null, a, new int[n], 0, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g).invoke();
}
可以看到當(dāng)數(shù)組的長(zhǎng)度小于MIN_ARRAY_SORT_GRAN或者p = ForkJoinPool.getCommonPoolParallelism()) == 1 (在單線程下)的時(shí)候,調(diào)用sort()排序的底層實(shí)現(xiàn)的DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);Arrays的開頭定義的常量如下:
private static final int MIN_ARRAY_SORT_GRAN = 1 << 13; // 這個(gè)值是8192
對(duì)比兩者,也就是在數(shù)組的長(zhǎng)度比較大或者是多線程的情況下,優(yōu)先考慮并行排序,否則使用串行排序。
兩個(gè)排序的核心思想:
- sort()方法的核心還是快排和優(yōu)化后的歸并排序, 快速排序主要是對(duì)哪些基本類型數(shù)據(jù)(int,short,long等)排序, 而合并排序用于對(duì)對(duì)象類型進(jìn)行排序。
- parallelSort()它使用并行排序-合并排序算法。它將數(shù)組分成子數(shù)組,這些子數(shù)組本身先進(jìn)行排序然后合并。
由于并行排序和串行排序的底層比較復(fù)雜,且篇幅有限,想要詳細(xì)了解底層實(shí)現(xiàn)的話,可以移步到串行排序和并行排序
3.6 toString方法
基本用法:
@Test
public void toStringTest(){
int[] array = {1,3,2,5};
System.out.println(Arrays.toString(array));
}
結(jié)果:
[1, 3, 2, 5]
源碼分析如下:
public static String toString(int[] a) {
// 1.判斷數(shù)組的大小
if (a == null)
return "null";
int iMax = a.length - 1;
if (iMax == -1)
return "[]";
// 2.使用StringBuilder進(jìn)行追加
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(a[i]);
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
具體的實(shí)現(xiàn),已在源碼的注釋中進(jìn)行了說(shuō)明。這個(gè)方法對(duì)于基本數(shù)據(jù)類型來(lái)說(shuō),很方便的遍歷數(shù)組。
追本溯源,方能闊步前行。
參考資料
http://www.fzitv.net/article/180770.htm
http://www.fzitv.net/article/116323.htm
javaSE的官方問檔。
到此這篇關(guān)于java中的Arrays這個(gè)工具類你真的會(huì)用嗎(一文秒懂)的文章就介紹到這了,更多相關(guān)java中Arrays工具類內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?cloud?Hystrix注解初始化源碼過程解讀
這篇文章主要為大家介紹了Hystrix初始化部分,我們從源碼的角度分析一下@EnableCircuitBreaker以及@HystrixCommand注解的初始化過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-12-12
Java實(shí)現(xiàn)單鏈表翻轉(zhuǎn)實(shí)例代碼
Java實(shí)現(xiàn)單鏈表反轉(zhuǎn),遞歸和非遞歸兩種形式。接下來(lái)通過本文給大家分享Java實(shí)現(xiàn)單鏈表翻轉(zhuǎn)實(shí)例代碼,需要的的朋友參考下2017-03-03
Reactor 多任務(wù)并發(fā)執(zhí)行且結(jié)果按順序返回第一個(gè)
這篇文章主要介紹了Reactor 多任務(wù)并發(fā)執(zhí)行且結(jié)果按順序返回第一個(gè),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-09-09
SpringBoot中@PathVariable、@RequestParam和@RequestBody的區(qū)別和使用詳解
這篇文章主要介紹了SpringBoot中@PathVariable、@RequestParam和@RequestBody的區(qū)別和使用詳解,@PathVariable 映射 URL 綁定的占位符,通過@RequestMapping注解中的{}占位符來(lái)標(biāo)識(shí)URL中的變量部分,需要的朋友可以參考下2024-01-01
為什么rest接口返回json建議采用下劃線形式,不要用駝峰
為什么rest接口返回json建議采用下劃線形式,不要用駝峰?今天小編就來(lái)為大家說(shuō)明一下原因,還等什么?一起跟隨小編過來(lái)看看吧2020-09-09
詳解使用SSM實(shí)現(xiàn)簡(jiǎn)單工作流系統(tǒng)之實(shí)現(xiàn)篇
這篇文章主要介紹了使用SSM實(shí)現(xiàn)簡(jiǎn)單工作流系統(tǒng)之實(shí)現(xiàn)篇,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-12-12

