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://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. (Java实现) 洛谷 P1042 乒乓球

    题目背景 国际乒联现在主席沙拉拉自从上任以来就立志于推行一系列改革,以推动乒乓球运动在全球的普及.其中1111分制改革引起了很大的争议,有一部分球员因为无法适应新规则只能选择退役.华华就是其中一位,他 ...

  2. Java实现 蓝桥杯VIP 基础练习 高精度加法

    java算法 蓝桥杯 高精度加法 问题描述 在C/C++语言中,整型所能表示的范围一般为-231到231(大约21亿),即使long long型,一般也只能表示到-263到263.要想计算更加规模的数 ...

  3. Java实现 LeetCode 387 字符串中的第一个唯一字符

    387. 字符串中的第一个唯一字符 给定一个字符串,找到它的第一个不重复的字符,并返回它的索引.如果不存在,则返回 -1. 案例: s = "leetcode" 返回 0. s = ...

  4. Java实现 蓝桥杯VIP 算法提高 Quadratic Equation

    算法提高 Quadratic Equation 时间限制:1.0s 内存限制:512.0MB 问题描述 求解方程ax2+bx+c=0的根.要求a, b, c由用户输入,并且可以为任意实数. 输入格式: ...

  5. java实现猜算式

    题目:猜算式 你一定还记得小学学习过的乘法计算过程,比如: x 15 ------ 273 ------ 请你观察如下的乘法算式 *** x *** -------- *** *** *** ---- ...

  6. Python数据分析之双色球高频数据统计

    Step1:基础数据准备(通过爬虫获取到),以下是从第一期03年双色球开奖号到今天的所有数据整理,截止目前一共2549期,balls.txt 文件内容如下 : 备注:想要现成数据的可以给我发邮件哟~ ...

  7. python——公司年会抽奖小程序

    张三科技有限公司有300名员工,开年会抽奖,奖项如下一等奖3名 : 泰国五日游二等奖6名 :iphone手机三等奖30名 :避孕套一盒规则:1.一共抽3次,第一次抽3等奖,第二次抽2等奖,第三次压轴抽 ...

  8. mysql基础之-mysql锁和事务(七)

    0x01 MySQL锁: 执行操作时施加锁的模式 读锁:用户在读的时候施加的锁,为防止别人修改,但是用户可以读,还被称为共享锁 不会对其他用户进行阻塞 理解: ----->(这里的不阻塞,是可以 ...

  9. 说出 Servlet 的生命周期,并说出 Servlet 和 CGI 的区别。

    Servlet 被服务器实例化后,容器运行其 init 方法,请求到达时运行其 service 方法,service 方法自动派 遣运行与请求对应的 doXXX 方法(doGet,doPost)等,当 ...

  10. 【shell】十分钟轻松入门;如果没入门,您吐口口水再走吧!

    一.什么是shell? Shell是什么? 1.Shell 是一个程序,Linux默认是用bash. Shell 是一个用 C 语言编写的程序,既是一种命令语言,又是一种程序设计语言,是用户使用Lin ...