TreeSet有序和无重复值特性的内部原理剖析

此篇用以致敬:那些年,我们一起学过的TreeSet。
相信很多学过Java的小孩子们,都知道TreeSet有两大特性:来,大声喊出来

一、有序;二、值唯一

一、写作背景

有木有感觉好高大

在最初我们学习集合框架时,Collection接口以及他的宝宝List接口和Set接口一定是我们最先开始接触的。而Set接口是个神奇的接口,为什么呢?因为他有一个淘气的TreeSet。嗯嘛嘛(之所以淘气嘛,是因为博主刚开始自己学不会TreeSet,一下就掉进坑里,哼哧哼哧爬不上来,啊哈哈哈哈)

二、TreeSet的排序分析

在TreeSet中,默认是升序排序哦

public class testTreeSet {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        set.add(5);
        set.add(4);
        set.add(3);
        set.add(2);
        set.add(1);
        System.out.println(set);
    }
}

输出结果:

我们来看看,他为什么默认是升序?

(1)TreeSet是基于TreeMap实现的;

 

(2)查看TreeSet的add()方法;

在add()方法中,竟然有个m,让我们来猜猜m是什么呢?

m是NavigableMap的一个实例;

那么NavigableMap又是什么?

不难发现,TreeMap实现了NavigableMap接口

搞清楚m后,继续回到TreeMap的add()方法中,发现了put(),啊,熟悉的put,为了查看这个源码,发现源码在Map接口中,但是没有方法体,所以还是去TreeMap中去看,毕竟TreeSet还是基于TreeMap(好像分析了挺久,怎么回到了(1),不管了,继续征程。。。)

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

从put()方法中,阅读源码发现,将元素构成了一棵树,但是不允许有重复元素,
(1)先将第一个元素作为根节点,
(2)比较已存在结点和新结点的值; 大于根结点,将它置为右结点; 小于根结点值,将它置为左结点; 等于根结点值,不存储;
(3)其他元素按照步骤依次存储 以5,4,3,2,1为例,将会构造出这样一棵树:

                                  

这样就剖析结束了吗,呐,按照剧情走,还有。。。

在仔细观察put()后,发现在比较值时,使用了compare(),其中有一个对象是cpr,这个对象是一个由K值决定类型的比较器对象;稍稍解释一下,就是如果K值类型是Integer,那么就可以通过cpr调用Integer类中的compare()方法

得出结论:Integer、String等对象Object,java中已经为这些对象写了compare();

总结:通过源码我们可以发现,存入元素的时候,它创建了一个树,第一个元素就是树的根节点,后面的元素依次从树的根节点开始向后比较(创建比较器,利用comparator()方法进行比较),小的就往左边放,大的就往右边放,而相同的就不放进去(实现了唯一性)。取出元素的时候,它采用中序遍历的方法(左子树 根节点 右子树)遍历整个树,达到有序。

自定义对象的保存

那么如果一个自定义对象要存储到TreeSet集合中该怎么办呢?

假如源程序是这样的,自定义对象为person对象,测试一下:

class Person{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
}
​
public class testTreeSet {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        System.out.println(set);
    }
}

输出结果:

呀,报错了

不要着急,不要着急,休息一下,休息一下

三、TreeSet保存自定义对象时的排序分析

TreeSet保存自定义对象,有两种方式保证有序和值唯一的特性;
法一:自定义类实现Comparable接口,覆写compareTo()方法,比较规则可自己设定;
法二:构造自定义类的外部比较器类,覆写compare()方法;将比较器对象传入TreeSet中,比较规则可自己设定;

1.内部比较

自定义类自己实现了Comparable接口

Comparable接口

(1)java.lang.Comparable:内部排序接口

(2)类实现了Comparable表示此类具备可比较的性质

(3)比较方法compareTo(T o);

public int compareTo(T o) {}

返回正数:表示当前对象>目标对象

返回0:表示当前对象=目标对象

返回负数:表示当前对象<目标对象

实现内部比较的Person类如下:

class Person implements Comparable<Person>{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
​
    @Override
    public int compareTo(Person o) {
        //按照年龄排序
        //如果年龄相同,去比较姓名;不相同就返回年龄差值
        int num = this.age - o.age;
        int num1 = num == 0 ? this.name.compareTo(o.name) : num;
        return num1;
    }
}

测试一下,测试类如下:

public class testTreeSet {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        for(Person p : set){
            System.out.println("年龄为:"p.getAge()+",姓名为:"+p.getName());
        }
    }
}

测试结果:

2.外部比较器

从外部传入一个该类的比较器对象,该类的比较器类实现了Comparator接口

Comparator接口

(1)java.util.Comparator:外部排序接口(策略模式

(2)类本身不具备可比较的特性,专门有一个类来比较自定义类的大小

(3)比较方法compare(T o1,T o2);

public int compare(T o1,T o2) {}

返回正数:表示当前对象>目标对象

返回0:表示当前对象=目标对象

返回负数:表示当前对象<目标对象

实现外部比较器的Person类:

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
​
class Person{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
}
​
/*按照年龄升序排序,实现了外部比较器Comparator接口*/
class PersonAgeSec implements Comparator<Person>{
​
    @Override
    public int compare(Person o1, Person o2) {
        if(o1.getAge() > o2.getAge()){
            return 1;
        }
        if(o1.getAge() < o2.getAge()){
            return -1;
        }
        return 0;
    }
}

测试类,嗯嘛嘛,跟上面一样,哈哈哈:

public class testTreeSet {
    public static void main(String[] args) {
        Comparator p = new PersonAgeSec();
        Set<Person> set = new TreeSet<>(p);//将比较器对象传入TreeSet的构造方法中
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        for(Person p : set){
            System.out.println("年龄为:"+p.getAge()+",姓名为:"+p.getName());
        }
    }
}

测试结果:

四、其他Set接口子类重复元素判断

使用TreeSet子类进行数据保存的时候,重复元素的判断依靠的是Comparable接口完成,但这并不是全部Set接口判断重复元素的方式;在HashSet子类中,判断重复元素的方式依靠的是Object类中的两个方法;如下:

1.hash码:public native int hashCode();
2.对象比较:public boolean equals(Object obj);

在Java中进行对象比较的操作:
(1)通过一个对象的唯一编码找到一个对象的信息;
(2)编码匹配后调用equals()方法进行内容的比较;

栗子:

package com.collection.Set;
​
import java.util.*;
​
class Person1{
    private Integer age;
    private String name;
​
    public Person1(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
​
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        if(o == null || this.getClass() != o.getClass()) return false;
        Person1 person1 = (Person1) o;
        return Objects.equals(name,person1.name) &&
                Objects.equals(age,person1.age);
    }
​
    @Override
    public int hashCode(){
        return Objects.hash(name,age);
    }
}
​
​
public class setTest {
    public static void main(String[] args) {
        Set<Person1> set = new HashSet<>();
        set.add(new Person1(20,"A"));
        //重复元素
        set.add(new Person1(20,"A"));
        set.add(new Person1(23,"y"));
        set.add(new Person1(21,"B"));
        for(Person1 p : set){
            System.out.println("年龄为:"+p.getAge()+",姓名为:"+p.getName());
        }
    }
}

输出结果:

如果要想标识出对象的唯一性,一定需要equals()和hashCode()方法共同调用

结论:
(1)如果两个对象equals()相等,那么他们的hashCode必然相等;
(2)如果两个对象hashCode相等,那么他们的equals不一定相等,因为可能产生hash碰撞;

对象的判断必须两个方法equals()、hashCode()返回值都相同才判断为相同;

写的不恰当的可留言呐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章