美文网首页
jdk源码学习 内外部比较器(Comparator、Compar

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

作者: 古剑诛仙 | 来源:发表于2019-04-10 21:36 被阅读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 有些类似:
    将这个对象与指定的对象进行比较。当该对象小于,等于或大于时

    相关文章

      网友评论

          本文标题:jdk源码学习 内外部比较器(Comparator、Compar

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