面试官:从源码分析一下TreeSet(基于jdk1.8)

简介: 这个TreeSet其实和HashSet类似。HashSet底层是通过HashMap实现的,TreeSet其实底层也是通过TreeMap实现的。这篇文章就通过源码来分析一下TreeSet。

一、简介


TreeSet的作用是保存无重复的数据,不过还对这些数据进行了排序。TreeMap的底层是通过红黑树实现的,所以TreeSet底层也是通过红黑树实现的。TreeSet最主要的特点就是对元素进行了排序。我们对其特点进行总结一下:


(1)TreeSet是基于TreeMap的NavigableSet实现。

(2)TreeSet的元素存储在TreeMap中的key中,TreeMap的value是一个常量对象。

(3)非线程安全 。

(4)java8新增分割器spliterator() 方法。


在源码分析中我们也是基于jdk1.8来分析。下面我们就直接来看一下;


二、源码分析


1、继承关系

//继承了AbstractSet抽象类
java.lang.Object
        java.util.AbstractCollection<E>
              java.util.AbstractSet<E>
                   java.util.TreeSet<E>
//实现了NavigableSet接口:支持导航方法,比如查找
//实现了Cloneable接口:意味着它能被克隆
//实现了Serializable接口:意味着被序列化
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

2、参数变量

//存放元素的集合
private transient NavigableMap<E,Object> m;
//PRESENT就是上面NavigableMap中的Object
private static final Object PRESENT = new Object();

第一个疑问:不是说TreeSet是基于TreeMap实现的,为什么这里的参数是NavigableMap?


这是因为TreeMap实现了NavigableMap接口。


第二个疑问,既然TreeSet只使用了NavigableMap的key,那么直接使用null作为NavigableMap的value不就完了,还方面节省内存。


这个疑问就要好好说一下了,如果看过我HashSet那篇文章想必你一定很清楚了。这里可以直接跳过,如果你没看过,我们直接定位到TreeSet的增删方法。深入源码中找寻答案。

//add方法其实就是Map的put方法
public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}
//remove方法其实就是Map的remove方法
public boolean remove(Object o) {
    return m.remove(o)==PRESENT;
}

在add方法中,我们会出现两种put情况


情况1:元素e不重复,直接插入即可。

情况2:元素e重复,m.put(e, PRESENT)一看e这个key已经存在了,就会将PRESENT替换掉相应的value值。然后map返回这个value。value不等于null,于是返回false。很明显插入失败了。


但是如果我们把PRESENT替换成null呢?这时候m.put(e, PRESENT)一看e这个key已经存在了,于是map返回null。完了这时候你会发现null等于null。整个add方法返回的就是true。这不就矛盾了嘛。命名插入的是重复数据,返回的结果依然还是true。所以这里使用了PRESENT这个对象就能有效地避免这种情况。


3、构造方法


TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }
    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

这五个构造方法,基本上把HashSet的几个特性全部说完了,底层就是new一个TreeMap来实现的,我们还可以假如自己的比较器和排序器。


4、其他方法


增删方法我们已经看了其实就是通过map的put的remove方法来实现的。我们来看一些其他的常见方法


(1)返回子Set

//底层其实就是通过TreeMap的subMap()实现的
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                        E toElement,   boolean toInclusive) {
   return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                        toElement,   toInclusive));
}

(2)返回Set的头部和尾部

// 返回Set的头部,范围是:从头部到toElement。
// inclusive:是否包含toElement的标志
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
    return new TreeSet<E>(m.headMap(toElement, inclusive));
}
// 返回Set的尾部,范围是:从fromElement到结尾。
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    return new TreeSet<E>(m.tailMap(fromElement, inclusive));
}

(3)返回set中大于小于等于e的元素

// 返回Set的第一个元素
public E first() { return m.firstKey(); }
// 返回Set的最后一个元素
public E last() { return m.lastKey(); }
// 返回Set中小于e的最大元素
public E lower(E e) { return m.lowerKey(e); }
// 返回Set中小于/等于e的最大元素
public E floor(E e) { return m.floorKey(e);}
// 返回Set中大于/等于e的最小元素
public E ceiling(E e) { return m.ceilingKey(e);}
// 返回Set中大于e的最小元素
public E higher(E e) { return m.higherKey(e);}

(4)获取首尾元素并移除

// 获取第一个元素,并将该元素从TreeMap中删除。
public E pollFirst() {
    Map.Entry<E,?> e = m.pollFirstEntry();
    return (e == null)? null : e.getKey();
}
// 获取最后一个元素,并将该元素从TreeMap中删除。
public E pollLast() {
    Map.Entry<E,?> e = m.pollLastEntry();
    return (e == null)? null : e.getKey();
}

(5)读写数据

// 将TreeSet的“比较器、容量,所有的元素值”都写入到输出流中
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    s.defaultWriteObject();
    // 写入比较器
    s.writeObject(m.comparator());
    // 写入容量
    s.writeInt(m.size());
    // 写入“TreeSet中的每一个元素”
    for (Iterator i=m.keySet().iterator(); i.hasNext(); )
        s.writeObject(i.next());
}

还有一个读数据

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    // 从输入流中读取TreeSet的“比较器”
    Comparator<? super E> c = (Comparator<? super E>) s.readObject();
    TreeMap<E,Object> tm;
    if (c==null)
        tm = new TreeMap<E,Object>();
    else
        tm = new TreeMap<E,Object>(c);
    m = tm;
    int size = s.readInt();
    // 读取TreeSet的全部元素
    tm.readTreeSet(size, s, PRESENT);
}

源码很简单基本上都在这。下面我们总结一下。


对于HashSet是用Hash表来存储数据,而TreeSet是用二叉树来存储数据。 在不需要排序的时候,还是建议优先使用HashSet,因为速度更快,二叉树需要排序就免不了跳转旋转,所以速度会很慢。


感谢大家一直以来的支持,今天的文章先写到这。

相关文章
|
7月前
|
Java 编译器 API
【面试问题】JDK 和 JRE 的区别?
【1月更文挑战第27天】【面试问题】JDK 和 JRE 的区别?
|
7月前
|
存储 缓存 并行计算
【面试问题】JDK并发类库提供的线程池实现有哪些?
【1月更文挑战第27天】【面试问题】JDK并发类库提供的线程池实现有哪些?
|
7月前
|
存储 安全 Java
【JDK 源码分析】ConcurrentHashMap 底层结构
【1月更文挑战第27天】【JDK 源码分析】ConcurrentHashMap 底层结构
|
7月前
|
Java
【JDK 源码分析】HashMap 操作方法
【1月更文挑战第27天】【JDK 源码分析】HashMap Put 元素 初始化
|
7月前
|
存储 网络协议 Java
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)(二)
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)
81 0
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)(二)
|
7月前
|
Java
Integer类【JDK源码分析】
Integer类【JDK源码分析】
47 0
源码分析系列教程(12) - 手写Map框架(基于JDK1.7)
源码分析系列教程(12) - 手写Map框架(基于JDK1.7)
37 0
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
29 1
|
4月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
5月前
|
Java 编译器 程序员
JVM常见面试题(一):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别
JVM常见面试题(一):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别
JVM常见面试题(一):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别