Java 源码 - java.util.Arrays

695 阅读12分钟

概述

java.util.Arrays 类是 JDK 提供的一个工具类,用来处理数组的各种方法,而且每个方法基本上都是静态方法,能直接通过类名Arrays调用。

构造方法和属性

private Arrays() {}
// 进行并行排序的最小数组长度
private static final int MIN_ARRAY_SORT_GRAN = 1 << 13;
// 在使用归并排序中优先使用插入排序的阈值(后续会移除掉)
private static final int INSERTIONSORT_THRESHOLD = 7;

和Collections一样,构造方法私有,不对外提供。我们只需调用Arrays的静态方法来操作数据即可。

基本方法

sort()

Arrays提供了一系列重载的sort方法,大体上可以分为两种,一种是针对基本数据类型来进行排序,包括int,long,byte,float,double等类型,另一种是针对Object数组类型来进行排序。默认都是升序排列的。我们来简单看一下int数组的排序:

public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
// 对数组的一部分进行排序
public static void sort(int[] a, int fromIndex, int toIndex) {
    // 检测数组的下标范围
    rangeCheck(a.length, fromIndex, toIndex);
    DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}

  可以看到,底层都是通过调用DualPivotQuicksort该类的sort方法来实现的。DualPivotQuicksort这个类是JAVA1.7之后专门提供给Java内部排序使用的专用类,被称为双枢轴快速排序,用来优化原先的快速排序,该算法一般能提供O(nlog(n))的时间复杂度,大家有兴趣的可以查看它的源码来学习一下。
至于其他类型的排序,和int类型排序都是类似的,不多说了。另外简单说一点,就是我们使用的Collections的sort方法就是借助Arrays.sort方法来实现的。

另外一种重载的sort方法:

public static void sort(Object[] a)
public static void sort(Object[] a, int fromIndex, int toIndex)
// 带比较器的排序算法
public static <T> void sort(T[] a, Comparator<? super T> c)
// 带比较器的 数组的部分排序
public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                Comparator<? super T> c)

该方法要求传入的参数必须实现了Comparable,这个排序算法是一种稳定的算法。由于实现大致相似,我们来简单看下sort(Object[] a)

public static void sort(Object[] a) {
    // 使用归并排序
    if (LegacyMergeSort.userRequested)
        legacyMergeSort(a);
    // 使用TimSort排序
    else
        ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}

该算法的实现又分为两种,一种通过归并排序算法来实现,另一种通过使用TimSort排序算法来实现。TimSort算法是一种插入与传统归并算法结合的一种算法,是对归并算法的一种优化。至于使用哪一种算法,需要设置系统变量:java.util.Arrays.useLegacyMergeSort,通过设置为true,来使用归并算法,否则使用TimSort算法。默认情况下我们是不会用到归并算法的,并且在JDK文档中有说明,在后续的版本中,legacyMergeSort归并算法会被移除掉。所以,如果有兴趣,大家可以去看下TimSort算法的实现。这里参考自:
Compile in Java 6, run in 7 - how to specify useLegacyMergeSort?

/**
 * Old merge sort implementation can be selected (for
 * compatibility with broken comparators) using a system property.
 * Cannot be a static boolean in the enclosing class due to
 * circular dependencies. To be removed in a future release.
 */
static final class LegacyMergeSort {
    private static final boolean userRequested =
        java.security.AccessController.doPrivileged(
            new sun.security.action.GetBooleanAction(
                "java.util.Arrays.useLegacyMergeSort")).booleanValue();
}

/** To be removed in a future release. */
private static void legacyMergeSort(Object[] a) {
    Object[] aux = a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}

在 Arrays 类中有该方法的一系列重载方法,能对7种基本数据类型,包括 byte,char,double,float,int,long,short 等都能进行排序,还有 Object 类型(实现了Comparable接口),以及比较器 Comparator 。  

  ①、基本类型的数组

  这里我们以 int[ ] 为例看看:

 int[] num = {1,3,8,5,2,4,6,7};
 Arrays.sort(num);
 System.out.println(Arrays.toString(num));//[1, 2, 3, 4, 5, 6, 7, 8]

  通过调用 sort(int[] a) 方法,将原数组按照升序的顺序排列。下面我们通过源码看看是如何实现排序的:

    public static void sort(int[] a) {
        DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
    }

  在 Arrays.sort 方法内部调用 DualPivotQuicksort.sort 方法,这个方法的源码很长,分别对于数组的长度进行了各种算法的划分,包括快速排序,插入排序,冒泡排序都有使用。详细源码可以参考这篇博客。

  ②、对象类型数组

  该类型的数组进行排序可以实现 Comparable 接口,重写 compareTo 方法进行排序。

 String[] str = {"a","f","c","d"};
 Arrays.sort(str);
 System.out.println(Arrays.toString(str));//[a, c, d, f]

  String 类型实现了 Comparable 接口,内部的 compareTo 方法是按照字典码进行比较的。

  ③、没有实现Comparable接口的,可以通过Comparator实现排序

