Java源码系列三-工具类Arrays

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

1.认识Arrays这个类:

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

2.构造方法

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

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

3.常用方法的解析

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

基本使用:

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

输出结果:

  1. 2 4 5 6 6

看一下源码:

  1. @SafeVarargs
  2. @SuppressWarnings("varargs")
  3. public static <T> List<T> asList(T... a) {
  4. return new ArrayList<>(a);
  5. }
  6. // ArrayList的构造方法和属性
  7. private final E[] a;
  8. ArrayList(E[] array) {
  9. a = Objects.requireNonNull(array);
  10. }

​ 这个方法的实现比较简单,就是调用ArrayList的构造方法,并且参数是一个数组,也就是将我们要构造的数传入到ArrayList的构造方法中去,进行实例化。

3.2.二分查找的方法

Arrays类中的二分查找八种基本类型都有涉及,但都是方法的重载。其实现原理都是一样,这里以int类型为例,进行说明。

基本使用:

  1. @Test
  2. public void binarySearchTest(){
  3. int[] arrays = {1,4,6,7,9,3};
  4. // 查找元素为7的下标值
  5. int result = Arrays.binarySearch(arrays,7);
  6. System.out.println(result);
  7. }

结果:

  1. 3

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

  1. // 我们常用的方法
  2. public static int binarySearch(int[] a, int key) {
  3. return binarySearch0(a, 0, a.length, key);
  4. }
  5. /*
  6. 参数说明如下: a 待查找的数组
  7. fromIndex 查找的开始位置
  8. toIndex 查找的结束位置
  9. key 查找的目标值
  10. */
  11. public static int binarySearch(int[] a, int fromIndex, int toIndex,
  12. int key) {
  13. // 进行异常检查
  14. rangeCheck(a.length, fromIndex, toIndex);
  15. return binarySearch0(a, fromIndex, toIndex, key);
  16. }
  17. // Like public version, but without range checks.
  18. private static int binarySearch0(int[] a, int fromIndex, int toIndex,
  19. int key) {
  20. int low = fromIndex;
  21. int high = toIndex - 1;
  22. while (low <= high) {
  23. // 找出查找范围的中间值
  24. int mid = (low + high) >>> 1;
  25. int midVal = a[mid];
  26. // 进行比较
  27. if (midVal < key)
  28. low = mid + 1;
  29. else if (midVal > key)
  30. high = mid - 1;
  31. else
  32. return mid; // key found
  33. }
  34. return -(low + 1); // key not found.
  35. }

当然实现的核心方法还是上述私有方法binarySearch0()这个方法,实现的逻辑也不复杂。

第一步就是声明两个变量存储查找区域的开始和结束。

第二步 循环,比较,不断的缩小比较的范围,直到找到数组中的值和目标值相同,返回下标,如果没有找到就返回一个负数也就是下面的这l两行代码:

  1. return mid; // key found
  2. return -(low + 1); // key not found.

我认为:这个二分法实现的亮点就在于求中间值的移位运算:

  1. int mid = (low + high) >>> 1;

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

3.3 数组的拷贝

  1. @Test
  2. public void testCopyArrange(){
  3. // 原数组
  4. int [] srcArray = {11,2,244,5,6,54};
  5. // 拷贝原数组长度为3的部分
  6. int[] descArray = Arrays.copyOf(srcArray,3);
  7. System.out.println(Arrays.toString(descArray));
  8. }

输出结果:

  1. [11, 2, 244]

源码分析:

  1. /* 参数说明:
  2. original 原数组
  3. newLength 拷贝的数组长度
  4. */
  5. public static int[] copyOf(int[] original, int newLength) {
  6. // 声明一个新数组的长度,存储拷贝后的数组
  7. int[] copy = new int[newLength];
  8. System.arraycopy(original, 0, copy, 0,
  9. Math.min(original.length, newLength));
  10. return copy;
  11. }
  12. public static native void arraycopy(Object src, int srcPos,
  13. Object dest, int destPos,
  14. int length);

分析: 主要还是调用了本地的方法arraycopy完成数组的指定长度拷贝,可以看到源码并没有对数组的长度进行检查,主要是arraycopy()这个方法时使了Math.min()方法,保证了你声明的长度在一个安全的范围之内,如果你拷贝的长度超出了数组的长度,就默认拷贝整个数组。至于native修饰的方法的使用,可以看看这里

  1. System.arraycopy(original, 0, copy, 0,
  2. Math.min(original.length, newLength));

