Java Arrays源码剖析

Java中有一个类Arrays,包含一些对数组操作的静态方法,本文主要就来讨论这些方法以避免重新造轮子,在需要的时候自己实现它不具备的功能。

toString

Arrays的toString()方法可以方便地输出一个数组的字符串形式,以便查看。它有9个重载的方法,下面列举两个常用的方法分析

public static String toString(int[] a)
public static String toString(Object[] a)

如果尝试直接输出数组,会输出元素类型@地址,使用toString后才会输出带[]格式的数组

除此之外,toString也被很多容器直接作为成员方法,方便打印查看

排序

用Java写算法题的时候,比较复杂的题目会先对输入的数组进行排序,语法上用Arrays.sort()

public static void sort(int[] a)
public static void sort(double[] a)

如果元素是对象类型,那么对象需要实现Comparable接口。

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)

Comparator就是比较器,Java中的定义如下

public interface Comparator<T> {
    int compare(T ol, T o2);
    boolean equals(Object obj);
}

最主要的是compare这个方法,它比较两个对象,返回一个表示比较结果的值,-1表示o1小于o2,0表示o1等于o2,1表示o1大于o2。排序是通过比较来实现的,sort方法在排序的过程中需要对对象进行比较的时候,就调用比较器的compare方法。

为进一步理解Comparator,我们来看下String中比较器的主要实现代码

private static class CaseInsensitiveComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        
        return n1 - n2;
    }
}

代码会逐个比较两个字符串 s1s2 中对应位置上的字符,如果发现字符不相等,则将它们转换为大写字母后再次比较。如果转换后依然不相等,则继续比较下一个字符。在循环结束后,将最后一个字符转换为小写字母后再次比较,如果仍然不相等,则返回最后一个字符的差值。如果所有字符都相等,则比较两个字符串的长度,返回长度差值。

传递比较器Comparator给sort方法,体现了程序设计中一种重要的思维方式。将不变和变化相分离,排序的基本步骤和算法是不变的,但按什么排序是变化的,sort方法将不变的算法设计为主体逻辑,而将变化的排序方式设计为参数,允许调用者动态指定,这也是一种常见的设计模式,称为策略模式,不同的排序方式就是不同的策略。

针对sort算法的底层实现,如果数组长度比较小,会采用效率较高的插入排序。当数组长度足够大时,对于基本类型的数组,Java7之前采用的算法是普通的快速排序,Java7及以后采用的算法是双枢轴快速排序(Dual-PivotQuicksort),实现代码位于类java.util.DualPivotQuicksort中。

传统的快速排序算法选择一个基准元素,然后将数组分成两部分,其中一部分包含小于等于基准元素的元素,另一部分包含大于基准元素的元素。分别对这两部分进行递归排序,最终得到有序的数组。

双枢轴快速排序在选择基准元素时,选择两个基准元素,通常是左边和右边的元素。然后将数组分成三个部分:小于第一个基准元素、介于两个基准元素之间、大于第二个基准元素。分别对这三部分进行递归排序。由于双枢轴快速排序同时处理两个基准元素,它可以更快地将数组分成更小的部分,并且在某些情况下可以减少比较和交换的次数。这使得双轴快速排序相对于传统的单轴快速排序更快。

对于对象类型,Java采用的算法是TimSort。TimSort也是在Java7引入的,在此之前,Java采用的是归并排序。TimSort实际上是对归并排序的一系列优化,位于类java.util.TimSort中。

TimSort算法的基本思路:

  • 遍历数组,识别出局部有序的run,并使用插入排序对每个run进行排序。
  • 将排序好的run按照一定规则进行归并,直到整个数组有序。

这样TimSort算法结合了归并排序和插入排序的特性,在大多数情况下能够利用数据的局部有序性。

查找

Arrays包含很多与sort对应的查找方法,可以在已排序的数组中进行二分查找。二分查找是从中间开始查找,如果小于中间元素,则在前半部分查找,否则在后半部分查找,每比较一次,要么找到,要么将查找范围缩小一半,所以查找效率非常高。

二分查找既可以针对基本类型数组,也可以针对对象数组,对对象数组,也可以传递Comparator,也可以指定查找范围。比如,针对int数组:

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

针对对象数组,需要对象拥有或者指定比较器

public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c)

二分查找的代码相信大家都非常熟悉,源码中选用左闭右开的区间

private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
                                        T key, Comparator<? super T> c) {
       int low = fromIndex;
       int high = toIndex - 1;
       while(low <= high) {
           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
}

其他方法

equals

判断两个数组是否相同,支持基本类型和对象类型。只有数组长度相同,且每个元素都相同,才返回true,否则返回false。

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

填充

Arrays包含很多fill方法,可以给数组中的每个元素设置一个相同的值:

public static void fill(int[] a, int val)

也可以给数组中一个给定范围的每个元素设置一个相同的值:

public static void fill(int[] a, int fromIndex, int toIndex, int val)

实际使用发现fill的参数不能是二维数组,只能一层一层填充

hashCode

计算hashCode的算法和String是类似的

public static int hashCode(int a[]) {
       if(a == null)
           return 0;
       int result = 1;
       for(int element : a)
           result = 31 * result + element;
       return result;
   }

代码中将每个元素的值进行累加,并且每次累加都乘以一个固定的质数。这样做的目的是为了使得不同的数组具有不同的哈希值,并且尽可能减少哈希冲突。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值