java collectionJAVA学习之路

Java集合-TreeMap深入浅出源码分析Java8

2017-08-28  本文已影响315人  Misout

概要

在阅读本文时,强烈建议读者先学习红黑树的相关知识,推荐笔者写的上一篇文章:ConcurrentHashMap与红黑树实现分析Java8,此文中从二叉查找树说起,慢慢到红黑树,并用图文清晰描述了整个过程,清晰易懂。

本文学习知识点:

1、TreeMap的继承关系。
2、TreeMap的数据结构。
3、TreeMap的初始化。
4、TreeMap默认的comprator。
5、TreeMap中红黑树的构造。

TreeMap继承关系

TreeMap继承自AbstractMap抽象类,并实现了SortedMap接口,如下图所示:

TreeMap的类继承关系

从继承图来看,既然继承并实现了Map,那么TreeMap肯定具有和Map一样执行put,get的操作,直接通过key取value值。同时实现SortedMap,支持遍历时按元素的大小有序遍历。

TreeMap的数据结构

TreeMap采用红黑树的数据结构来实现。树节点Entry实现了Map.Entry,采用内部类的方式实现:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
    
    // 其他省略
}

节点很简单,存储了父节点,左右子节点,以及红黑颜色,元素的key以及value信息。

再来看下TreeMap中支持红黑树的数据成员:

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;
    
    // 树中元素个数
    private transient int size = 0;
    
    // 其他省略
}

因红黑树在构造过程中需要比对元素的大小来决定插入左边还是右边,因此TreeMap里面有一个比较器,可以用默认的,也可以自定义比较器。而ConcurrentHashMap采用key的hash值来比较大小,红黑树的示意图如下图:

红黑树示意图

要知道TreeMap,必须要知道TreeMap的构造过程。因此有必要知道其发展和红黑树的相关知识,这些知识已经在笔者写的ConcurrentHashMap与红黑树实现分析Java8中介绍,不再详述,请读者先看这部分知识后继续往下阅读。

TreeMap的初始化与比较器

先看默认的构造方法:

/**
 * Constructs a new, empty tree map, using the natural ordering of its
 * keys.  All keys inserted into the map must implement the {@link
 * Comparable} interface.  Furthermore, all such keys must be
 * <em>mutually comparable</em>: {@code k1.compareTo(k2)} must not throw
 * a {@code ClassCastException} for any keys {@code k1} and
 * {@code k2} in the map.  If the user attempts to put a key into the
 * map that violates this constraint (for example, the user attempts to
 * put a string key into a map whose keys are integers), the
 * {@code put(Object key, Object value)} call will throw a
 * {@code ClassCastException}.
 */
public TreeMap() {
    comparator = null;
}

根据方法的英文注释,有一下几个重点:

1、默认构造方法会创建一颗空树。
2、默认使用key的自然顺序来构建有序树,所谓自然顺序,意思是key的类型是什么,就采用该类型的compareTo方法来比较大小,决定顺序。例如key为String类型,就会用String类的compareTo方法比对大小,如果是Integer类型,就用Integer的compareTo方法比对。Java自带的基本数据类型及其装箱类型都实现了Comparable接口的compareTo方法。
3、key的类型,必须实现Comparable接口,如果不实现,就没办法完成元素大小的比较来实现有序性的。比如自定义了一个类User来作为key,忘记实现了Comparable接口,就没有一个规则比较User的大小,无法实现TreeMap最重要的有序性。

除了用key的默认比较器,TreeMap还提供了支持外部比较器来初始化构造方法:

/**
 * Constructs a new, empty tree map, ordered according to the given
 * comparator.  All keys inserted into the map must be <em>mutually
 * comparable</em> by the given comparator: {@code comparator.compare(k1,
 * k2)} must not throw a {@code ClassCastException} for any keys
 * {@code k1} and {@code k2} in the map.  If the user attempts to put
 * a key into the map that violates this constraint, the {@code put(Object
 * key, Object value)} call will throw a
 * {@code ClassCastException}.
 *
 * @param comparator the comparator that will be used to order this map.
 *        If {@code null}, the {@linkplain Comparable natural
 *        ordering} of the keys will be used.
 */
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

即在外部定义一个类,实现Compartor接口的compare方法,就可以自定义对象的比较规则。

TreeMap的构造——put方法分析

put方法构造红黑树的源码如下:

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 {// 默认key的比较器
        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;
}

从代码中也能很清晰的看出比较器选用的规则。其中方法fixAfterInsertion(e)为红黑树的性质恢复操作,因为插入节点后,可能会破坏红黑树的性质。关于红黑树的恢复原理和动作,请查看笔者写的文章:ConcurrentHashMap与红黑树实现分析Java8

get方法分析

get方法源码如下,实现非常简单,就是简单的二叉查找的过程,需要注意比较器的选择:

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        // 如果外部比较器,就采用外部比较器比对查找元素
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
        
        // 采用key的默认比较器
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

删除操作-remove分析

remove方法会调用deleteEntry方法进行删除,deleteEntry源码如下:

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    // 情况一:待删除的节点有两个孩子
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    // 情况二:待删除节点只有一个孩子
    if (replacement != null) {
        // Link replacement to parent
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // 情况三:根节点
        root = null;
    } else { //情况四:无任何孩子节点
        if (p.color == BLACK)
            fixAfterDeletion(p);

        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {// 找右子树中最小元素节点
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {// 下面这段代码根本走不到,因为deleteEntry在调用此方法时传过来的t非null
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

红黑树的删除分两步:
(1)以二叉查找树的方式删除节点。
(2)恢复红黑树的性质。

删除分为四种情况:

1、树只有根节点:直接删除即可。
2、待删除节点无孩子:直接删除即可。
3、待删除节点只有一个孩子节点:删除后,用孩子节点替换自己即可。
4、待删除节点有两个孩子:删除会复杂点,见下文介绍。

我们来看一下第4种情况,例如我们想删除如下图的节点3:

删除节点过程

删除过程:找到节点3右子树中最小的节点2,将3和2节点进行交换,然后删除3节点,3删除后,将原来的4节点变为5节点的子节点。
如果3节点和2节点被替换后,3节点下仍有两个孩子节点,重复利用上述规则删除即可。这种方式的巧妙之处在于,总是将删除的当前节点向叶子节点方向移动,保证最后没有两个孩子节点时就可以执行真正的删除了,而利用右子树的最小节点与自身交换的动作并不会破坏二叉查找树的任何特性。

上一篇下一篇

猜你喜欢

热点阅读