当然如果需要拷贝数组指定的区间 ,可以使用Arrays的 copyOfRange(int[] original, int from, int to) 实现原理和arraycopy()方法的原理类似:

  1. @Test
  2. public void testCopy(){
  3. int [] srcArray = {11,2,244,5,6,54};
  4. // 拷贝指定范围的数组
  5. int[] descArray = Arrays.copyOfRange(srcArray,0,3);
  6. System.out.println(Arrays.toString(descArray));
  7. }

输出结果:

  1. [11, 2, 244]

注: copyOfRange(int[] original, int from, int to)中的参数to是不包含在拷贝的结果中的,上述的例子,就只能拷贝到索引为2的元素,不包含索引为3的元素,这点需要注意。

3.4 equals方法

主要重写了Object类的equals方法,用来比较两个数组内容是否相等,也就是他们中的元素是否相等。

基本用法:

  1. @Test
  2. public void equalTest(){
  3. int[] array ={1,2,3,4};
  4. int[] result ={1,2,3,4};
  5. System.out.println(Arrays.equals(array,result));
  6. System.out.println(array == result);
  7. }

结果:

  1. true
  2. false

看源码之前,有必要讲一下重写了equals方法之后,两个对象比较的是值,也就是他们的内容,这点非常的重要。重写equals方法的注意事项可以移步这里

源码如下:

  1. public static boolean equals(int[] a, int[] a2) {
  2. // 基于地址的比较
  3. if (a==a2)
  4. return true;
  5. if (a==null || a2==null)
  6. return false;
  7. int length = a.length;
  8. // 基于长度的比较
  9. if (a2.length != length)
  10. return false;
  11. // 比较每个元素是否相等
  12. for (int i=0; i<length; i++)
  13. if (a[i] != a2[i])
  14. return false;
  15. return true;
  16. }

源码说明如下:

源码判断了四次,分别是首地址比较,是否为空,以及长度的比较,最后对于数组的各个元素进行比较。

有必要说明下第一个判断,也就是首地址的比较。当我们声明一个数组变量时,这个变量就代表数组的首地址,看下面这个代码:

  1. @Test
  2. public void equalTest(){
  3. int[] array ={1,2,3,4};
  4. System.out.println(array);
  5. }

