美文网首页
03-Arrays、Collections、Objects 常用

03-Arrays、Collections、Objects 常用

作者: xinxisimple | 来源:发表于2020-02-27 15:47 被阅读0次

    注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

    1 工具类通用的特征

    工具类通用的特征写法:

    1. 构造器必须是私有的。这样的话,工具类就无法被 new 出来,因为工具类在使用的时候,无需初始化,直接使用即可,所以不会开放出构造器。
    2. 工具类的工具方法必须被 static、final 关键字修饰。这样的话就可以保证方法不可变,并且可以直接使用,非常方便。

    注意:尽量不要在工具方法中,对共享变量有做修改的操作(如果必须要做的话必须加锁),因为会有线程安全的问题。除此之外,工具类方法本身是没有线程安全问题的,可以放心使用。(因为被 static 修饰的方法,数据运行在栈里面,栈的数据每个线程都是隔开的,所以不会有线程安全的问题,<a href="https://blog.csdn.net/xinxisimple/article/details/104502582#t2">参考:static 关键字</a>)

    2 Arrays

    Arrays 主要对数组提供了一些高效的操作,比如排序、查找、填充、拷贝、相等判断等。

    2.1 排序

    Arrays.sort 方法主要用于排序,入参支持 int、long、double 等各种基本类型的数组,也支持自定义类的数组。如:

    @Data
    class SortEntity {
        private String target;
    
        SortEntity(String target) {
            this.target = target;
        }
    }
    
    @Test
    public void sortTest() {
        List<SortEntity> list = List.of(
                new SortEntity("100"),
                new SortEntity("30"),
                new SortEntity("90"),
                new SortEntity("200"),
                new SortEntity("150")
        );
        SortEntity[] array = new SortEntity[list.size()];
        list.toArray(array);
    
        System.out.println("排序之前:" + JSON.toJSONString(array));
        Arrays.sort(array, Comparator.comparing(d -> d.getTarget()));
        System.out.println("排序之后:" + JSON.toJSONString(array));
    }
    

    执行结果:

    排序之前:[{"target":"100"},{"target":"30"},{"target":"90"},{"target":"200"},{"target":"150"}]
    排序之后:[{"target":"100"},{"target":"150"},{"target":"200"},{"target":"30"},{"target":"90"}]
    

    从输出结果中可以看到,排序之后的数组已经是有顺序的了,也可以看到 sort 方法支持两个入参:要排序的数组和外部排序器。

    sort 方法排序的性能较高,主要原因是 sort 使用了双轴快速排序算法(参考:<a href="">略</a>)。

    2.2 二分查找法

    Arrays.binarySearch 方法主要用于快速从数组中查找出对应的值。其支持的入参类型非常多,如 byte、int、lang 各种类型的数组。返回参数是查找到的对应数组下标的值,如果查询不到,则返回负数。如:

    List<SortEntity> list = List.of(
            new SortEntity("100"),
            new SortEntity("30"),
            new SortEntity("90"),
            new SortEntity("200"),
            new SortEntity("150")
    );
    SortEntity[] array = new SortEntity[list.size()];
    list.toArray(array);
    
    System.out.println("排序之前:" + JSON.toJSONString(array));
    Arrays.sort(array, Comparator.comparing(d -> d.getTarget()));
    System.out.println("排序之后:" + JSON.toJSONString(array));
    
    int index = Arrays.binarySearch(array, new SortEntity("90"), Comparator.comparing(d -> d.getTarget()));
    if (index < 0) {
        throw new RuntimeException("Not Found");
    }
    System.out.println("搜索结果:" + index);
    

    执行结果:

    排序之前:[{"target":"100"},{"target":"30"},{"target":"90"},{"target":"200"},{"target":"150"}]
    排序之后:[{"target":"100"},{"target":"150"},{"target":"200"},{"target":"30"},{"target":"90"}]
    搜索结果:4
    

    从上述代码中我们需要注意两点:

    1. 如果被搜索的数组是无序的,一定要先排序,否则二分搜索很可能搜索不到,我们示例中也先对数组进行了排序;
    2. 搜索方法返回的的数组的下标值。如果搜索不到,返回的下标值就会是负数,这时我们需要判断一下正负。如果是负数,还从数组中获取数据的话,会报数组越界的错误。示例中对这种情况进行了判断,如果是负数,会提前抛出明确的异常。

    二分法底层代码的实现:

    // a: 要搜索的数组;fromIndex: 开始搜索的索引,默认是0;toIndex: 结束搜索的索引,默认是数组大小
    // key: 我们要搜索的值;c: 外部比较器
    private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
                                         T key, Comparator<? super T> c) {
        // 如果比较器 c 是空的,则直接使用 key 的 Comparable.compareTo 方法进行排序
        // 假设 key 类型是 String,String 默认实现了 Comparable 接口,就可以直接使用 compareTo 方法进行排序
        if (c == null) {
            return binarySearch0(a, fromIndex, toIndex, key);
        }
        int low = fromIndex;
        int high = toIndex - 1;
        // 开始位置小于结束位置,就会一直循环搜索
        while (low <= high) {
            // 找到开始位置和结束位置的中间值
            // 假设 low = 0, high = 10, 那么 mid 就是 5,所以说二分的意思主要是在这里,每次都是计算索引的中间值
            int mid = (low + high) >>> 1;
            T midVal = a[mid];
            // 比较数组中间值和给定值 key 的大小关系
            int cmp = c.compare(midVal, key);
            // 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边
            if (cmp < 0)
                low = mid + 1;
            // 要找的值在中间值的左边
            else if (cmp > 0)
                high = mid - 1;
            // 找到了
            else
                return mid; // key found
        }
        // 返回的值数负数,表示没有找到
        return -(low + 1);  // key not found.
    }
    

    二分的主要意思是每次查找之前,都找到中间值,然后拿我们要比较的值和中间值比较,根据结果修改比较的上限或者下限,通过循环最终找到相等的位置索引。

    2.3 拷贝

    数组拷贝我们经常遇到,有时需要拷贝整个数组,有时需要拷贝部分,比如 ArrayList 在 add(扩容) 或 remove(删除元素不是最后一个) 操作时,会进行一些拷贝。拷贝整个数组我们可以使用 copyOf 方法,拷贝部分我们可以使用 copyOfRange 方法。查看 copyOfRange 底层源码的实现:

    // orginal: 要拷贝的原始数组
    // from: 拷贝起点
    // to: 拷贝终点
    public static int[] copyOfRange(int[] original, int from, int to) {
        // 需要拷贝的长度
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        // 初始化新的数组
        int[] copy = new int[newLength];
        // 调用 native  方法进行拷贝,参数意思分别为:
        // 被拷贝的数组、从被拷贝数组的哪里开始、目标数组、从目标数组的哪里开始拷贝、拷贝的长度
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }
    

    从源码中,我们发现,Arrays 的拷贝方法,实际上底层调用的是 System.arraycopy 这个native 方法。

    3 Collections

    Collections 是为了方便使用集合而产生的工具类,Arrays 方便数组使用,Collections 方便集合使用。

    Collections 也提供了 sort 和 binarySearch 方法,sort 底层使用的就是 Arrays.sort 方法,binarySearch 底层是自己重写的二分查找算法,实现的逻辑和 Arrays 的二分查找法完全一致,这两个方法上 Collections 和 Arrays 的内部实现很类似,我们看看 Collections 独有的特性。

    3.1 求集合中最大、最小值

    提供了 max 方法来取得集合中的最大值,min 方法来取得集合中的最小值,max 和 min 方法很相似,以 max 为例说明,max 提供了两种类型的方法,一个需要传外部排序器,一个不需要传排序器,但需要集合中的元素强制实现 Comparable 接口。源码如下:

    public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();
    
        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) > 0)
                candidate = next;
        }
        return candidate;
    }
    

    使用如图:


    -

    从源码中我们可以学到:

    1. max 方法泛型 T 定义的非常巧妙,意思是泛型必须继承 Object 并且实现 Comparable 的接口。一般让我们来定义的话,我们可以会在方法里去判断有无实现 Comparable 接口,这种是在运行时才能知道结果。而这里泛型直接定义了必须实现 Comparable 接口,在编译的时候就可以告诉使用者,当前类没有实现 Comparable 接口,使用起来很友好;
    2. 给我么提供了实现两种排序机制的友好示例:自定义类实现 Comparable 接口和传入外部排序器。两种排序实现原理类似,但实现有所差别,我们在使用中如果需要些排序的工具类时可以效仿。

    3.2 多种类型的集合

    Collections 对原始集合类进行了封装,提供了更好的集合类给我们,一种是线程安全的集合,一种是不可变的集合,针对 List、Map、Set 都有提供。

    3.2.1 线程安全的集合

    线程安全的集合都是 synchronized 开头的,如下:

    synchronizedXxx
    从方法命名我们可以看出,底层都是通过 synchronized 轻量锁来实现的,以 synchronizedList 为例说明底层实现:
    synchronizedList
    可以看到 List 的所有操作方法都加上了 synchronized 锁,所以多线程对集合同时进行操作是线程安全的。

    3.2.2 不可变的集合

    得到不可变集合的方法都是以 unmodifiable 开头的。这类方法的意思是,我们会从原集合中得到一个不可变的新集合,新集合只能访问,无法修改。一旦修改,就会抛出异常。这主要原因是因为只开放了查询方法,其余任何修改操作都会抛出异常,以 unmodifiableList 为例查看底层实现机制:

    unmodifiableList 源码

    4 Objects

    4.1 相等判断

    Objects 有提供 equals 和 deepEquals 两个方法来进行相等判断,前者是判断基本数据类型和自定义类的,后者是用来判断数组的。底层实现如下:

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
    
    public static boolean deepEquals(Object a, Object b) {
        if (a == b)
            return true;
        else if (a == null || b == null)
            return false;
        else
            return Arrays.deepEquals0(a, b);
    }
    
    static boolean deepEquals0(Object e1, Object e2) {
        assert e1 != null;
        boolean eq;
        if (e1 instanceof Object[] && e2 instanceof Object[])
            eq = deepEquals ((Object[]) e1, (Object[]) e2);
        else if (e1 instanceof byte[] && e2 instanceof byte[])
            eq = equals((byte[]) e1, (byte[]) e2);
        else if (e1 instanceof short[] && e2 instanceof short[])
            eq = equals((short[]) e1, (short[]) e2);
        else if (e1 instanceof int[] && e2 instanceof int[])
            eq = equals((int[]) e1, (int[]) e2);
        else if (e1 instanceof long[] && e2 instanceof long[])
            eq = equals((long[]) e1, (long[]) e2);
        else if (e1 instanceof char[] && e2 instanceof char[])
            eq = equals((char[]) e1, (char[]) e2);
        else if (e1 instanceof float[] && e2 instanceof float[])
            eq = equals((float[]) e1, (float[]) e2);
        else if (e1 instanceof double[] && e2 instanceof double[])
            eq = equals((double[]) e1, (double[]) e2);
        else if (e1 instanceof boolean[] && e2 instanceof boolean[])
            eq = equals((boolean[]) e1, (boolean[]) e2);
        else
            eq = e1.equals(e2);
        return eq;
    }
    

    从源码中看出,Objects 对基本类型和复杂类型的对象,都有比较细粒度的判断。

    4.2 为空判断

    NULL

    Objects 提供了各种关于空的判断,isNull 和 nonNull 对于对象是否为空返回 Boolean,requireNonNull 方法更加严格,如果一旦为空,会直接抛出异常,我们需要根据实际场景选择使用。

    5 面试题

    5.1 工作中有没有遇到特别好用的工具类,如何写好一个工具类

    答:有的,像 Arrays 的排序、二分查找,Collections 的不可变、线程安全集合类,Objects 的空判断、相等判断等等工具类,好的工具类肯定好用,比如说使用 static final 关键字对方法进行修饰,工具类构造器必须是私有等等手段来写好工具类。

    5.2 写一个二分查找算法的实现

    答:参考 Arrays 的 binarySearch 方法的源码实现。伪代码如下:

    T[] array;
    T key;
    int low = left, high = right;
    while(low <= high) {
        int mid = low + (high - low) / 2;
        T midVal = arrary[mid];
        // 比较数组中间值和给定值 key 的大小关系:
        // 小于0则中间值小于给定值 key;
        // 大于0则中间值大于给定值 key;
        // 等于0则中间值等于给定值 key.
        int com = midVal.compareTo(key);
    
        if(com < 0) {
            low = mid + 1;
        }else if(com > 0) {
            high = mid - 1;
        }else {
            return mid;
        }
    }
    return -(low + 1);
    

    5.3 如果希望 ArrayList 初始化之后,不能被修改,该怎么办

    答:可以使用 Collections 的 unmidifiableList 的方法,该方法会返回一个不能被修改的内部类集合,这些集合只开放查询的方法,对于调用修改集合的方法会直接抛出异常。

    ------------------------------------- END -------------------------------------

    相关文章

      网友评论

          本文标题:03-Arrays、Collections、Objects 常用

          本文链接:https://www.haomeiwen.com/subject/vyfxhhtx.html