Person[] p = new Person[]{new Person("zhangsan",22),new Person("wangwu",11),new Person("lisi",33)};
Arrays.sort(p,new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
        if(o1 == null || o2 == null){
            return 0;
        }
        return o1.getPage()-o2.getPage();
    }
});    
System.out.println(Arrays.toString(p));

parallelSort()

Arrays同样提供了一系列重载的parallelSort方法,用于数字类型的并行排序,同样默认也是升序排列的。这一系列算法是JAVA1.8之后引入的,基于JAVA的并行处理框架fork/join框架,而fork/join框架,是Java1.7引入,目的是为了充分利用多核处理器,编写并行程序,提高程序性能的框架。

public static void parallelSort(byte[] a) {
    int n = a.length, p, g;
    // 如果数组长度小于并行排序的最小长度或者并行线程池的大小是1
    if (n <= MIN_ARRAY_SORT_GRAN ||
        (p = ForkJoinPool.getCommonPoolParallelism()) == 1)
        DualPivotQuicksort.sort(a, 0, n - 1);
    else
        new ArraysParallelSortHelpers.FJByte.Sorter
            (null, a, new byte[n], 0, n, 0,
             ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
             MIN_ARRAY_SORT_GRAN : g).invoke();
}

我们可以看到,程序里进行了判断,如果数组长度太小,小于并行排序的最小长度或者并行线程池的大小是1,这时候就不再使用并行处理,还是使用DualPivotQuicksort的sort方法来进行排序。这里有两点简单说明下:

  1. Arrays的常量MIN_ARRAY_SORT_GRAN ,如果数组容量太小,而使用并行处理的话,通常会导致跨任务的内存竞争,这样的话并行的速度就没有预想中的那么令人满意。
  2. ForkJoinPool是fork/join框架中的一个核心类,通过ForkJoinPool的getCommonPoolParallelism方法我们可以获取到并行线程池的大小,如果是1的话,也就是单核,就不需要使用并行处理了。因为默认情况下,并行线程池的大小等于CPU的核数。

parallelPrefix()

public static void parallelPrefix(int[] array, IntBinaryOperator op)
public static void parallelPrefix(T[] array, int fromIndex,int toIndex, BinaryOperator op)

Arrays提供了一系列parallelPrefix方法,用于对数组进行并行的二元迭代操作,其中方法的最后一个参数是二元操作符的意思,该运算符其实是一个函数表达式。说起来有点绕口,先看个例子:

public static void main(String[] args) {
    int[] numbers = {1, 2, 3, 4, 5};
    System.out.println("old array: ");
    Arrays.stream(numbers).forEach(n -> System.out.print(n + " "));

    // 二元迭代:累加
    System.out.println("\noperator: + ");
    IntBinaryOperator binaryOperator = (x, y) -> (x + y);
    Arrays.parallelPrefix(numbers, binaryOperator);
    Arrays.stream(numbers).forEach(n -> System.out.print(n + " "));

    // 二元迭代:累乘
    System.out.println("\noperator: * ");
    Arrays.parallelPrefix(numbers, (x, y) -> x * y);
    Arrays.stream(numbers).forEach(n -> System.out.print(n + " "));
}

output:

old array: 
1 2 3 4 5 
operator: + 
1 3 6 10 15 
operator: * 
1 3 18 180 2700 

例子参考自:Java 8 Arrays parallelPrefix method Example
也就是说,该方法是一种累计的操作,比如累加,数组中的每个元素依次与前面的所有元素累加,然后替换原来的元素。对于大型数组,并行迭代操作通常比顺序循环性能更好。同样,该方法也有多种重载方式,支持int,long,double等类型的操作。

该方法是JDK1.8之后引入的,底层是通过JDK内部提供的ArrayPrefixHelpers类来实现的,当然最终还是通过fork/join框架来实现的。

源码如下:

public static void parallelPrefix(int[] array, IntBinaryOperator op) {
    Objects.requireNonNull(op);
    if (array.length > 0)
        new ArrayPrefixHelpers.IntCumulateTask
                (null, op, array, 0, array.length).invoke();
}

binarySearch()