结果:

  1. [I@4f2410ac // [代表数组 I代表整数 @分隔符 后边内存地址十六进制

​ 这表示的是一个地址。还是因为在声明一个数组时,会在堆里面创建一块内存区域,但是这块内存区域相对于堆来说可能很小,不好找。为了方便查找,所以将数组内存中的首地址表示出来。虚拟机将地址传给变量名array。这也是引用类型,传的是地址,也就是理解成array指向内存地址(类似于家庭的地址),每次运行可能地址都不一样,因为虚拟机开辟的内存空间可能不一样。

理解了这个,那么a==a2就好理解了,如果两个数组内存地址都相同,那么两个数组的肯定是相等的。

还有我认为程序写的比较好的地方就是源码中对数组每个元素的比较,也就是下面这段代码;

  1. for (int i=0; i<length; i++)
  2. if (a[i] != a2[i])
  3. return false;
  4. return true;

使用a[i] != a2[i] 作为判断条件,就可以减少比较次数,提高了性能。试想一下如果这里是相等的比较,那每次都要遍历整个数组,如果数据量大了,无疑在性能上会慢很多。又一次感叹到源码的魅力。

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

Arrays 这个类中主要涉及了两种类型的排序方法串行 sort()和并行parallelSort()这两个方法,当然对象的排序和基本类型的排序也不太一样。这里还是以int[]类型的为例。进行说明。

首先比较两个方法的性能:


  1. public final int UPPER_LIMIT = 0xffffff;
  2. final int ROUNDS = 10;
  3. final int INCREMENT = 5;
  4. final int INIT_SIZE = 1000;
  5. @Test
  6. public void sortAndParallelSortTest(){
  7. // 构造不同容量的集合
  8. for (int capacity = INIT_SIZE; capacity < UPPER_LIMIT ; capacity*= INCREMENT) {
  9. ArrayList<Integer> list = new ArrayList<>(capacity);
  10. for (int j = 0; j < capacity; j++) {
  11. list.add((int) (Math.random()*capacity));
  12. }
  13. double avgTimeOfParallelSort = 0;
  14. double avgTimeOfSort = 0;
  15. for (int j = 0; j <= ROUNDS ; j++) {
  16. // 每次排序都打乱顺序
  17. Collections.shuffle(list);
  18. Integer[] arr1 = list.toArray(new Integer[capacity]);
  19. Integer[] arr2 = arr1.clone();
  20. avgTimeOfParallelSort += counter(arr1,true);
  21. avgTimeOfSort += counter(arr2, false);
  22. }
  23. // 输出结果
  24. output(capacity,avgTimeOfParallelSort/ROUNDS,avgTimeOfSort/ROUNDS);
  25. }
  26. }
  27. private void output(int capacity, double v, double v1) {
  28. System.out.println("=======================测试排序的时间=========");
  29. System.out.println("Capacity"+capacity);
  30. System.out.println("ParallelSort"+v);
  31. System.out.println("Sort"+v1);
  32. System.out.println("比较快的排序是:"+(v < v1 ? "ParallelSort":"Sort"));
  33. }
  34. // 计算消耗的时间
  35. private double counter(Integer[] arr1, boolean b) {
  36. long begin,end;
  37. begin = System.nanoTime();
  38. if(b){
  39. Arrays.parallelSort(arr1);
  40. }else{
  41. Arrays.parallelSort(arr1);
  42. }
  43. end = System.nanoTime();
  44. return BigDecimal.valueOf(end-begin,9).doubleValue();
  45. }

部分的测试的结果:

  1. =======================测试排序的时间=========
  2. Capacity1000
  3. ParallelSort6.284099999999999E-4
  4. Sort5.599599999999999E-4
  5. 比较快的排序是:Sort
  6. =======================测试排序的时间=========
  7. Capacity5000
  8. ParallelSort0.00163599
  9. Sort0.0018313699999999995
  10. 比较快的排序是:ParallelSort

可以看到在数据量比较小的情况下,使用sort()方法更快,一旦过了一个阈值,就是ParallelSort()这个方法性能好。这个阈值是多少呢。

我们先看一下parallelSort的源码:

  1. public static void parallelSort(int[] a) {
  2. int n = a.length, p, g;
  3. if (n <= MIN_ARRAY_SORT_GRAN ||
  4. (p = ForkJoinPool.getCommonPoolParallelism()) == 1)
  5. DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);
  6. else
  7. new ArraysParallelSortHelpers.FJInt.Sorter
  8. (null, a, new int[n], 0, n, 0,
  9. ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
  10. MIN_ARRAY_SORT_GRAN : g).invoke();
  11. }

可以看到当数组的长度小于MIN_ARRAY_SORT_GRAN或者p = ForkJoinPool.getCommonPoolParallelism()) == 1 (在单线程下)的时候,调用sort()排序的底层实现的DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);Arrays的开头定义的常量如下:

  1. private static final int MIN_ARRAY_SORT_GRAN = 1 << 13; // 这个值是8192

对比两者,也就是在数组的长度比较大或者是多线程的情况下,优先考虑并行排序,否则使用串行排序。

两个排序的核心思想:

  • sort()方法的核心还是快排和优化后的归并排序, 快速排序主要是对哪些基本类型数据(int,short,long等)排序, 而合并排序用于对对象类型进行排序。

  • parallelSort()它使用并行排序-合并排序算法。它将数组分成子数组,这些子数组本身先进行排序然后合并。

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

3.6 toString方法

基本用法:

  1. @Test
  2. public void toStringTest(){
  3. int[] array = {1,3,2,5};
  4. System.out.println(Arrays.toString(array));
  5. }

结果:

  1. [1, 3, 2, 5]

源码分析如下:

  1. public static String toString(int[] a) {
  2. // 1.判断数组的大小
  3. if (a == null)
  4. return "null";
  5. int iMax = a.length - 1;
  6. if (iMax == -1)
  7. return "[]";
  8. // 2.使用StringBuilder进行追加
  9. StringBuilder b = new StringBuilder();
  10. b.append('[');
  11. for (int i = 0; ; i++) {
  12. b.append(a[i]);
  13. if (i == iMax)
  14. return b.append(']').toString();
  15. b.append(", ");
  16. }
  17. }

具体的实现,已在源码的注释中进行了说明。这个方法对于基本数据类型来说,很方便的遍历数组。

追本溯源,方能阔步前行。

参考资料

https://blog.csdn.net/ExcellentYuXiao/article/details/52344628

sakuraTears的博客

javaSE的官方问档。

