18年冬,利用了周末时间和自己交流了很多,由于交流都是基于代码的形式,没有文字可言,最多是在代码的方法上标注了这个方法是用作什么的,别无其他文字说明,19年秋自己开始了自己公众号的第一篇文章的输出,是的,当时是国庆节假期,自己写了一篇文章。
竟然在朋友圈里面发出来了,看来我是神经了,我输出文章是为了炫耀的?不,我是为了自己将自己的思考和书写的代码有文字可表述的状态,从此我走上了书写自己技术点文章的道路上,一直到现在,有的时候自己会迷茫,会感触很多,但是自己都将思考的内容写进了自己的便签里,这也为自己后来去分享自己的思考做了一个缓存,便于快速查找自己过去的心得,这也是公众号写的一样,我喜欢分享,你喜欢阅读。
这次自己去分析了TreeSet的源码,这里就开始文章的下文吧,首先我还是按照构造函数的分析开始了。
public TreeSet() { this(new TreeMap<E,Object>()); }
每次我们创建一个TreeSet集合时,本质上就是new出了一个TreeMap()键值对集合,但是写到这里我还没有分析TreeMap的源码,但是这不影响我的分析,因为我已经分析完TreeSet集合的源码,整个的过程中没有阻塞性,所以分析TreeSet集合继续了。
首先,TreeSet集合和其它普通的集合一样,主要是用来作为数据的容器装载,集合嘛可以理解为动态扩容的数组,因为数组的内存空间是静态分配的,记得理解动态和静态分配的这个内容是在18年初,那个时候可谓是对java这门语言没有一个很高的把握,但是工作中的内容还是可以的,那个时候自己都写了java8的写法,但是时过两年后自己才去输出和分享了java8的文章,所以觉得自己想写的文章是不是拖延了很久,读过我的文章读者的就知道,我在文章中提到过你们现在看到的文章都是自己思考了很久才输出来的,因为没有一点一点时间的沉淀就没有一篇简短的技术文章输出。
接下来就是看下add()方法的分析了,这个方法其实还是蛮简单的。
public boolean add(E e) { return m.put(e, PRESENT)==null; } 这里先简单看下m是什么? private transient NavigableMap<E,Object> m;
这是一个NavigableMap接口的引用,也是TreeMap接口,这里面调用的就是treeMap的put()方法,这里在看下PRESENT是什么?
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
是的,它就是一个常量值,这样就会直接调用TreeMap的put()方法不用传动态的value值了,其实你看过HashSet的源码就会知道Set集合和TreeSet都是将元素当做Map的键,这样就会保证set容器的元素不会重复了,这也是为什么很多面试官会问到List和Set的区别。
接下来分析一下TreeSet集合中的first()方法,也就是获取TreeSet集合中第一个元素的方法,这个方法见到的很少,所以这里就分析一下。
public E first() { return m.firstKey(); }
调用map的firstKey()的方法,继续看下调用理解一下是如何获取第一个元素的,
public K firstKey() { return key(getFirstEntry()); } 步骤一: static <K> K key(Entry<K,?> e) { if (e==null) throw new NoSuchElementException(); return e.key;//返回获取到的entry节点的key,这就是set集合中的元素 } 步骤二:获取第一个节点,先判断根节点是否为null,然后判断根节点的左子树是否为空,不为空,循环获取左子树节点 然后进行返回左子树的节点 final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; if (p != null) while (p.left != null) p = p.left; return p; }
接下来就是分析TreeSet集合的最后一个元素的last()方法,所以这里就继续分析一下这个方法。
public E last() { return m.lastKey(); } 步骤一: public K lastKey() { return key(getLastEntry()); } 步骤二: static <K> K key(Entry<K,?> e) { if (e==null) throw new NoSuchElementException(); return e.key;//返回获取到的entry节点的key } 步骤三: final Entry<K,V> getLastEntry() { Entry<K,V> p = root;//首先获取根节点 if (p != null)//判断根节点是否为null while (p.right != null)//循环判断根节点的右自述节点是否为空,若不为空则循环判断 p = p.right; return p; }
继续分析TreeSet集合中如何获取元素个数的方法。
public int size() { return m.size(); } 步骤一: int size(); 步骤二: /** * Returns the number of key-value mappings in this map. * * @return the number of key-value mappings in this map */ public int size() { return size; }
上面的注释说明已经很好的解释了,就是获取TreeMap的键值对的个数。
或许你看过我写的文章,一般获取集合的元素都会有着不同的方法,比如说队列里面获取队首元素都会有着两个不同的方法。所以TreeSet集合自然提供了获取第一个元素的pollFirst()方法。
public E pollFirst() { Map.Entry<E,?> e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); } 步骤一: public Map.Entry<K,V> pollFirstEntry() { Entry<K,V> p = getFirstEntry();//这句就是获取第一个entry节点 Map.Entry<K,V> result = exportEntry(p);//这个就是获取到的entry节点 if (p != null) deleteEntry(p); return result; } 步骤二:这个就是获取左子树的节点 final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; if (p != null) while (p.left != null) p = p.left; return p; } 步骤三:这个就是获取entry节点的,这样获取到entry节点后,就可以获取entry节点的key值,就是TreeSet的元素值了 static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) { return (e == null) ? null : new AbstractMap.SimpleImmutableEntry<>(e); } 步骤四: /** * Delete node p, and then rebalance the tree. */ private void deleteEntry(Entry<K,V> p) {}
最后就是删除节点,重新构建一个红黑树,看到上面的解释了,由于篇幅的问题,这里就暂不做更多的说明了,先理解一下什么是二叉树的概念对于后面的理解很重要。
我既然分享了如何获取TreeSet集合的第一个元素的方法,自然会去分析一下如何获取TreeSet集合获取最后一个元素pollLast()方法,所以继续分析一下了。
public E pollLast() { Map.Entry<E,?> e = m.pollLastEntry();//获取最后一个entry节点 return (e == null) ? null : e.getKey();//获取entry节点的key,这就是TreeSet集合中元素存储的地方 }步骤一:public Map.Entry<K,V> pollLastEntry() { Entry<K,V> p = getLastEntry();//获取红黑树最后一个entry节点 Map.Entry<K,V> result = exportEntry(p);//获取entry节点的键值对信息 if (p != null) deleteEntry(p);//删除entry节点,重新构建红黑树 return result;//返回获取的entry节点 } 步骤二:static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) { return (e == null) ? null : new AbstractMap.SimpleImmutableEntry<>(e); } 步骤三:获取右子树节点entry final Entry<K,V> getLastEntry() { Entry<K,V> p = root; if (p != null) while (p.right != null) p = p.right; return p; } 步骤四: /** * Delete node p, and then rebalance the tree. */ private void deleteEntry(Entry<K,V> p) {}
同样,删除节点自然要重新构建红黑树,所以这里由于篇幅原因暂时不分析如何重新构建红黑树的过程。
一般我们用集合时,判断集合是否为空也是比较常用的方法,所以这里看下TreeSet集合的isEmpty()方法。
public boolean isEmpty() { return m.isEmpty(); }
接下来就是看下TreeSet集合提供的如何获取相对某个元素较小的元素或者相对某个元素较大的元素的方法了,这里先看下lower()方法
public E lower(E e) { return m.lowerKey(e); } 步骤一: public K lowerKey(K key) { return keyOrNull(getLowerEntry(key)); }
这里就着重看下下面的方法了,因为这个方法太长了,就先分析一下吧
final Entry<K,V> getLowerEntry(K key) { Entry<K,V> p = root;//将根节点赋值给临时引用p while (p != null) {//首先判断节点是否为null int cmp = compare(key, p.key);//然后比较传入的key和根节点的key进行比较 if (cmp > 0) {//若大于0,则比较右子树 if (p.right != null) p = p.right; else return p; } else {//否则比较左子树,由于左子树不一样,这里就继续了后面的步骤 if (p.left != null) { p = p.left; } else { Entry<K,V> parent = p.parent; Entry<K,V> ch = p; while (parent != null && ch == parent.left) { ch = parent; parent = parent.parent; } return parent; } } } return null;//直接返回null }
由于higher()方法就是获取右子树的节点,所以和上面的方法分析基本上一样,这里就不做分析了,自己简单看下,很好理解的。
TreeSet集合也需要判断某个元素是否存在contains()方法,这里就先分析一下contains()方法。
public boolean contains(Object o) { return m.containsKey(o); } 步骤一: public boolean containsKey(Object key) { return getEntry(key) != null; } 步骤二: final Entry<K,V> getEntry(Object key) { if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root;//获取根节点 while (p != null) { int cmp = k.compareTo(p.key);//判断key和根节点的key大小,根据这判断是继续判断左子树还是右子树 if (cmp < 0)//若小于0则直接去左子树判断 p = p.left; else if (cmp > 0)//若大于0则直接去右子树判断 p = p.right; else return p; } return null; }
上面还有一步没有分析,就是看你是否传入了比较器,若传入了比较器则按照比较器进行元素的比较,若没有传入比较器就按照了上面的分析进行了,接下下看下传入比较器是如何比较的。
final Entry<K,V> getEntryUsingComparator(Object key) { @SuppressWarnings("unchecked") K k = (K) key; Comparator<? super K> cpr = comparator;//获取比较器 if (cpr != null) { Entry<K,V> p = root; while (p != null) { int cmp = cpr.compare(k, p.key);//根据比较器的规则进行判断key值 if (cmp < 0)//进行左子树的查找 p = p.left; else if (cmp > 0)//进行右子树的查找 p = p.right; else return p; } } return null; }
这里分析到这里了,我觉得看到这里如果会Comparator和Comprable比较器的内容就更好了,如果不会,自己可以看下我写的历史文章,即使写到这里自己还没有将比较器的内容发出来,已经写好久了。
一般说到这,自己就会分析一下如何清空集合元素的clear()方法了,所以我们看下clear()方法。
public void clear() { modCount++; size = 0; root = null; }
将根节点置为null,便于GC机制的触发,使size=0,这样整个TreeSet集合的元素就清空了。
到这里自己想要分析的TreeSet集合就结束了