问题描述

今天在学习JAVA的API之一——TreeSet时,对TreeSet.add()方法的具体实现存在一些疑问,本篇博客将对发现的问题进行分析。

下面的代码实现的是新建一个TreeSet并往其中添加6个自定义的Persons对象:

import java.util.Set;
import java.util.TreeSet;

public class SetLearn {
    public static void main(String[] args) {
        Set set = new TreeSet();    //新建Set
        
        //添加Persons对象
        set.add(new Persons("Andy", 18));
        set.add(new Persons("Billi", 22));
        set.add(new Persons("Cathy", 13));
        set.add(new Persons("Doggy", 25));
        set.add(new Persons("Eve", 24));
        set.add(new Persons("Frank", 23));
        
        //打印set信息
        System.out.println(set);
    }
}

因为TreeSet底层采用平衡二叉树的数据结构来存放元素,将会对存储的元素进行排序。进行排序时就需要用到TreeSet类实现接口Comparable中的compareTo()方法。而compareTo()方法的返回值为int类型,其含义如下:

        1.正数:返回正数代表存储在树的右边

        2.负数:存储在树的左边

        3.返回值为0:不存储这个元素

在自己写的Persons类中,按照人物年龄从小到大的排序方法,重写了compareTo方法。其中,添加了一行打印语句,以方便观察每次进行add()操作时compareTo()的调用情况。

@Override
    public int compareTo(Object o) {
        Persons persons = (Persons) o;
        System.out.println("this: " + this + "  input: " + o);
        return this.getAge() - persons.getAge();
    }

运行后得到如下的输出。而其中的疑问是:在输出的第一行,为什么Andy与自己比较了一次?按照个人想法如果TreeSet为空,则直接加入,不存在与自己比较的步骤。为了搞明白第一条输出语句的产生原因,决定看一下add()方法的具体实现。

this: Persons{name='Andy', age=18}  input: Persons{name='Andy', age=18}
this: Persons{name='Billi', age=22}  input: Persons{name='Andy', age=18}
this: Persons{name='Cathy', age=13}  input: Persons{name='Andy', age=18}
this: Persons{name='Doggy', age=25}  input: Persons{name='Andy', age=18}
this: Persons{name='Doggy', age=25}  input: Persons{name='Billi', age=22}
this: Persons{name='Eve', age=24}  input: Persons{name='Andy', age=18}
this: Persons{name='Eve', age=24}  input: Persons{name='Billi', age=22}
this: Persons{name='Eve', age=24}  input: Persons{name='Doggy', age=25}
this: Persons{name='Frank', age=23}  input: Persons{name='Andy', age=18}
this: Persons{name='Frank', age=23}  input: Persons{name='Eve', age=24}
this: Persons{name='Frank', age=23}  input: Persons{name='Billi', age=22}
[Persons{name='Cathy', age=13}, Persons{name='Andy', age=18}, Persons{name='Billi', age=22}, Persons{name='Frank', age=23}, Persons{name='Eve', age=24}, Persons{name='Doggy', age=25}]

输出分析

从输出的第二行开始看:

  • 当add(Billi)时,调用了compareTo()方法,将此时的根节点中存的Andy对象作为参数传入compareTo()方法,Billi 的年龄22 > Andy的年龄,所以将add()中新添加的Billi存到Andy的右子节点。
  • 当add(Doggy)时,根节点存Andy,Doggy的年龄25 > Andy的年龄;此时,Doggy再与Andy的右子节点Billi调用compareTo()方法对比,此时将Doggy存放在Billi的右子节点中。
  • 当Eve存放进去后,此时树不平衡,进行旋转,过程如下图所示:

        

java strem 分组获取第一个 java set获取第一个元素_数据结构

  • 经过旋转后树重新达到平衡,当add(Frank)时,根节点存Andy,23 > 18;Frank再与右子节点Eve比,23 < 24;Frank再与左子节点Billi比,23 > 22 ,则将其存放在Billi的右子节点上。(此时树再次达到不平衡,需要进行旋转)

代码分析

 导致输出第一行(即向一个空的TreeSet中添加第一个元素时,该元素将与自身作比较)问题的答案找到了。在一层层溯源TreeSet的add()方法后,在TreeMap类实现的put()方法中:

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;
}

也就是说,当TreeMap中根节点为空,即没有存储任何数据的时候也要进行compare(),此处的compare()中调用了接口Comparable中的compareTo()方法(没错,这个方法就是之后被重写的那个)。在compare()方法结束后,再进行存值操作(把Andy和Andy比较之后再将Andy存入)。

额外发现

(1)当发现Doggy的年龄大于Andy之后,add()又继续对比了Andy的右子节点,该实现的代码也写在TreeMap类的put()方法中:

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);

(2)当存完Frank之后,树结构再次不平衡,此时通过put()方法中的:

fixAfterInsertion(e);

实现了树的平衡。