黑马程序员----学习集合遇到的两个问题

来源:互联网 发布:php程序员的简历 编辑:程序博客网 时间:2024/05/20 18:45

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ------

问题1:

对于比较方法中的所谓主次要顺序问题:
例如名字为主要,年龄为次要,那么我们返回的都不过是名字相比的值或者年龄相减的值,跟主次要有什么关系呢?

原因分析:

当我们存入元素时,先跟根节点比较姓名:
如果相等,那么比较年龄确定在根节点的左侧还是右侧
如果不相等,跟左或右的节点继续比较姓名
也就是说如果跟当前元素的姓名一致,那么待添加元素距离当前元素的距离肯定比跟当前元素姓名不一致的元素要近。
例如二叉树有三个元素,分别为s1="helong",20s2="helong",18s3="helong",22其中s1为根节点,s2在左侧,s3在右侧,如果我们要添加两个元素s4="helong",17和s5="ldy",22,我们会发现添加s4时,由于姓名与根节点一样,那么它距离跟节点不会远,其实就是说同名的元素肯定挨着,而s5就不同了,由于它跟根节点不同名,因此会被移动到一侧跟那一侧的元素继续比较姓名,如果移动到了还是跟根节点同名的元素那一侧,那么依然不同名,会被再次移动,直到跟它比较的元素不是跟根节点同名为止。
这就是主要和次要条件的分别,关键在于主要条件是先考虑的,而次要条件是后考虑的
还不太明白?又或者本来明白的被我说晕了?没问题,来看张图理解一下吧!




问题2:

对于比较器方法或者compareTo方法的调用问题:

当我们向TreeSet中只添加一个元素时,会调用一次比较方法??
当我们向TreeMap中只添加一个元素时,会调用两次比较方法??
而且这种调用都是元素自己与自己的比较???


在TreeMap中添加元素的情况:

首先我们来看看put方法源代码:

