Java工具类Arrays中不得不知的常用方法

Arrays

位于java.util包内的Arrays类是Java提供的一个操作数组的工具类,其内部定义了一些常见的用于操作数组的静态方法,下面就按照以下几个常用类型,梳理一下。

  • 数组转List
  • 排序
  • 查找
  • 元素填充
  • 数组复制
  • toString
  • 相等性判断

Arrays 数组操作集

数组转List ---asList

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

这个被“普遍”称为数组转List的方法,可能是Arrays内大家使用频率最高的一个静态方法了。使用起来也很简单,下面就很容易的实现了将数组转为List。

        String[] b = new String[]{"5", "6", "7", "8"};
        List<String> datas = Arrays.asList(b);

当然还有另一种使用方法:

List<String> datas = Arrays.asList("5", "6", "7", "8");

其实,个人感觉“数组转List”这种说法是没有意义的;我们都知道在List是一个接口,而真正实现了这个接口的类只有ArrayList,LinkedList,Vector。其中ArrayList和Vector内部都是使用“动态数组”实现,LinkedList采用链表结构实现。根据上一篇数据接口-线性表我们知道,数据的物理结构只用顺序存储接口和链式存储结构,List也不出其右。因此说这个方法实现了数组拷贝更确切一点

下面,根据他的具体实现,我们更能体会到拷贝的意义。

/**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * <p>This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * <pre>
     *     List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
     * </pre>
     *
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    @SafeVarargs
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }


    /**
     * @serial include
     */
    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
        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;
        }
        ……
    }

这里需要注意的是,asList内部的ArrayList并不是我们常用的那个ArrayList,而是在Arrays类内部的一个私有静态类。从代码可以看出,这个内部的ArrayList,和常规的ArrayList相比,并没有实现List接口,而是直接继承了AbstractList。

以下所说的ArrayList 统一指此处的静态类

asList的实现很简单,返回了一个ArrayList的实例,参数为所要拷贝的数组名。

可以看到asList() 是接受一个泛型的变长参数的,而基本数据类型是无法被泛型化的。而对于泛型而言,基本数据类型,实际上会被人为是一个 [x 的类型。 [ 表示这是一个数组,x 为当前数组的类型。

因此,这个方法不能直接“转换”基础数据类型的数组。

        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
        public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

可以看到,正常情况下,ArrayList的构造函数完成的工作就是一个赋值操作,把我们传递进来的数组赋给a,而a就是一个数组。说白了,这就是一个数组拷贝的过程。再看ArrayList内部实现,get,set 都是根据数组下标实现简单的数组赋值操作。这里省略了ArrayList内部几个方法,总之都是对数组的操作,有兴趣的同学,可以自行查看源码。注意,这个类内部,并没有add方法的具体实现,也就是说AbstractList内部的add方法并没有被覆盖

AbstractList#add

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

因此,通过asList返回的List,一定不能进行add 操作,否则会抛出异常。

通过以上分析,我们可以得出以下结论:

  • asList 不接受基本数据类型的数组名,作为参数直接传递。
  • asList 通过backed(拷贝)的方式,返回的是一个固定长度的List,这点从方法注释也可以看到
  • 鉴于第二条,不能对这个返回的List执行add 方法,可以调用set方法。
  • 同理不能调用remove方法,但可以调用get方法获取元素。

这里关于第一个结论,还需要解释一下,不接受基本数据类型的数组名作为参数,但是以下实现是可以的。

List ds = Arrays.asList(1, 2, 4, 4, 5);

好了,Arrays.asList的用法就说到这里了。既然都到这里了,顺便多说一句,List转数组的实现,Collection接口定义了统一的方法toArray。对于不同的List实现,统一调用即可。

List<String> datas;
.....
String[] result= (String[]) datas.toArray();

排序

sort

sort() 方法顾名思义,主要是实现数组的排序,默认按升序进行排列。

Arrays 内部关于sort的实现,可以大体分为两类,一类是基本数据类型的排序,一类是Object类型的排序。

  • 基本数据类型的排序
    //对数组排序
    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);
    }

sort静态方法的实现,按参数主要有两种实现方法,一种是数组整体进行排序;一种是在数组内指定一段起始位置进行排序,之后的对象数组排序也只是按指定起始位置排序,不再重复描述。其内部具体实现是DualPivotQuicksort(双轴快速排序)。

这里可以接收的参数类型除了int数组,还可以是long,short,char,byte,float,double类型数组。

  • Object 类型数组排序

实现Comparable接口的对象数组排序

    public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }

注意,使用这个方法时,提供的“数组中的对象”必须是实现了Comparable接口的,也就是说必须告知明确告知,对数组中的对象是按什么规则排序。

实现Comparator接口的排序

    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

使用这个方法是泛型为T的数组,需要提供一个实现了Comparator接口的实例,同理也是必须明确告知排序规则,如果同时实现了Comparable接口和Comparator接口,Comparator接口的实现将覆盖Comparable接口的排序规则。

