Java八股文——数据结构与集合(二)

元素的比较

Comparable和Comparator

在元素排序中,常用的两个接口是ComparableComparator

  • Comparable是自己和自己比,比较方法是:compareTo
public class SearchResult implements Comparable<SearchResult> {

    int relativeRatio;
    long count;
    int recentOrder;

    public SearchResult(int relativeRatio, long count) {
        this.relativeRatio = relativeRatio;
        this.count = count;
    }

    @Override
    public int compareTo(SearchResult o) {
        // 按照relativeRatio降序排序
        if (this.relativeRatio != o.relativeRatio) {
            return this.relativeRatio > o.relativeRatio ? 1 : -1;
        }
        // 按照count降序排序
        if (this.count != o.count) {
            return this.count > o.count ? 1 : -1;
        }
        return 0;
    }

    public void setRecentOrder(int recentOrder) {
        this.recentOrder = recentOrder;
    }
    
}
  • Comparator是第三方比较器,比较方法是:compare
public class SearchResultComparator implements Comparator<SearchResult> {
    @Override
    public int compare(SearchResult o1, SearchResult o2) {
        // 按照relativeRatio降序排序
        if (o1.relativeRatio != o2.relativeRatio) {
            return o1.relativeRatio > o2.relativeRatio ? 1 : -1;
        }
        if (o1.count != o2.count) {
            return o1.count > o2.count ? 1 : -1;
        }
        return 0;
    }
}

注意:无论是Comparable还是Comparator,小于的情况返回-1,等于的情况返回0,大于的情况返回1

**也可以这么理解:返回大于0的数表示当前顺序符合预期,返回小于0的数表示当前顺序不符合预期。**比如o1o2,如果o1relativeRatioo2的要大,返回1,表示当前的顺序就是先o1o2,符合按照relativeRatio降序排序。

hashCode和equals

这两个方法用来标识对象,用来判断两个对象是否相等。

顺序:先判断hashCode是否相等,不等直接判为不同;若相等,则再调用equals判断

任何时候覆写equals,都必须同时覆写hashCode

在Map和Set类集合中,自定义的对象必须覆写这两个方法,如果仅覆写equals,那么会在比较对象或者健相等的时候去调用Object中的hashCode但是Object中的hashCode的实现是默认为每一个对象生成不同的int数值,只与对象的内存地址有关

String覆写了hashCode,所以可以在自定义的hashCode()中直接调用。

在调用equals的时候,建议使用Objects.equals(Object a, Object b),可以避免NPE问题。

fail-fast机制

  • fail-fast:对集合遍历操作时的错误检测机制。这种机制出现在多线程下,当前线程会维护一个针对目标集合的计数比较器,记录已修改的次数。在进入遍历前,如果实时修改次数与已修改的次数不一样,会抛出异常。

        public static void main(String[] args) {
            List masterList = new ArrayList();
            masterList.add("one");
            masterList.add("two");
            masterList.add("three");
            masterList.add("four");
            masterList.add("five");
    
            List branchList = masterList.subList(0, 3);
    
            // 以下三行代码需要删除,否则运行时会报错
            masterList.remove(0);
            masterList.add("ten");
            masterList.clear();
    
            branchList.clear();
            branchList.add("six");
            branchList.add("seven");
            branchList.remove(0);
    
            // 输出seven
            for (Object o : branchList) {
                System.out.println(o);
            }
    
            // 输出[seven, four, five]
            System.out.println(masterList);
        }
    

    解释:

    • subList的修改会导致主列表的修改,所以最后的输出是[seven, four, five]
    • 那三行代码需要删除的原因在于:计数比较器记录的对branchList的修改次数为0次(显式调用修改方法的次数),但是对于其主列表的修改实际上也算对于子列表的修改,所以在调用branchList.clear();前实际修改次数是4次,这两者不一样,所以会在这里报错。

    建议使用Iterator机制进行遍历时修改的操作

  • fail-safe:在安全的副本上进行遍历,存在修改的时候先复制一份,在复制品上进行修改,这样集合修改与副本遍历没有任何关系。并发包的集合中都是采用这种机制实现,这种机制会导致读取不到最新的数据。常见的例如并发容器CopyOnWriteArrayList

Map类集合

面对频繁的插入和删除,红黑树更为合适;面对低频修改,大量查询时,AVL树更合适。

原因:在插入和删除后,AVL树需要向上回溯,回溯次数最差为O(logn),而红黑树回溯步长为2。由于红黑树只是大致平衡,所以高度相比于AVL可能会更高,所以查询效率较低。

TreeMap

注意:HashMap使用hashCodeequals实现去重,而TreeMap依靠ComaparableComparator来实现去重。

源码解析:

  • 类名和关键属性:
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 static final boolean RED   = false;
    private static final boolean BLACK = true;
    
    // 存储红黑结点的载体类
    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;
    }
 // ...   
}

插入新节点前的三个前提条件:

  1. 需要调整的新节点总是红色的;
  2. 如果插入新节点的父节点是黑色的,无需调整;
  3. 如果插入新节点的父节点是红色的,因为不能出现两个相邻的红色节点,所以需要进入循环判断,或重新着色,或左右旋转,最终达到约束条件后结束。

