【Java杂记】工具类:数组工具类 Arrays

233 阅读5分钟

在文章开头,我们先看看工具类通用的特征:

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

我们需要注意的是,尽量不在工具方法中,对共享变量有做修改的操作访问(如果必须要做的话,必须加锁),因为会有线程安全的问题。除此之外,工具类方法本身是没有线程安全问题的,可以放心使用。

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

1.Arrays.toString()

将数组转化为字符串,也可以看做是输出当前数组所有元素。下面是 toString 方法的源码:

// 这里除了int外,Arrays还提供了 double、long、char等...
public static String toString(int[] 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(a[i]);
            // 如果数组遍历完了,加上右边],然后toString转为String
            if (i == iMax)
                return b.append(']').toString();
            b.append(", ");
        }
    }

2.Arrays.sort()

数组排序, 使用了双轴快速排序算法,效率高

  • 入参支持 int、long、double 等各种基本类型的数组
  • 同样支持自定义类的数组,但必须要有比较器
    • 实现Comparable接口,重写compareTo方法
    • 传入Comparator比较器,重写comparing方法

下面是一个自定义类型的代码示例:

	// 自定义类
    class SortDTO {
        private String sortTarget;

        public SortDTO(String sortTarget) {
            this.sortTarget = sortTarget;
        }
		
        public String getSortTarget() {
            return sortTarget;
        }

        @Override
        public String toString() {
            return "SortDTO{" +
                    "sortTarget='" + sortTarget + '\'' +
                    '}';
        }
    }

    public void testSort(){
        List<SortDTO> list = new ArrayList<SortDTO>(){
            {
                add(new SortDTO("test01"));
                add(new SortDTO("test03"));
                add(new SortDTO("test02"));
            }
        };
        // 先把数组的大小初始化成 list 的大小,保证能够正确执行 toArray
        SortDTO[] array = new SortDTO[list.size()];
        list.toArray(array);
        System.out.println("排序之前:" +  Arrays.toString(array));
        
        // sort 需要传入自定义比较器
        // Arrays.sort(array, (a, b) -> {
        //            return a.getSortTarget().compareTo(b.getSortTarget());
        //        });
        Arrays.sort(array, Comparator.comparing(SortDTO::getSortTarget));
        System.out.println("排序之后:" + Arrays.toString(array));
    }

执行结果

排序之前:[SortDTO{sortTarget='test01'}, SortDTO{sortTarget='test03'}, SortDTO{sortTarget='test02'}]
排序之后:[SortDTO{sortTarget='test01'}, SortDTO{sortTarget='test02'}, SortDTO{sortTarget='test03'}]

3.Arrays.binarySearch()

二分查找。

  • 支持的入参类型非常多,如 byte、int、long 各种类型的数组
  • 若被搜索的数组是无序的,一定要先排序,否则二分搜索很有可能搜索不到
  • 返回参数是查找到的对应数组下标的值,如果查询不到,则返回负数

下面是一个自定义类的代码示例:

public void testBinarySearch(){
        List<SortDTO> list = new ArrayList<SortDTO>(){
            {
                add(new SortDTO("100"));
                add(new SortDTO("300"));
                add(new SortDTO("500"));
                add(new SortDTO("200"));
            }
        };

        SortDTO[] array = new SortDTO[list.size()];
        list.toArray(array);
        System.out.println("搜索之前:" +  Arrays.toString(array));
		
		// 必须要先排序
        Arrays.sort(array, Comparator.comparing(SortDTO::getSortTarget));
        System.out.println("先排序,结果为:" + Arrays.toString(array));
		
		// 将排序好的数组传入,若是自定义类型还要传入比较器
        int index = Arrays.binarySearch(array, new SortDTO("200"), Comparator.comparing(SortDTO::getSortTarget));
        // 返回的index小于0表示没找到
        if(index<0){
            throw new RuntimeException("没有找到200");
        }
        System.out.println("搜索结果:" + array[index]);
    }

执行结果

搜索之前:[SortDTO{sortTarget='100'}, SortDTO{sortTarget='300'}, SortDTO{sortTarget='500'}, SortDTO{sortTarget='200'}]
先排序,结果为:[SortDTO{sortTarget='100'}, SortDTO{sortTarget='200'}, SortDTO{sortTarget='300'}, SortDTO{sortTarget='500'}]
搜索结果:SortDTO{sortTarget='200'}

接下来,我们来看下二分法底层代码的实现:

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

// 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];
        // 比较数组中间值和给定的值的大小关系
        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.
}

4.Arrays.copyOf/copyOfRange()

  • 拷贝整个数组我们可以使用 copyOf 方法,如 ArrayList#add
  • 拷贝部分我们可以使用 copyOfRange 方法,如 ArrayList#remove(除最后一个元素)

Arrays 的拷贝方法,实际上底层调用的是 System.arraycopy 这个 native 方法,如果你自己对底层拷贝方法比较熟悉的话,也可以直接使用。

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

5.Arrays.asList()

asList 的作用是将数组转换为 List

List list = Arrays.asList("a","b","c")

这里注意, Arrays.asList 在使用时有两个坑:

  • 坑 1:数组被修改后,会直接影响到新 List 的值。
  • 坑 2:不能对新 List 进行 add、remove 等操作,否则运行时会报 UnsupportedOperationException 错误。
public void testArrayToList(){
  Integer[] array = new Integer[]{1,2,3,4,5,6};
  List<Integer> list = Arrays.asList(array);

  // 坑1:修改数组的值,会直接影响原 list
  log.info("数组被修改之前,集合第一个元素为:{}",list.get(0));
  array[0] = 10;
  log.info("数组被修改之前,集合第一个元素为:{}",list.get(0));

  // 坑2:使用 add、remove 等操作 list 的方法时,
  // 会报 UnsupportedOperationException 异常
  list.add(7);
}

我们就从源码中看看这两个坑从何来:

从上图中,我们可以发现Arrays.asList 方法返回的 List 并不是 java.util.ArrayList,而是自己内部的一个静态类

  1. 该静态类没有自己创建一个数组,而是直接持有数组的引用,所以修改数组,该list就会改变
  2. 并且没有实现 add、remove 等方法,所以调用add,remove时会抛出异常

若是要变成 java.util.ArrayList 可以 new ArrayList(Arrays.asList(arr)),即 array--> Collection --> ArrayList