TreeSet的源码分析

我们都知道TreeSet里面存到元素是有序的,他的有序取决于使用TreeSet的哪个构造器。要是以无参的构造器来构造TreeSet集合,那么这个TreeSet集合里面存的元素的类就得是实现了Comparable接口的,并实现他的compareTo方法。要是用那个带构造器的构造方法构造,那么就得自己传一个外比较器到集合里。那么为什么需要这样呢?TreeSet到底是怎么做到排序的呢?今天我们就来看看源码

先看无参的构造方法

比如我们有这样一个例子

测试类:

package com.liudashuai;

import java.util.TreeSet;

public class Demo{
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student> ts = new TreeSet<Student>();

        //创建学生对象
        Student s1 = new Student("xishi", 29);
        Student s2 = new Student("wangzhaojun", 28);
        Student s3 = new Student("diaochan", 30);
        Student s4 = new Student("yangyuhuan", 33);

        Student s5 = new Student("linqingxia",33);
        Student s6 = new Student("linqingxia",33);

        //把学生添加到集合
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);

        //遍历集合
        for (Student s : ts) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

学生类:

package com.liudashuai;
public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
         = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
         = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Student s) {
//        return 0;
//        return 1;
//        return -1;
        //按照年龄从小到大排序
        int num = this.age - s.age;
//        int num = s.age - this.age;
        //年龄相同时,按照姓名的字母顺序排序
        int num2 = num==0?.compareTo():num;
        return num2;
    }
}

执行结果是这样的:

treemap 获前三个个元素_算法

然后我们看看源码:

点击TreeSet ts = new TreeSet();进入TreeSet的无参构造器,发现在TreeSet里面的无参构造其实是直接调用了TreeMap构造器。

然后我们看TreeSet的add方法,我们点ts.add(s1);进入add方法,发现他的底层是调用了一个NavigableMap类对象的put方法,这个方法我不知道,不过没有关系,我们点击debug在ts.add(s1);这句语句处加断点,然后强制进入这个put方法,这样就会去可以查看实际运行的方法是什么样的。发现实际执行的put方法是HashMap的put方法,取证请看截图1。那么这个TreeSet执行add方法,实际上就是相当于直接执行TreeMap的put方法,把add的参数当作TreeSet的键存到TreeMap里,然后把键对应的值放一个没有用的虚拟值。这样就借助TreeMap的键不会重复的特点达到TreeSet集合中的元素不重复的效果。

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable
{	
    private transient NavigableMap<E,Object> m;//transient表示,这个变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用													//者的内存中而不会写到磁盘里进行持久化。
    private static final Object PRESENT = new Object();//这个PRESENT,相当于一个虚拟值,没有什么用的,作用是占一个位置
    ……
    public TreeSet() {
    	this(new TreeMap<E,Object>());//这个E是由new TreeSet()的时候决定的,比如上面的例子,这个E就是Student
	}
 
 	public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
 	……
}

截图一:

treemap 获前三个个元素_treemap 获前三个个元素_02

TreeMap的源码分析

我们知道TreeSet的无参构造和传一个比较器的构造方法的使用是不一样的,无参是构造是用元素实现Comparable接口后实现的compareTo方法来排序的,但是传一个比较器的话,就用那个比较器来排序的。为什么呢?我们看看源码

TreeSet的无参构造是这样的,直接调用了一个TreeMap无参的构造方法,TreeSet带构造器的构造方法是调用了TreeMap的那个需要一个比较器的构造方法

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    ……
    public TreeSet() {
    	this(new TreeMap<E,Object>());
	}
	public TreeSet(Comparator<? super E> comparator) {
    	this(new TreeMap<>(comparator));
	}
    ……
}

然后我们看看TreeMap这两个构造器的是怎样的,下面进入到TreeMap的源码。

这个TreeMap有一个comparator成员变量,要是你用无参构造,则这个comparator就是null,要是你用带比较器的构造方法,那么这个comparator就是指向那个比较器。