public V put(K key, V value) {        <strong><span style="color:#ff0000;">Entry<K,V> t = root;        if (t == null) {            compare(key, key); // type (and possibly null) check</span></strong>            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;    }
查阅put方法源代码我们发现,即便是插入第一个元素时,也就是root==null,也就是t==null时,依然会调用compare方法

 final int compare(Object k1, Object k2) {        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)            : comparator.compare((K)k1, (K)k2);    }

该compare方法内部是使用了key的compareTo方法或者比较器的compare方法。实现了第一个元素自己与自己比较的情况,这也就解释了为什么我们只添加了一个元素,却也会调用了compare方法的问题。



下面我们来看看导致第二次调用compare或者compareTo方法的地方,get方法源代码:

public V get(Object key) {        Entry<K,V> p = <strong><span style="color:#ff0000;">getEntry(key)</span></strong>;        return (p==null ? null : p.value);    }

当我们使用keySet方法获得TreeMap中key的集合时,我们最终会通过map的get方法通过传入key来获取value,查阅get方法源代码我们发现,在get方法内部是通过调用getEntry方法来得到映射关系p的。

 final Entry<K,V> getEntry(Object key) {        // Offload comparator-based version for sake of performance        <strong><span style="color:#ff0000;">if (comparator != null)            return getEntryUsingComparator(key);</span></strong>        if (key == null)            throw new NullPointerException();        @SuppressWarnings("unchecked")            <strong><span style="color:#ff0000;">Comparable<? super K> k = (Comparable<? super K>) key;</span></strong>        Entry<K,V> p = root;        while (p != null) {            int cmp = <strong><span style="color:#ff0000;">k.compareTo(p.key)</span></strong>;            if (cmp < 0)                p = p.left;            else if (cmp > 0)                p = p.right;            else                return p;        }        return null;    }

通过get方法调用了getEntry方法,查阅getEntry方法我们发现,到此分为两种情况

1.使用自然排序的,也就是comparator==null,没有比较器传入进来的。

对于这种情况,源代码中将key转为Comparable接口对象,然后调用它的compareTo方法。

2.使用比较器排序的,也就是comparator!=null,也就是在构造函数中传入了比较器的。

对于这种情况,又调用了getEntryUsingComparator方法传入key,参看下面的getEntryUsingComparator方法源代码

final Entry<K,V> getEntryUsingComparator(Object key) {        @SuppressWarnings("unchecked")            K k = (K) key;        <strong><span style="color:#ff0000;">Comparator<? super K> cpr = comparator;</span></strong>        if (cpr != null) {            Entry<K,V> p = root;            while (p != null) {                int cmp =<strong><span style="color:#ff0000;"> cpr.compare(k, p.key);</span></strong>                if (cmp < 0)                    p = p.left;                else if (cmp > 0)                    p = p.right;                else                    return p;            }        }        return null;    }

查阅源代码我们发现,其内部最终还是调用了compare方法。


这就解释了为什么我们在TreeMap集合中添加一个元素,并用keySet方式获取时最终调用了两次compare方法,本质就是put方法时调用一次,而get方法时,由get调用getEntry方法,而getEntry方法调用了getEntryUsingComparator方法,该方法内部又调用了一次compare方法。


TreeMap中即便只是添加一个元素,也要有可比性,因为根据源码可知,它会调用compare或者compareTo方法,因此必须具有可比性。




在TreeSet中添加元素的情况:

在毕老师的视频中,只添加一个自定义元素是不会报错的,当添加多个时,最开始报错的是添加第二个元素时,这说明在当时的JDK版本下,当二叉树的根节点为null时,是不会调用自定义元素的compareTo方法的,也就不会报类型转换异常。

然而在JDK8后:
import java.util.*;class Demo{}class TreeSetTest {public static void main(String[] args) {TreeSet set=new TreeSet();<strong><span style="color:#ff0000;">set.add(new Demo());//12行</span></strong>set.add(new Demo());//13行}}


我们可以看到出错的是12行,也就是说添加第一个元素时就出错了。这与我在看毕向东老师的视频时的情况不同。

查阅add方法源码发现了问题所在:

 public boolean add(E e) {        return m.put(e, PRESENT)==null;    }
这个是TreeSet的add方法,我们发现其实它内部是调用的put方法

public V put(K key, V value) {        Entry<K,V> t = root;       <strong><span style="color:#ff0000;"> if (t == null) {            compare(key, key); // type (and possibly null) check</span></strong>            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;    }
可以看到,即便root为null,依然会调用compare方法

 final int compare(Object k1, Object k2) {        return comparator==null ? ((Comparable<? super K>)k1).<strong><span style="color:#ff0000;">compareTo</span></strong>((K)k2)            : comparator.<strong><span style="color:#ff0000;">compare</span></strong>((K)k1, (K)k2);    }
可以看到TreeMap的compare内部还是使用了compareTo或者比较器的compare方法,因此即便我们只添加一个元素,依然会报错。

而其实本质是m是一个本地的map对象,也就是说TreeSet底层其实是用TreeMap实现的,而我们知道TreeMap的put方法在即便只有一个元素时也会调用compare或者compareTo方法的,因此会报错。


根据这个我估计应该是JDK的版本问题,因为现在在使用的是JDK8,而毕老师视频中的貌似是JDK6。


三、我的心得

这是一个Java升级带来的改变,目的应该是Java升级三大目的中的提高安全性,因为原本那种只添加一个元素时可以不具备比较性的特性是没什么意义的,因为既然使用集合,那我们不可能只添加一个元素吧,所以升级改变了原来TreeMap的put方法(由于TreeSet的add方法实际是调用的TreeMap的put方法,因此TreeSet也受到了影响),使得其在添加第一个元素时,会调用元素的compareTo方法进行自己跟自己比较,其实自己跟自己比较本身没什么意义,但这句代码的意义在于确定自定义元素是否具有可比性,如果不具有就抛出异常。







-------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

0 0