java中的Arrays这个工具类你真的会用吗的更多相关文章

  1. java中excel导入\导出工具类

    1.导入工具 package com.linrain.jcs.test; import jxl.Cell; import jxl.Sheet; import jxl.Workbook; import ...

  2. java中定义一个CloneUtil 工具类

    其实所有的java对象都可以具备克隆能力,只是因为在基础类Object中被设定成了一个保留方法(protected),要想真正拥有克隆的能力, 就需要实现Cloneable接口,重写clone方法.通 ...

  3. java中文件操作的工具类

    代码: package com.lky.pojo; import java.io.BufferedReader; import java.io.BufferedWriter; import java. ...

  4. 在JAVA中自定义连接数据库的工具类

    为什么要自定义数据库连接的工具类: 在开发中,我们在对数据库进行操作时,必须要先获取数据库的连接,在上一篇随笔中提到的获取数据库连接的步骤为: 1.定义好4个参数并赋值 2.加载驱动类 3.获取数据库 ...

  5. Java中的集合Collections工具类(六)

    操作集合的工具类Collections Java提供了一个操作Set.List和Map等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序.查询和修改等操作,还提供了将集 ...

  6. java中重要的多线程工具类

    前言 之前学多线程的时候没有学习线程的同步工具类(辅助类).ps:当时觉得暂时用不上,认为是挺高深的知识点就没去管了.. 在前几天,朋友发了一篇比较好的Semaphore文章过来,然后在浏览博客的时候 ...

  7. java中IO写文件工具类

    以下是一些依据经常使用java类进行组装的对文件进行操作的类,平时,我更喜欢使用Jodd.io中提供的一些对文件的操作类,里面的方法写的简单易懂. 当中jodd中提供的JavaUtil类中提供的方法足 ...

  8. Java中Date类型的工具类

    package com.mytripod.util; import java.text.DateFormat; import java.text.SimpleDateFormat; import ja ...

  9. java中常用的并发工具类

    · 1. 等待多线程完成的CountDownLatch 构造函数接收一个int类型的参数作为计数器,如果想等待N个点,就传入N.当调用CountDownLatch的countDown方法时,N就会减一 ...

随机推荐

  1. css背景图定位和浮动

    网站图标引入:<link rel="shortcut icon" href="ico图标地址"> 背景图片  background-image: u ...

  2. Redis 入门到分布式 (一)Redis初识

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.Redis特性目录 Redis的特性: 速度快 持久化 多种数据结构 支持多种编辑语言 功能丰富 简 ...

  3. Java实现 蓝桥杯VIP 算法训练 蜜蜂飞舞

    时间限制:1.0s 内存限制:512.0MB 问题描述 "两只小蜜蜂呀,飞在花丛中呀--" 话说这天天上飞舞着两只蜜蜂,它们在跳一种奇怪的舞蹈.用一个空间直角坐标系来描述这个世界, ...

  4. Java实现 LeetCode 11 盛最多水的容器

    11. 盛最多水的容器 给定 n 个非负整数 a1,a2,-,an,每个数代表坐标中的一个点 (i, ai) .在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) ...

  5. Java实现 LeetCode 2 两数相加

    两数相加 给出两个 非空 的链表用来表示两个非负的整数.其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字. 如果,我们将这两个数相加起来,则会返回一个新的链表来表 ...

  6. java实现蓝桥杯约瑟夫环

    n 个人的编号是 1~n,如果他们依编号按顺时针排成一个圆圈,从编号是1的人开始顺时针报数. (报数是从1报起)当报到 k 的时候,这个人就退出游戏圈.下一个人重新从1开始报数. 求最后剩下的人的编号 ...

  7. Java实现 蓝桥杯 算法提高金属采集

    问题描述 人类在火星上发现了一种新的金属!这些金属分布在一些奇怪的地方,不妨叫它节点好了.一些节点之间有道路相连,所有的节点和道路形成了一棵树.一共有 n 个节点,这些节点被编号为 1~n .人类将 ...

  8. java实现第三届蓝桥杯地址格式转换

    地址格式转换 [编程题](满分21分) Excel是最常用的办公软件.每个单元格都有唯一的地址表示.比如:第12行第4列表示为:"D12",第5行第255列表示为"IU5 ...

  9. 本地存储 localStorage

    本地存储localStorage 概念:window对象下面的属性,html5新增的,将5M大小的数据存储本地的浏览器上面. 浏览器支持存储5M大小 本地存储localStorage特点 本地存储属于 ...

  10. Openshift 4.4 静态 IP 离线安装系列:准备离线资源

    本系列文章描述了离线环境下以 UPI (User Provisioned Infrastructure) 模式安装 Openshift Container Platform (OCP) 4.4.5 的 ...