然后我们看看为什么用不同的构造器构造的TreeSet他们的比较规则不同,或者说,为什么TreeMap用不同构造器来构造会导致这个TreeMap的键的排序规则就不同。(其实,TreeSet用不同构造器比较的规则不同,是因为TreeSet用不同构造器是调用不同的TreeMap构造方法,不同TreeMap构造器构造TreeMap集合他排序键的规则就不同。因为TreeSet的底层就是纯纯的直接用TreeMap,所以TreeSet的排序规则就是借用了TreeMap键的排序规则。)

其实TreeSet是在调用add的时候就是先比较然后再存数据的,这样存数据的时候,就使集合有序了。TreeMap是在用put的时候先比较键然后再存数据的,所以TreeMap的键也是排序的。在上面我们知道了TreeSet的add方法实际上就是直接用TreeMap的put方法,且add传的元素之间作为TreeMap的put方法的键,值的位置用一个虚拟值来占位置。下面我们看看put的源码就知道TreeMap为什么是在put方法调用的时候先比较然后再排序的。

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{	
    private final Comparator<? super K> comparator;
    private transient Entry<K,V> root;
    public TreeMap() {
        comparator = null;
    }
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)//要是你用无参构造就是返回这个
            : comparator.compare((K)k1, (K)k2);//要是传了一个比较器来构造就返回这个构造器比较结果
    }
    //不管带比较器构造的TreeSet调用add方法还是不带参数构造的TreeSet调用add方法都是执行下面这个put方法。不管是带比较器构造的TreeMap还是不带参构造的TreeMap调用put方法也是执行下面的这个方法。
    public V put(K key, V value) {
        Entry<K, V> t = root;   //声明节点t指向root节点,不直接操作root节点。因为这个TreeMap其实就是一棵红黑树组织的树结构
        if (t == null) {       //如果根节点为空,则进行初始化。要是那个集合里面没有元素就会执行这个,有元素就执行下面的if外语句
            compare(key, key); //执行这个要是你传的第一个参数即没有实现Comparable接口,也没有外比较器就会出错,这是保证你传进来的第一个元素的键是符合要求的。且要求你可以自己和自己比较,要是自己和自己不能比较就也会出错
            root = new Entry<>(key, value, null);  //插入的节点作为root节点
            size = 1;
            modCount++;
            return null;
        }
        int cmp;           //根节点不为空,cmp为比较器的返回值,-1,0,1三个值
        Entry<K, V> parent; //记录查找过程中的父节点,用于初始化节点
        Comparator<? super K> cpr = comparator; //获取比较器,这个comparator是空,那么就用内比较器,要是有,就用这个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); //查找到key相同的键,直接将value存入并返回旧值
            } while (t != null);
        } else { //如果比较器为空则根据key的比较方式进行比较
            if (key == null) //TreeMap在未自定义比较器的时候是不支持key为空指针的
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;  //获取比较器,因为那个键是实现Comparable接口的,所以可以用这样的多态方式来接收
            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) //根据cmp的值决定插入到父节点的左边还是右边
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);  //插入后的调整,红黑树最核心的方法,这个我们就不研究了,毕竟我数据结构不是很会
        size++;
        modCount++;
        return null;
    }
}

TreeMap要是判断出添加进来的键和集合中的某个要比较的元素的键是一样的,即添加进来的元素和集合中那些会和他比较的元素(为什么说时"那些会和他比较的元素"呢?因为这个二叉树结构的TreeMap不是让每一个元素都和添加进来的元素执行比较器的比较方法的)执行比较方法时要是结果是0就表示键一样,不会添加新键,这个比较是用外比较器还是内比较器就看那个集合构造时的构造方法了。所以:HashMap是用hashCode方法和equals方法来保证键的不重复,TreeSet和TreeMap是用比较器来保证键不重复的。