对象数组排序内部实现采用了LegacyMergeSort(归并排序)和TimSort排序。

parallelSort

parallelSort 是Java8新增的排序方式,和sort方法不同的是,他采用多线程并行的方式进行排序,当数据规模较大时和sort相比有明显优势;具体可见arrays-sort-versus-arrays-parallelsort.

public static final int MIN_ARRAY_SORT_GRAN = 1 << 13;

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

parallelSort 使用方式及可接受参数类型和sort方法基础数据类型时的参数完全一致。

查找

Arrays内部的查找,主要是binarySearch(二分查找法)。可以说,关于查找到实现分类和排序完全一样。首先从数据类型上也是分为基础数据类型构成的数组和对象数组。都支持按特定范围进行排序;对于对象数组的排序,对象数组需要实现Comparable接口或者是提供Comparator接口的实例。

    public static int binarySearch(int[] a, int key) {
        return binarySearch0(a, 0, a.length, key);
    }
    //可以在指定范围排序
    public static int binarySearch(int[] a, int fromIndex, int toIndex,
                                   int key) {
        rangeCheck(a.length, fromIndex, toIndex);
        return binarySearch0(a, fromIndex, toIndex, key);
    }
    //Object[]数组中的Object必须实现了Comparable接口
    public static int binarySearch(Object[] a, int fromIndex, int toIndex,
                                   Object key) {
        rangeCheck(a.length, fromIndex, toIndex);
        return binarySearch0(a, fromIndex, toIndex, key);
    }
    //提供Comparator实例
    public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c) {
        return binarySearch0(a, 0, a.length, key, c);
    }

元素填充

    public static void fill(Object[] a, int fromIndex, int toIndex, Object val) {
        rangeCheck(a.length, fromIndex, toIndex);
        for (int i = fromIndex; i < toIndex; i++)
            a[i] = val;
    }

fill()方法,使用很简单,也很好理解,将数组用特定的元素val 填满即可,也可以是特定位置。

复制

这个方法,就是实现两个将原数组按指定长度复制到目标数组内返回。

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        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.arraycopy方法,这是一个Java提供的native方法,因此效率会高一些。Java 内部关于数组复制的实现,都用到了这个方法。

同样,也包括一个copyOfRange的方法,这个按名字就可以理解,就是按照范围进行复制。

toString

这个toString的静态方法,其实也很实用;从下面的代码的实现,可以看出,他的作用就是将我们定义的数组,按照 "[a0,a1,....]"的格式转成字符串,方便我们直接打印整个数组,打印出来的日志也会看起来更直观,更方便。

public static String toString(double[] 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(", ");
        }
    }

它除了支持8种基础数据类型的数组外,还支持Object类型的数组。

相等性

equals

Arrays内部关于两个数组相等的判断可以首先看下下面的代码:

基础数据类型数组,以long类型为例

    public static boolean equals(float[] a, float[] 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 (Float.floatToIntBits(a[i])!=Float.floatToIntBits(a2[i]))
                return false;

        return true;
    }

Object 类型数组

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

我们知道,数组名代表数组首地址;因此,从以上代码可以得出结论,

当两个数组不是同一数组时,也就是a==a2 不成立。

当满足以下任一条件时:

  • 两个数组中有一个为空时
  • 两个数组长度不等时
  • 两个数组中包含任意不相等的元素时

就认为两个数组不相等,反之则认为相等。对于对象数组,相同位置的对象均为null是,认为是两个相同的元素。

deepEquals

关于deepEquals和equals的区别,可以看看这篇文章Java中Arrays类的两个方法:deepEquals和equals

其他

数组交换

    /**
     * Swaps x[a] with x[b].
     */
    private static void swap(Object[] x, int a, int b) {
        Object t = x[a];
        x[a] = x[b];
        x[b] = t;
    }

这个方法其实挺实用的,以后如果懒得写了,可以直接一行代码搞定。

最后

以上分析是基于Java jdk1.8 版本,在Java中由于lamdba表达式,函数式编程思想的引入,Arrays内部新增了许多相关的类如Stream 等,考虑到使用频率,暂时不展开讨论了。


参考内容:

https://juejin.im/post/5989841b6fb9a03c38109b8c

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 对象的创建与销毁 Item 1: 使用static工厂方法,而不是构造函数创建对象:仅仅是创建对象的方法,并非Fa...
    孙小磊阅读 1,963评论 0 3
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,485评论 0 3
  • Java 语言支持的类型分为两类:基本类型和引用类型。整型(byte 1, short 2, int 4, lon...
    xiaogmail阅读 1,344评论 0 10
  • 我为什么不睡她们 昨天发了篇《你为什么泡不到妞》的文章受到大家的热烈欢迎,某时间段内一度风靡朋友圈,当然“威逼力诱...
    easy喂阅读 308评论 2 2