Java1.8-Arrays源码解析

概述

集合框架的工具类除了Collecions之外,另一个就是Arrays了,上文已经说过,Java中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);
}
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 <T> void parallelPrefix(T[] array, int fromIndex,int toIndex, BinaryOperator<T> 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;
}
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> 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);
    }
}
hashCode方法

  获取数组的hashCode值,该值是基于数组的每一个元素的hashCode来实现的。
  如果数组中有嵌套数组,我们还想根据里层数组的每个元素的hashCode来获取hash的话,我们可以通过deepHashCode方法,该方法通过递归的方式来实现。

通俗的来讲,hashCode方法只计算到数组的第一层,而deepHashCode方法则会一直递归调用到数组无法再拆分为止。

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;
    int result = 1;
    // 遍历每个元素,调用每个元素的hashCode方法
    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());
    return result;
}

大概看一下deepHashCode的方法:

for (Object element : a) {
            int elementHash = 0;
            if (element instanceof Object[])
                elementHash = deepHashCode((Object[]) element);
            else if (element instanceof byte[])
                elementHash = hashCode((byte[]) element);
            else if (element instanceof short[])
                elementHash = hashCode((short[]) element);
            else if (element instanceof int[])
                elementHash = hashCode((int[]) element);
            else if (element instanceof long[])
                elementHash = hashCode((long[]) element);
            else if (element instanceof char[])
                elementHash = hashCode((char[]) element);
            else if (element instanceof float[])
                elementHash = hashCode((float[]) element);
            else if (element instanceof double[])
                elementHash = hashCode((double[]) element);
            else if (element instanceof boolean[])
                elementHash = hashCode((boolean[]) element);
            else if (element != null)
                elementHash = element.hashCode();

            result = 31 * result + elementHash;
        }
deepEquals方法

同样,对于数组的equals方法,也是只比较第一层,如果有多层嵌套的话,我们可以使用deepEquals方法来比较。

toString方法

这个方法我们经常用到,这里就是返回数组的字符串表示形式,我们可以更直观的看到数组里保存的都是什么。

public static String toString(Object[] a) {
    if (a == null)
        return "null";

    int iMax = a.length - 1;
    if (iMax == -1)
        return "[]";
    // 内部使用StringBuilder来表示
    StringBuilder b = new StringBuilder();
    b.append('[');
    for (int i = 0; ; i++) {
        b.append(String.valueOf(a[i]));
        if (i == iMax)
            return b.append(']').toString();
        b.append(", ");
    }
}

同理,还有一个deepToString方法,用来返回嵌套数组的字符串表示形式。这里就不多说了。

setAll方法

public static <T> void setAll(T[] array, IntFunction<? extends T> generator)

使用生成器函数更新数组的每个元素,其中最后一个参数是JDK1.8之后的一个一元操作符,也是函数式接口。我们先看一个例子:

public static void main(String[] args) {
    int[] number = {1, 2, 3, 4, 5, 6};
    Arrays.setAll(number, x -> x * 3);
    System.out.println(Arrays.toString(number));
}

output:

[0, 3, 6, 9, 12, 15]

上述方法是为数组的每个元素乘以3,重新赋值给数组。
源码如下:

public static <T> void setAll(T[] array, IntFunction<? extends T> generator) {
    Objects.requireNonNull(generator);
    for (int i = 0; i < array.length; i++)
        array[i] = generator.apply(i);
}

通过调用函数式接口generator的apply方法来实现一元运算符操作。

parallelSetAll方法

同样,parallelSetAll方法是setAll的并行方式,通过借助JDK1.8的IntStream对象来实现。

public static <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator) {
    Objects.requireNonNull(generator);
    IntStream.range(0, array.length).parallel().forEach(i -> { array[i] = generator.apply(i); });
}
spliterator方法

JDK8之后引入的并行迭代器,目的就是并行遍历数组中的元素。可以与原先的Iterator迭代器相比较。Spliterator是很有意思的一个类,如果有时间,再单独分析一下,现在先简单看一下源码即可:

public static <T> Spliterator<T> spliterator(T[] array) {
    return Spliterators.spliterator(array,Spliterator.ORDERED | Spliterator.IMMUTABLE);
}
stream方法

Stream也是JDK8之后引入的用于处理集合或者数组等类型的流,一般Stream配合lambda表达式使用,可以极大的提高我们编程的效率和程序的可读性。对于Stream,我们也是等有时间了单独分析一下,现在也是先简单看一下源码,顺便看一下简单的实现:

public static <T> Stream<T> stream(T[] array) {
    return stream(array, 0, array.length);
}

public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
    return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
}

看一下简单的实现:

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

output:

1 2 3 4 5 6 

总结

Arrays的源码到现在基本上已经看完了,我们可以做个大致的总结:

  1. Arrays的源码大致分为三种:parallel开头的都是并行处理的,deep开头的都是用于数组嵌套相关的操作,另一种就是我们常用的简单操作;
  2. Arrays中基本上每个方法都有一系列的重载方法用于满足不同类型的操作,我们可以根据需要选择性的调用。

本文参考的链接如下:
https://docs.oracle.com/javase/8/docs/api/
读 Java Arrays 源码 笔记
快速排序实现及优化 | DualPivotQuicksort
Java8里面的java.util.Spliterator接口有什么用?

参考网站主要是:stackoverflow,搜索引擎:Google。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容