public static int binarySearch(int[] a, int key)
public static int binarySearch(int[] a, int fromIndex, int toIndex,int key)

顾名思义,这个方法是二分查找的意思,用来在数组中查找元素。当然,使用该方法之前,必须保证该数组已经排序完成,并且是升序完成。否则,查找将没有什么意义。如果数组包含多个指定查找值的元素,则无法保证找到具体哪一个值。
源码如下:

public static int binarySearch(int[] a, int key) {
    return binarySearch0(a, 0, a.length, key);
}

/**
 * 其中该方法和binarySearch的public方法一样,就是没有索引校验
 */
 // 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.
}

该方法比较简单,当然也是提供了一系列重载的方法,其中也可以自定义查找时的比较规则:
private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex, T key, Comparator<? super T> c)
大家等用到的时候可自行查看。

equals()

public static boolean equals(Object[] a, Object[] a2)

这个方法很简单,就是判断两个数组是否相等。比较的类型如果重写了equals方法,则按照对象的equals方法进行比较。该方法也有许多重载方法,实现类似,我们来查看下object数组作为参数的源码:

public static boolean equals(Object[] a, Object[] a2) {
    // 如果是同一个数组引用,则相等
    if (a==a2)
        return true;
    // 如果有一个数组为null,则不相等
    if (a==null || a2==null)
        return false;

    int length = a.length;
    // 如果长度不相等,则不相等
    if (a2.length != length)
        return false;
    // 如果对应位置上的值不相等,则不相等
    for (int i=0; i<length; i++) {
        Object o1 = a[i];
        Object o2 = a2[i];
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    如果以上都不满足,则相等
    return true;
}

equals() 和 deepEquals()

  ①、equals

  equals 用来比较两个数组中对应位置的每个元素是否相等。   

  八种基本数据类型以及对象都能进行比较。

  我们先看看 int类型的数组比较源码实现:

public static boolean equals(int[] a, int[] a2) {
        if (a==a2)//数组引用相等,则里面的元素一定相等
            return true;
        if (a==null || a2==null)//两个数组其中一个为null,都返回false
            return false;

        int length = a.length;
        if (a2.length != length)//两个数组长度不等,返回false
            return false;

        for (int i=0; i<length; i++)//通过for循环依次比较数组中每个元素是否相等
            if (a[i] != a2[i])
                return false;

        return true;
    }

  在看对象数组的比较:

public static boolean equals(Object[] a, Object[] 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++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }

        return true;
    }

  基本上也是通过 equals 来判断。

  ②、deepEquals

  也是用来比较两个数组的元素是否相等,不过 deepEquals 能够进行比较多维数组,而且是任意层次的嵌套数组。

         String[][] name1 = {{ "G","a","o" },{ "H","u","a","n"},{ "j","i","e"}};  
         String[][] name2 = {{ "G","a","o" },{ "H","u","a","n"},{ "j","i","e"}};
         System.out.println(Arrays.equals(name1,name2));// false  
         System.out.println(Arrays.deepEquals(name1,name2));// true

fill方法

public static void fill(long[] a, long val)
public static void fill(long[] a, int fromIndex, int toIndex, long val)
这个方法也很简单,就是将指定值填充到数组的每个元素。底层实现就是循环遍历填充即可。一般我们批量初始化数组的时候可以使用该方法。

public static void fill(int[] a, int val) {
    for (int i = 0, len = a.length; i < len; i++)
        a[i] = val;
} 

copyOf和copyOfRange方法

public static int[] copyOf(int[] original, int newLength)
public static T[] copyOfRange(T[] original, int from, int to)

复制数组,newLength是新数组的长度。如果新数组长度大于原数组长度,则新值填充null或相关类型默认值(比如int,默认填充0,double类型默认为0.0)。先来看下泛型的源码:

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

可以看到,底层是通过System的native方法arraycopy来实现的,这个方法在操作集合的时候也经常用到。而对于类型固定的则和这类似,看一下int类型:

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

而copyOfRange底层实现类似,就多了步对索引的判断,就不多说了。

asList()

将数组转为List,该方法与集合的toArray方法一起充当了基于数组和集合之间的桥梁。并且该方法还提供了一种很便捷的方法来创建一个初始化大小的列表,该列表初始化包含几个元素:

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

该方法源码如下:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

