java中的Arrays这个工具类你真的会用吗(一文秒懂)

Java源码系列三-工具类Arrays

​ 今天分享java的源码的第三弹,Arrays这个工具类的源码。因为近期在复习数据结构,了解到Arrays里面的排序算法和二分查找等的实现,收益匪浅,决定研读一下Arrays这个类的源码。不足之处,欢迎在评论区交流和指正。

1.认识Arrays这个类:

​ 首先它在java的utils包下,属于Java Collections Framework中的一员。它的初衷就是一个工具类,封装了操纵数组的各种方法,比如排序,二分查找,数组的拷贝等等。满足了我们日常对数组操做的基本需求,了解它的底层实现,不仅能帮助我们更好的使用它,而且还能培养我们更好的代码的思维。

2.构造方法

​ 因为是一个工具类,所以它的构造方法定义为私有的,且所有的实现方法都是静态方法。也就是说这个类不能被实例化,通俗的讲,就是不能new。只能通过类名来直接调用方法(反射除外)。这样做的目的是强化该类不可实列化的能力,突出该类作为工具类的根本职能。源码如下:

 // Suppresses default constructor, ensuring non-instantiability.
 private Arrays() 
{
}

3.常用方法的解析

3.1快速插入集合元素的方法asList(T... a):

基本使用:

 /**
  * 数组转化为集合
  */
 @Test
 public void toArrayTest(){
  List<Integer> list = Arrays.asList(2,4,5,6,6);
  for (Integer integer : list) {
   System.out.print(integer+" ");
  }
 }

输出结果:

2 4 5 6 6

看一下源码:

@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的构造方法中去,进行实例化。

3.2.二分查找的方法

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);
 }

结果:

3

这个方法主要涉及的一下三个方法:

// 我们常用的方法
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;

有人就纳闷了,为什么还要使用移位运算,除法不行吗?主要还是为了性能考量。因为移位运算占两个机器周期,而乘除法占四个运算周期,所以移位运算的速度肯定比乘除法的运算速度快很多,计算量小了可能区别不大,但是计算量很大,就区别很明显了。

3.3 数组的拷贝

 @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的元素,这点需要注意。

3.4 equals方法

主要重写了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] 作为判断条件,就可以减少比较次数,提高了性能。试想一下如果这里是相等的比较,那每次都要遍历整个数组,如果数据量大了,无疑在性能上会慢很多。又一次感叹到源码的魅力。

3.5 排序相关的方法sort()和parallelSort()

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()它使用并行排序-合并排序算法。它将数组分成子数组,这些子数组本身先进行排序然后合并。

由于并行排序和串行排序的底层比较复杂,且篇幅有限,想要详细了解底层实现的话,可以移步到串行排序并行排序

3.6 toString方法

基本用法:

 @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://www.nhooo.com/article/180770.htm
https://www.nhooo.com/article/116323.htm

javaSE的官方问档。

到此这篇关于java中的Arrays这个工具类你真的会用吗(一文秒懂)的文章就介绍到这了,更多相关java中Arrays工具类内容请搜索呐喊教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持呐喊教程!

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#nhooo.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。