【传智播客上海校区】TreeMap源码解析

案例剖析

关于Map集合的特点,我们都知道底层数据结构是控制Map集合的键,而和值即value是无关的。通过查阅JDK的API我们发现TreeMap集合的底层是基于红黑树的即二叉树。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。如果使用的构造方法是TreeMap()那么底层的键就是自然排序,如果使用的构造方法是TreeMap(Comparator<? super K> comparator) ,那么底层的键就是Comparator进行排序的。

接下来我们先看如下案例:

向TreeMap集合中存储数据,要求键为自定义Student类的对象,家庭住址作为TreeMap的值,类型是String类型。

学生Student类代码如下:

packagecn.itcast.sh.set;/** 描述学生*/publicclassStudent {//属性Stringname;intage;publicStudent(Stringname, intage) {this.name=name;this.age=age;}@OverridepublicStringtoString() {return"Student [name="+name+", age="+age+"]";}}

测试类TreeMapDemo01代码如下:

publicclassTreeMapDemo01 {publicstaticvoidmain(String[] args) {// 创建集合对象TreeMap<Student, String>tm=newTreeMap<Student,String>();//添加数据tm.put(newStudent("张三",18), "上海");tm.put(newStudent("李四",19), "北京");tm.put(newStudent("王五",18), "深圳");tm.put(newStudent("张三",18), "上海");//获取所有的键Set<Student>keys=tm.keySet();//遍历Set集合for (Studentkey : keys) {//输出数据System.out.println(key+"----"+tm.get(key));}}}

上述代码运行结果会报如下错误:

分析异常的原因:

通过以上报异常错误原因大概知道我们要将自己定义的类Student的存储到TreeMap集合中的时候,发生自定义类Student不能被转换到Comparable比较器接口的异常,我们在代码中明明没有书写和Comparable接口相关的代码啊,那怎么会报这个异常呢?

这里我们需要查看一下TreeMap底层的源代码进行进一步分析。

关于TreeMap集合的深入分析

问题1:为什么集合中存储的元素可以不重复呢?

TreeMap集合底层使用了二叉树结构,在存储元素时,拿存储的元素会和树结构中已经存在元素进行比较大小(结果有三种:大于、小于、相等)。当比较的结果相等时,表示该元素已经存在了,则不存储。

问题2:为什么TreeMap集合中存储的元素会排序呢?

在存储元素时,先拿要存储的元素和树结构中已经存在的元素进行比较大小,如果要存储的元素大于树结构中已存在的元素,则把要存储的元素存放比较元素的右边;如果要要存储的元素小于树结构中已存在的元素,则把要存储的元素放在左边。

分析TreeMap集合的源码查找为什么会报上述异常:

当上述代码中我们使用集合对象tm调用put方法向集合添加数据时,报了异常。所以我们可以查看put方法的源代码:

TreeMap集合中的put方法源代码如下所示:

publicVput(Kkey, Vvalue) {Entry<K,V>t=root;//表示二叉树的根元素if (t==null) {compare(key, key); // type (and possibly null) checkroot=newEntry<>(key, value, null);size=1;modCount++;returnnull; }intcmp;//记录比较的结果,有三种结果 大 小 相等Entry<K,V>parent;// split comparator and comparable pathsComparator<?superK>cpr=comparator;//自定义比较器对象if (cpr!=null) {//判断自定义比较器对象是否为null,由于我们在创建TreeMap集合的时候根本就没有传递自定义比较器对象,所以cpr是null,那么这里false,所以不会执行if中的代码,执行elsedo {parent=t;cmp=cpr.compare(key, t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }else {if (key==null)thrownewNullPointerException();Comparable<?superK>k= (Comparable<?superK>) key;do {parent=t;cmp=k.compareTo(t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }Entry<K,V>e=newEntry<>(key, value, parent);if (cmp<0)parent.left=e;elseparent.right=e;fixAfterInsertion(e);size++;modCount++;returnnull; }

对于上述源代码的解释:

Entry<K,V> t = root;//表示二叉树的根元素

if (t==null) {//判断根元素t是否为空,在插入第一个数据之前根元素肯定是nullcompare(key, key); // type (and possibly null) checkroot=newEntry<>(key, value, null);size=1;modCount++;returnnull; }compare(key, key);比较key,其实这里是判断key位置对象所属类即Student是否实现Comparable接口。代码如下:```javafinal int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); }

由于我们在创建TreeMap对象时并没有传递自定义比较器Comparator的对象,所以comparator等于null。这里会执行 ((Comparable<? super K>)k1).compareTo((K)k2)。这句话的意思是对两个键进行比较,按照自然排序Comparable接口中的compareTo进行排序,但是必须将传递进来的Student对象进行强转。但是Student根本就没有实现Comparable接口,所以这里会报类转换异常。为了解决上述错误,只需Student实现Comparable接口即可。

root = new Entry<>(key, value, null); 表示创建根元素对象,即将第一个元素添加到二叉树的根元素位置。

由于在创建TreeMap集合对象时并没有传递自定义比较器Comparator的对象,所以自定义比较器对象是null,执行如下代码:

else {if (key==null)//在TreeMap集合中key不能是null,只要是null就会报空指针异常thrownewNullPointerException();Comparable<?superK>k= (Comparable<?superK>) key;//将添加到集合中的key强转为Comparable类型do {parent=t;cmp=k.compareTo(t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }

cmp = k.compareTo(t.key);

t.key表示根元素的键,k表示后添加的键,按照自然排序进行比较。将返回值放到cmp变量中。

如果cmp变量小于0,将执行t = t.left;如果cmp变量大于0,将执行 t = t.right;如果cmp等于0,后添加的值将之前的值覆盖。

这里由于 t.left或者t.right初始化值都是null,所以t等于null。

执行如下代码:

Entry<K,V>e=newEntry<>(key, value, parent);if (cmp<0)parent.left=e;//将元素放到左子树elseparent.right=e;//将元素放到右子树fixAfterInsertion(e);//进行反转size++;modCount++;returnnull;


猜你喜欢

转载自blog.csdn.net/qq_39332786/article/details/79963290