这里返回的ArrayList不是我们常用的java.util.ArrayList,而是Arrays的一个静态内部类,并且该内部类中没有add和remove方法,所以不支持添加和移除等操作。

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public int size() {
        return a.length;
    }

    @Override
    public Object[] toArray() {
        return a.clone();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        ...
    }

    @Override
    public E get(int index) {
        return a[index];
    }

    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }

    @Override
    public int indexOf(Object o) {
        ...
    }

    @Override
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(a, Spliterator.ORDERED);
    }

    @Override
    public void forEach(Consumer<? super E> action) {
        ...
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        ...
    }

    @Override
    public void sort(Comparator<? super E> c) {
        Arrays.sort(a, c);
    }
}

  作用是返回由指定数组支持的固定大小列表。

  注意:这个方法返回的 ArrayList 不是我们常用的集合类 java.util.ArrayList。这里的 ArrayList 是 Arrays 的一个内部类 java.util.Arrays.ArrayList。这个内部类有如下属性和方法:   

  ①、返回的 ArrayList 数组是一个定长列表,我们只能对其进行查看或者修改,但是不能进行添加或者删除操作

  通过源码我们发现该类是没有add()或者remove() 这样的方法的,如果对其进行增加或者删除操作,都会调用其父类 AbstractList 对应的方法,而追溯父类的方法最终会抛出 UnsupportedOperationException 异常。如下:

 String[] str = {"a","b","c"};
 List<String> listStr = Arrays.asList(str);
 listStr.set(1, "e");//可以进行修改
 System.out.println(listStr.toString());//[a, e, c]
 listStr.add("a");//添加元素会报错 java.lang.UnsupportedOperationException 

  ②、引用类型的数组和基本类型的数组区别

String[] str = {"a","b","c"};
List listStr = Arrays.asList(str);
System.out.println(listStr.size());//3

int[] i = {1,2,3};
List listI = Arrays.asList(i);
System.out.println(listI.size());//1

  上面的结果第一个listStr.size()==3,而第二个 listI.size()==1。这是为什么呢?

  我们看源码,在 Arrays.asList 中,方法声明为 List asList(T... a)。该方法接收一个可变参数,并且这个可变参数类型是作为泛型的参数。我们知道基本数据类型是不能作为泛型的参数的,但是数组是引用类型,所以数组是可以泛型化的,于是 int[] 作为了整个参数类型,而不是 int 作为参数类型。

  所以将上面的方法泛型化补全应该是:

String[] str = {"a","b","c"};
List<String> listStr = Arrays.asList(str);
System.out.println(listStr.size());//3

int[] i = {1,2,3};
List<int[]> listI = Arrays.asList(i);//注意这里List参数为 int[] ,而不是 int
System.out.println(listI.size());//1

Integer[] in = {1,2,3};
List<Integer> listIn = Arrays.asList(in);//这里参数为int的包装类Integer,所以集合长度为3
System.out.println(listIn.size());//3
复制代码

  ③、返回的列表ArrayList里面的元素都是引用,不是独立出来的对象

String[] str = {"a","b","c"};
List<String> listStr = Arrays.asList(str);
//执行更新操作前
System.out.println(Arrays.toString(str));//[a, b, c]
listStr.set(0, "d");//将第一个元素a改为d
//执行更新操作后
System.out.println(Arrays.toString(str));//[d, b, c]
复制代码

  这里的Arrays.toString()方法就是打印数组的内容,后面会介绍。我们看修改集合的内容,原数组的内容也变化了,所以这里传入的是引用类型。

  ④、已知数组数据,如何快速获取一个可进行增删改查的列表List?

 String[] str = {"a","b","c"};
 List<String> listStr = new ArrayList<>(Arrays.asList(str));
 listStr.add("d");
 System.out.println(listStr.size());//4
复制代码

  这里的ArrayList 集合类后面我们会详细讲解,大家目前只需要知道有这种用法即可。

  ⑤、Arrays.asList() 方法使用场景

  Arrays工具类提供了一个方法asList, 使用该方法可以将一个变长参数或者数组转换成List 。但是,生成的List的长度是固定的;能够进行修改操作(比如,修改某个位置的元素);不能执行影响长度的操作(如add、remove等操作),否则会抛出UnsupportedOperationException异常。

  所以 Arrays.asList 比较适合那些已经有数组数据或者一些元素,而需要快速构建一个List,只用于读取操作,而不进行添加或删除操作的场景。

toString() 和 deepToString()

  toString 用来打印一维数组的元素,而 deepToString 用来打印多层次嵌套的数组元素。

public static String toString(int[] a) {
        if (a == null)
            return "null";
        int iMax = a.length - 1;
        if (iMax == -1)
            return "[]";

        StringBuilder b = new StringBuilder();
        b.append('[');
        for (int i = 0; ; i++) {
            b.append(a[i]);
            if (i == iMax)
                return b.append(']').toString();
            b.append(", ");
        }
    }