插入的整体流程:按照Key的对比往下遍历,先按照二叉查找树的特性操作,无需关心颜色与树的平衡,后续会重新着色和旋转。

  • put的源码分析:
   public V put(K key, V value) {
        // t表示当前节点,先讲root赋值给当前节点 
        Entry<K,V> t = root;
        // 当前节点为空,即为空树,新增的节点即为根节点 
        if (t == null) {
            // 检测Key是否可以比较
            compare(key, key); // type (and possibly null) check

            // 构造出新的Entry对象,根节点没有父节点
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        // cmp用于接收比较结果
        int cmp;
        Entry<K,V> parent;
        // 构造方法中置入的外部比较器
        Comparator<? super K> cpr = comparator;
        // 重点步骤:根据二叉查找树的性质,找到新节点插入的合适位置
        if (cpr != null) {
            // 根据参数Key与当前节点Key不断地进行对比
            do {
                // 当前节点赋值给父节点
                parent = t;
                // 比较输入的参数和当前节点Key的大小
                cmp = cpr.compare(key, t.key);
                // 利用二叉查找树的性质,小则往左边走,大往右边走
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                // 相等,则根据Map的性质,覆盖掉当前节点的值,由于此时没有改变树的结构,所以直接返回
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 没有指定比较器则调用自然排序的Comparable
        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对象,并把parent置入参数
        Entry<K,V> e = new Entry<>(key, value, parent);
        // 上面查找结束的时候t=null,此时parent为待插入位置的父结点
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        // 对这个节点进行着色和旋转操作,以达到平衡
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }    
  • fixAfterInsertion源码分析:
   private void fixAfterInsertion(Entry<K,V> x) {
        // 新节点一律先赋值为红色
        x.color = RED;
        
        // 新节点是根节点或者其父节点为黑色
        // 插入红色节点不会破坏性质,无须调整
        // x值的改变过程是在不断地向上游遍历(或内部调整) 直到父亲为黑色或者到达根节点
        while (x != null && x != root && x.parent.color == RED) {
            // 如果父亲是爷爷的左子节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                // 看爷爷的右子节点的颜色(右叔)
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                // 右叔为红,此时通过局部颜色调整,可以满足性质
                if (colorOf(y) == RED) {
                    // 父亲置为黑色
                    setColor(parentOf(x), BLACK);
                    // 右叔置为黑色
                    setColor(y, BLACK);
                    // 爷爷置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 爷爷成为新的节点,进入下一轮循环
                    x = parentOf(parentOf(x));
                // 右叔为黑,需要加入旋转
                } else {
                    // 如果x是父亲的右子节点,先对父亲做一次左旋操作,转化x是父亲的左子节点
                    if (x == rightOf(parentOf(x))) {
                        // 对父亲做一次左旋转操作,红色的父亲沉入左侧,父亲赋值给x
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    // 重新着色并对爷爷进行右旋操作
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            // 思路相同,操作相反
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        // 根结点始终黑色
        root.color = BLACK;
    }
  • rotateLeft源码:
    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

红黑树调整总结

1.节点的父亲是黑色的,则直接插入,不需要调整或者着色;

2.节点的父亲是红色的:

  • 叔叔是红色的,则重新着色;
  • 叔叔是黑色的,新节点是父亲的左节点,进行右旋;
  • 叔叔是黑色的,新节点是父亲的右节点,进行左旋。

HashMap

HashMap存在死链问题以及扩容数据丢失问题,所以除了局部方法或绝对线程安全的情形外,推荐使用ConcurrentHashMap,两者性能接近。

通过indexFor()计算得出来相同indexentry放在同一个哈希桶中,一般使用链表连接,后续增大可能改为红黑树。

indexFor()主要是根据keyhashCode以及table的长度计算得出下标。

默认容量大小是16,默认负载因子大小是0.75,当Map中的数量超过容量大小*负载因子时会扩容,容量大小为原来的2倍。此处的扩容先是创建2倍大小恶毒存储空间,随后将当前存在的数据复制过去,但是在并发条件下会存在死链问题以及扩容数据丢失问题。

ConcurrentHashMap

JDK8中的ConcurrentHashMap通过避免锁的使用来提高效率,因为加锁会导致性能的进一步下降。转而使用CAS作为替代,

CASCompare And Swap解决轻微冲突的多线程并发场景下使用锁造成性能损耗的一种机制。不加锁而实现操作原子化的并发编程方式,JDK8之前采用了分段锁的设计理念。

ConcurrentHashMapJDK11中相较于JDK7的修改:

  1. 取消分段锁机制,降低了冲突概率

  2. 引入红黑树结构,同一个哈希槽上的元素个数超过一定阈值后,单向链表改为红黑树结构。

    当某个槽内的元素个数增加到超过8个且table(存放数据的地方,可能会有扩容)的容量大于等于84时,由链表转为红黑树;当某个槽内的元素个数减少到6个时,由红黑树重新转回链表。

  3. 使用更加优化的方式计算集合内元素数量

    统计数量时,难以百分之百准确,在put()remove()size()中,涉及元素总数的更新和计算,彻底避免锁的使用,使用的是CAS操作。

ConcurrentHashMap中包含的各种Node

  1. Node:存储单个KV数据节点,内部有keyvaluehashnext,有以下四种子类;
  2. TreeBin:不存储实际的数据,维护对桶内红黑树的读写锁,存储对红黑树节点的引用,相当于就是控制线程对于某个槽内的读写;
  3. TreeNode:在红黑树结构中,实际存储数据的节点;
  4. ForwardingNode:扩容转发节点,在table扩容时使用。在原table槽内放置,内部记录了扩容后的table
  5. ReservationNode:占位加锁节点,当执行某些方法时,对其加锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值