jdk源码学习 内外部比较器(Comparator、Compar

2019-04-10  本文已影响0人  古剑诛仙

在我对java的学习之初,经常遇到这两个接口,心里大概明白都是起个比较器的作用,但具体到使用场景的时候,尤其是阅读别人一些方法中精简的lambda表达式对这两个接口的应用的时候,感觉真是一脸懵逼。现在是时候做一份总结了~
我们先从简单的下手,就是俗称内比较器Comparable,它的源码不要太简单:

// 在官方注解里指出:1.如果指定的对象为空,则应抛出空指针异常
// 2.如果指定对象的类型禁止与本类的类型进行比较,则应抛出类型转换异常
public interface Comparable<T> {
   public int compareTo(T o);
}

首先我们发现内比较器是个泛型接口,任何一个类实现了此接口,就表明它的实例具有内在的排序关系。此时对一个对象数组 a ,排序就可以方便的使用 Arrays.sort(a); (Arrays类是功能强大的工具类,我们以后学习)来完成。同时对存储在集合中的元素类型如果实现了Comparable接口,那么对于这些元素进行搜索,计算极限值以及自动维护也变得非常简单。例如,以下程序依赖于实现了Comparable接口的String类,它的功能是将命令行参数去重,并按字母顺序打印出来:

// 在IDE里如何设置命令行参数,我常用的方式是在run含义的小绿色箭头左侧
// 有个下拉选项Edit configurations 点击然后在Program arguments里输入,以空格符隔开,run就可以啦
public static void main(String[] args) {
        Set<String> s=new TreeSet<>();
        Collections.addAll(s,args);
        System.out.println(s);

    }
// 这是书上的写法,我们做一下变形
public static void main(String[] args) {
        Set<String> s=new TreeSet<>();
        List<String> s1=new ArrayList<>();
        Collections.addAll(s,args);
        Collections.addAll(s1,args);
        System.out.println(s);
        System.out.println(s1);

    }

命令行参数如下:


image.png

结果:

[bbb, kkk, ttt, yyy]
[yyy, bbb, kkk, ttt, kkk]

有趣的是我们对两种数据结构调用了一种方法,结果表现显然不一样(Set不允许重复元素)。
为了满足好奇心,我们看一下集合工具类的源码:

// 其实Collections有好几个内部类它们也有各自的addAll方法,它们在线程安全等方面有更多的考虑,我们以后再说
// 两个参数,前者是一个集合,后者是一个T类型的可变长参数,集合元素要求是T的超类
// 因为本方法的意义在于将类型安全的元素添加到集合中去,注意collection的单个元素是key
// 是不存在value的概念的,而map的单个元素是键值对。所以无法用collections里的方法对map进行操作
// 但是可以用collections里定义的内部类里的方法来完成对map的操作
 @SafeVarargs
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)

// add方法在本类中是一个返回值为boolean类型的抽象方法,具体实现取决于具体的集合类
// 比较有趣的是迭代部分: result |= c.add(element);  其中 result的初值是false,
// 而迭代过程中一旦有一次添加成功(add返回true),result最终的值就变为了true
// 对于list集合是不在意重复元素的,每次都能添加成功,返回值为true
// 对于set集合添加重复元素是不可能的,返回值也是false
// 这样对于list每次都成功自然没什么意义,但对于set有可能成功有可能失败就起到了一个告诉我们
// 最后集合元素是否增加了的作用
            result |= c.add(element);
        return result;
    }

不改变命令行参数,我们对代码做出以下改动:

public static void main(String[] args) {
        Set<String> s=new TreeSet<>();
        List<String> s1=new ArrayList<>();
        System.out.println(Collections.addAll(s,args));
        System.out.println(Collections.addAll(s1,args));
        System.out.println(s);
        System.out.println(s1);
        System.out.println(Collections.addAll(s,args));
        System.out.println(Collections.addAll(s1,args));
        System.out.println(s);
        System.out.println(s1);

    }

结果显而易见:
true
true
[bbb, kkk, ttt, yyy]
[yyy, bbb, kkk, ttt, kkk]
false
true
[bbb, kkk, ttt, yyy]
[yyy, bbb, kkk, ttt, kkk, yyy, bbb, kkk, ttt, kkk]

我们为了能够展现 Collections.addAll 的区别选择用一个 arraylist 集合和 treeset 做对比。但细心的朋友肯定发现了list集合不是能放重复元素么,那我们实现 compareTo 方法的意义是什么?没错,如果单纯是个 list集合其实它实现不实现 Comparable 接口仅仅通过调用 Collections.addAll 是体现不出来区别的(这部分留到arraylist源码解析中的add方法详细解释)。但对于 treeset 不实现 Comparable 是绝对不可以接受的事。
原文中之所以使用 treeset,是因为 treeset 虽然是一个 set 保证其中的每个元素唯一不重复,但它其实跟普通 set 有区别,在于能够按照 compareTo 方法的具体实现完成排序(这也是为什么这个例子中按字母顺序打印)。排序的过程其实是在每次调用add方法时递归调用TreeMap的put方法实现的(注意 treeset 自己可没有put方法哦,类似于 hashset 借助于 hashmap 实现其功能一样,treeset 也偷了一次懒):

 public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
// m是什么呢,在字段定义部分我们找到了m
 private transient NavigableMap<E,Object> m;
// 原来m是被 treeset 组合进来的一个 NavigableMap 对象,而且m是在构造方法中初始化的
   public TreeSet() {
        this(new TreeMap<E,Object>());
    }
// 空参构造函数调用了一个有参构造来实现初始化
TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
// TreeMap是什么呢,我们看一下定义
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

public interface NavigableMap<K,V> extends SortedMap<K,V>
// NavigableMap扩展了 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法

// 也就是说 TreeSet 其实是通过构造函数里传入了一个 TreeMap 来实现的,而TreeMap 自然也是个有序的Map集合。那么我们看一下 TreeMap 的put方法
// 提前警告,内容比较多,有点耐心哦
public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

嗯细节问题咱们先略过,等分析treeset再认真逐行解释,在这里简单的给出 TreeMap 在每次put操作后究竟发生了什么的原理图:(图源来自网络)


image.png

从图中不难看出,只有元素类型实现了 Comparable 接口,对于每个新添加进来的元素,我们才能知道它应该放置的位置,不然也就无法实现 TreeMap 排序的功能了。当然具体细节还是挺繁琐的,我们以后再说。

那么言归正传,一旦一个类实现了Comparable接口,就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。Java 平台类库中几乎所有值类以及所有枚举类型都实现了 Comparable 接口。如果你正在编写具有明显内在排序关系(如字母顺序、数字顺序或时间顺序)的值类,则应该实现 Comparable 接口。

compareTo 方法的定义与 equals 有些类似:
将这个对象与指定的对象进行比较。当该对象小于,等于或大于时

上一篇下一篇

猜你喜欢

热点阅读