今天分享java的源码的第三弹,Arrays这个工具类的源码。因为近期在复习数据结构,了解到Arrays里面的排序算法和二分查找等的实现,收益匪浅,决定研读一下Arrays这个类的源码。不足之处,欢迎在评论区交流和指正。
首先它在java的utils包下,属于Java Collections Framework中的一员。它的初衷就是一个工具类,封装了操纵数组的各种方法,比如排序,二分查找,数组的拷贝等等。满足了我们日常对数组操做的基本需求,了解它的底层实现,不仅能帮助我们更好的使用它,而且还能培养我们更好的代码的思维。
因为是一个工具类,所以它的构造方法定义为私有的,且所有的实现方法都是静态方法。也就是说这个类不能被实例化,通俗的讲,就是不能new。只能通过类名来直接调用方法(反射除外)。这样做的目的是强化该类不可实列化的能力,突出该类作为工具类的根本职能。源码如下:
// Suppresses default constructor, ensuring non-instantiability. private Arrays() {}
基本使用:
/** * 数组转化为集合 */ @Test public void toArrayTest(){ List<Integer> list = Arrays.asList(2,4,5,6,6); for (Integer integer : list) { System.out.print(integer+" "); } }
输出结果:
看一下源码:
@SafeVarargs @SuppressWarnings("varargs") public static <T> List<T> asList(T... a) { return new ArrayList<>(a); } // ArrayList的构造方法和属性 private final E[] a; ArrayList(E[] array) { a = Objects.requireNonNull(array); }
这个方法的实现比较简单,就是调用ArrayList的构造方法,并且参数是一个数组,也就是将我们要构造的数传入到ArrayList的构造方法中去,进行实例化。
Arrays类中的二分查找八种基本类型都有涉及,但都是方法的重载。其实现原理都是一样,这里以int类型为例,进行说明。
基本使用:
@Test public void binarySearchTest(){ int[] arrays = {1,4,6,7,9,3}; // 查找元素为7的下标值 int result = Arrays.binarySearch(arrays,7); System.out.println(result); }
结果:
这个方法主要涉及的一下三个方法:
// 我们常用的方法 public static int binarySearch(int[] a, int key) { return binarySearch0(a, 0, a.length, key); } /* 参数说明如下: a 待查找的数组 fromIndex 查找的开始位置 toIndex 查找的结束位置 key 查找的目标值 */ public static int binarySearch(int[] a, int fromIndex, int toIndex, int key) { // 进行异常检查 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]; // 进行比较 if (midVal < key) low = mid + 1; else if (midVal > key) high = mid - 1; else return mid; // key found } return -(low + 1); // key not found. }
当然实现的核心方法还是上述私有方法 binarySearch0()
这个方法,实现的逻辑也不复杂。
第一步就是声明两个变量存储查找区域的开始和结束。
第二步 循环,比较,不断的缩小比较的范围,直到找到数组中的值和目标值相同,返回下标,如果没有找到就返回一个负数也就是下面的这l两行代码:
return mid; // key found return -(low + 1); // key not found.
我认为:这个二分法实现的亮点就在于求中间值的移位运算:
int mid = (low + high) >>> 1;
有人就纳闷了,为什么还要使用移位运算,除法不行吗?主要还是为了性能考量。因为移位运算占两个机器周期,而乘除法占四个运算周期,所以移位运算的速度肯定比乘除法的运算速度快很多,计算量小了可能区别不大,但是计算量很大,就区别很明显了。
@Test public void testCopyArrange(){ // 原数组 int [] srcArray = {11,2,244,5,6,54}; // 拷贝原数组长度为3的部分 int[] descArray = Arrays.copyOf(srcArray,3); System.out.println(Arrays.toString(descArray)); }
输出结果:
[11, 2, 244]
源码分析:
/* 参数说明: original 原数组 newLength 拷贝的数组长度 */ public static int[] copyOf(int[] original, int newLength) { // 声明一个新数组的长度,存储拷贝后的数组 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);
分析: 主要还是调用了本地的方法arraycopy完成数组的指定长度拷贝,可以看到源码并没有对数组的长度进行检查,主要是 arraycopy()
这个方法时使了 Math.min()
方法,保证了你声明的长度在一个安全的范围之内,如果你拷贝的长度超出了数组的长度,就默认拷贝整个数组。至于native修饰的方法的使用,可以
看看这里
。
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
当然如果需要拷贝数组指定的区间 ,可以使用Arrays的 copyOfRange(int[] original, int from, int to)
实现原理和 arraycopy()
方法的原理类似:
@Test public void testCopy(){ int [] srcArray = {11,2,244,5,6,54}; // 拷贝指定范围的数组 int[] descArray = Arrays.copyOfRange(srcArray,0,3); System.out.println(Arrays.toString(descArray)); }
输出结果:
[11, 2, 244]
注: copyOfRange(int[] original, int from, int to)
中的参数to是不包含在拷贝的结果中的,上述的例子,就只能拷贝到索引为2的元素,不包含索引为3的元素,这点需要注意。
主要重写了Object类的equals方法,用来比较两个数组内容是否相等,也就是他们中的元素是否相等。
基本用法:
@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); }
结果:
true false
看源码之前,有必要讲一下重写了equals方法之后,两个对象比较的是值,也就是他们的内容,这点非常的重要。重写equals方法的注意事项可以移步这里。
源码如下:
public static boolean equals(int[] a, int[] a2) { // 基于地址的比较 if (a==a2) return true; if (a==null || a2==null) return false; int length = a.length; // 基于长度的比较 if (a2.length != length) return false; // 比较每个元素是否相等 for (int i=0; i<length; i++) if (a[i] != a2[i]) return false; return true; }
源码说明如下:
源码判断了四次,分别是首地址比较,是否为空,以及长度的比较,最后对于数组的各个元素进行比较。
有必要说明下第一个判断,也就是首地址的比较。当我们声明一个数组变量时,这个变量就代表数组的首地址,看下面这个代码:
@Test public void equalTest(){ int[] array ={1,2,3,4}; System.out.println(array); }
结果:
[I@4f2410ac // [代表数组 I代表整数 @分隔符 后边内存地址十六进制
这表示的是一个地址。还是因为在声明一个数组时,会在堆里面创建一块内存区域,但是这块内存区域相对于堆来说可能很小,不好找。为了方便查找,所以将数组内存中的首地址表示出来。虚拟机将地址传给变量名array。这也是引用类型,传的是地址,也就是理解成array指向内存地址(类似于家庭的地址),每次运行可能地址都不一样,因为虚拟机开辟的内存空间可能不一样。
理解了这个,那么 a==a2
就好理解了,如果两个数组内存地址都相同,那么两个数组的肯定是相等的。
还有我认为程序写的比较好的地方就是源码中对数组每个元素的比较,也就是下面这段代码;
for (int i=0; i<length; i++) if (a[i] != a2[i]) return false; return true;
使用 a[i] != a2[i]
作为判断条件,就可以减少比较次数,提高了性能。试想一下如果这里是相等的比较,那每次都要遍历整个数组,如果数据量大了,无疑在性能上会慢很多。又一次感叹到源码的魅力。
Arrays
这个类中主要涉及了两种类型的排序方法串行 sort()
和并行 parallelSort()
这两个方法,当然对象的排序和基本类型的排序也不太一样。这里还是以int[]类型的为例。进行说明。
首先比较两个方法的性能:
public final int UPPER_LIMIT = 0xffffff; final int ROUNDS = 10; final int INCREMENT = 5; final int INIT_SIZE = 1000; @Test public void sortAndParallelSortTest(){ // 构造不同容量的集合 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); } // 输出结果 output(capacity,avgTimeOfParallelSort/ROUNDS,avgTimeOfSort/ROUNDS); } } private void output(int capacity, double v, double v1) { System.out.println("=======================测试排序的时间========="); System.out.println("Capacity"+capacity); System.out.println("ParallelSort"+v); System.out.println("Sort"+v1); System.out.println("比较快的排序是:"+(v < v1 ? "ParallelSort":"Sort")); } // 计算消耗的时间 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(); }
部分的测试的结果:
=======================测试排序的时间========= Capacity1000 ParallelSort6.284099999999999E-4 Sort5.599599999999999E-4 比较快的排序是:Sort =======================测试排序的时间========= Capacity5000 ParallelSort0.00163599 Sort0.0018313699999999995 比较快的排序是:ParallelSort
可以看到在数据量比较小的情况下,使用sort()方法更快,一旦过了一个阈值,就是ParallelSort()这个方法性能好。这个阈值是多少呢。
我们先看一下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(); }
可以看到当
数组的长度小于 MIN_ARRAY_SORT_GRAN
或者 p = ForkJoinPool.getCommonPoolParallelism()) == 1
(在单线程下)的时候,调用sort()排序的底层实现的 DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);
Arrays的开头定义的常量如下:
private static final int MIN_ARRAY_SORT_GRAN = 1 << 13; // 这个值是8192
对比两者,也就是在数组的长度比较大或者是多线程的情况下,优先考虑并行排序,否则使用串行排序。
sort()方法的核心还是快排和优化后的归并排序, 快速排序主要是对哪些基本类型数据(int,short,long等)排序, 而合并排序用于对对象类型进行排序。
parallelSort()它使用并行排序-合并排序算法。它将数组分成子数组,这些子数组本身先进行排序然后合并。
由于并行排序和串行排序的底层比较复杂,且篇幅有限,想要详细了解底层实现的话,可以移步到 串行排序 和 并行排序
基本用法:
@Test public void toStringTest(){ int[] array = {1,3,2,5}; System.out.println(Arrays.toString(array)); }
结果:
[1, 3, 2, 5]
源码分析如下:
public static String toString(int[] a) { // 1.判断数组的大小 if (a == null) return "null"; int iMax = a.length - 1; if (iMax == -1) return "[]"; // 2.使用StringBuilder进行追加 StringBuilder b = new StringBuilder(); b.append('['); for (int i = 0; ; i++) { b.append(a[i]); if (i == iMax) return b.append(']').toString(); b.append(", "); } }
具体的实现,已在源码的注释中进行了说明。这个方法对于基本数据类型来说,很方便的遍历数组。
https://blog.csdn.net/ExcellentYuXiao/article/details/52344628
sakuraTears的博客
javaSE的官方问档。