面试之敌系列 9 集合知识汇总

294 阅读5分钟

LinkedHashMap

LinkedHashMap 很像一个HashMap 加 一个双向链表,其实HashMap 用于存放数据,而双向链表维护访问顺序,这样的组合功能。在一般的情况下,HashMap的迭代访问是没没有顺序的,但是有些情况下既需要使用HashMap 的高性能,也需要使用到顺序访问的功能,因此就将两者结合起来。

  1. 首先LinkedHashMap 的put 和HashMap 的put 是一模一样的。但是!!!,LinkedHashMap 覆写了put 中会使用到的newNode方法和afterNodeInsertion方法。
    HashMap的newNode
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }
    
    LinkedHashMap的newNode
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        //从这里可以看出,对于新加入的节点,LinkedHashMap 会维护到双向链表的尾部。同时在HashMap 中也维护。
        return p;
    }
    //这是LinkedHashMap 的节点类,和HashMap的Node 不太一样。多了两个节点,用于构建双向链表。
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    //插入到尾部并同时维护双向链表的操作过程
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }
    
    

对于后面的afterNodeInsertion()

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    这个方法里面提供了一个插入一个新节点之后,头节点的改变情况,其中,removeEldestEntry(first)可以覆写,
    用于实现一些常见的操作。例如我们经常面试中被问及到的LRU算法就可以在这里实现逻辑。
    表示的是什么时候需要对头结点进行删除。LinkedHashMap是尾插法,
    但是删除的时候这里提供了头部删除逻辑,因此非常适合进行LRU 的实现。

对于accessOrder,主要就是加了一个afterNodeAccess来讲上次访问的元素移到双向链表的后面。这样就可以实现最近浏览的功能。

    欣赏一下LinkHashMap的get
    
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

    同时还有迭代器,可以看到到,此时的迭代是在一个新的LinkedEntrySet上执行的。而HashMap 是在EntrySet上执行。
    public Set<Map.Entry<K,V>> entrySet() {
            Set<Map.Entry<K,V>> es;
            return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
        }
    
    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        
        //Set迭代的实现流程
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

最后附上实现LRU的代码

    class LRUCache extends LinkedHashMap{
    private int cahceSize = 0;
    public LRUCache(int maxSize){
        super(maxSize, 0.75F, true);
        this.cahceSize = maxSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size()>cahceSize;
    }
}

一波总结

其实和HashMap是基本没有什么区别的,只是我们在操作元素的时候,额外的维护了一个双向链表来保证我们能按顺序地获取和插入元素,而且在迭代的时候实现了自己基于双向链表的跌代器。此外常考的就是实现LRU 算法,这里就是覆写removeEldestEntry()方法的判断逻辑即可。

List 之ArrayList, LinkedList 和 vector

  1. 首先这三个都是实现了List 接口,但是他们的不同主要表现在线程安全,扩容等方面。
  2. ArrayList 和vector 底层都是基于数组来实现的,而LinkedList底层是基于双向链表来做的。
  3. ArrayList是线程不安全的,LinkedList也是线程不安全的,vector 是线程安全的,线程安全的操作是在方法前面加入了synchronized 关键字。
  4. ArrayList是1.5倍扩容,LinkedList是双向链表,vector是2倍扩容。
  5. LinkedList提供了index的功能,但是需要额外的开销
  6. vector 提供了更多的元素遍历方法。

Map 之HashMap , ConcurrentHashMap, HashTable

首先,HashMap是线程不安全的,其他的chm和ht是线程安全的。其中,ht的线程安全是基于方法前面直接加synchronized的,也就是会直接将整个map 锁住,所以在数据访问大的情况下,性能会下降。为此,出现了chm,chm提供了cas加synchronized的方式来实现线程安全,在并发访问的时候只会锁住一个桶而已,不同的桶之间的并发完全可以同步进行。

1.7 和 1.8 的HashMap的改变

  1. 1.7的HashMap 在发生hash冲突的时候,采用的是单向链表的形式来插入数据的,当碰撞很严重的时候,查询的效率就变成了单向链表的效率。1.8中采用了链表加红黑树的形式来解决hash碰撞,当链表的长度大于8的时候,直接将链表树化成红黑树,这样搜索效率会提高。同时,当红黑树较小的时候,6,会反树化成链表。这是最明显的和1.7的区别
  2. 1.7是在插入前进行扩容,而1.8是插入后判断再进行扩容的。
  3. 1.7 是采用的头插法,因为JDK1.7是用单链表进行的纵向延伸,但是头插法时会容易出现逆序且环形链表死循环问题;1.8采用的是尾插法,来避免之前提到的各种问题。
  4. 扩容后新的位置的计算方式不同 rehash方式详解

1.7 和 1.8 的ConcurrentHashMap

  1. 将分段锁替换为cas+synchronized.提高并发粒度
  2. 引入了红黑树和链表组合的方式来避免索索性能的退化