「Android」 自建知识体系 - 集合篇

177 阅读15分钟

本文介绍了作为一个androider应该掌握的集合知识, 包括集合源码分析,重点方法解读以及集合的特征.

相关集合对比

ArrayList和Vector

  • Vector是线程安全的,而ArrayList线程不安全
  • Vector实现了同步,所以开销要比ArrayList大,访问速度慢
  • Vector扩容默认为2倍,ArrayList为1.5倍
  • 实现ArrayList同步可以通过Collections.sychronizedList(arrayList)得到一个线程安全得ArrayList;或者使用CopyOnWriteArrayList

ArrayList和LinkedList

  • ArrayList基于动态数组实现,LinkedList基于双向链表实现

集合解析

ArrayList

  • 底层数组实现
  • 默认长度为10
  • 只会序列化当前有元素的数组位置,并不会将数组整体序列化

1.7与1.8中的区别

  • 调用无参构造器时,1.7会创建长度为10的数组,而1.8会创建一个空数组

扩容

按照1.5倍扩容, 如当前是10, 扩容之后size变为15

如果调用addAll, 添加元素后,直接超过了1.5倍的size,则直接将size设置为添加元素后的size 如:初始size为0, 直接addAll了size为11的数组, 则ArrayList中的数组直接长度为11, 再add元素时按照1.5倍规则进行扩容

private void grow(int minCapacity) {
	// overflow-conscious code
    int oldCapacity = elementData.length;
    // 重点:扩容为原来的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

添加元素

先检测是否当前数组已经满了, 如果满了先进行扩容

  • 在数组最后添加元素
public boolean add(E e) {
	// 先去检测是否需要扩容,如果需要调用grow()方法进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
  • 在数组指定index处添加元素
public void add(int index, E element) {
	// 判断index合法性
    rangeCheckForAdd(index);
    // 检测是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将数组中的index以后的元素全部向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 将要添加的元素添加到index处
    elementData[index] = element;
    size++;
}

删除元素

  • 按照index删除元素
public E remove(int index) {
	// 判断index合法性
	rangeCheck(index);
	modCount++;
	E oldValue = elementData(index);
	int numMoved = size - index - 1;
	// 将index+1以后的数据向前移动一位到index
	if (numMoved > 0)
	    System.arraycopy(elementData, index+1, elementData, index, numMoved);
	// 将最后一位元素赋空
	elementData[--size] = null; // clear to let GC do its work
	
	return oldValue;
 }
  • 按照存储元素进行删除
public boolean remove(Object o) {
	// 检测是否当前要删除的元素是空的
	// 如果元素为null,遍历数组将第一个null元素删除
	// 如果元素不为null,遍历数组将对应元素删除
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

Fail-fast机制

modCount用来记录ArrayList结构变化次数,添加或者删除元素,或者调整内部数组的大小都会导致结构变化,在进行序列化或者迭代过程需要比较操作前后modCount是否变化,如果变化会抛出ConcurrentModificationException

Vector

与ArrayList类似,区别在于使用synchronized实现同步, 在remove,add,get的时候都会通过synchronized同步

扩容

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // capacityIncrement 是构造函数中传入的扩容值,如果不传入默认为0,则将之前容量扩容2倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

CopyOnWriteArrayList

  • 写操作是通过在一个复制的数组上进行,读操作在原数组中进行
  • 写操作需要加锁,防止并发写入出现数据丢失情况
  • 写操作结束之后将原始数据指向新的复制数组
  • remove和add场景都通过对象锁进行保证同步, get时直接获取,未实现同步

适用场景

  • 写的同时可以进行读取操作,大大提高了读取的性能,使用读多写少场景

缺陷

  • 内存占用过高 写操作需要复制新数组,是内存占用为原来的两倍
  • 数据不一致 读操作不能实时读取数据,存在写入操作未同步到数组中的情况

get

private E get(Object[] a, int index) {
    return (E) a[index];
}

remove

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

add

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

LinkedList

LinkedList实际上就是一个双向链表,可以实现队列,栈等功能,具体操作都是基于双向链表节点操作,相对简单

会记录首尾节点

add

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        // 直接添加到链表末尾
        linkLast(element);
    else
        // 获取到当前index的node节点,用于插入元素
        linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 添加新节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 更新后节点
    succ.prev = newNode;
    // 更新前节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

remove

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    // 更新前节点
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
    // 更新后节点
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    // 将当前节点置空
    x.item = null;
    size--;
    modCount++;
    return element;
}

get

获取的时候会进行一个简单的优化, 根据index和中心点进行比较, 从头或尾开始遍历查找

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

HashMap

详细解读,请参照 烟雨星空大神的HashMap底层原理

HashMap 数据结构

  • 1.7中是以数组 + 链表形式存储
  • 1.8中是以数组 + 链表(或者红黑树)存储

重要参数

  • 容量 capacity
    • 必须是2的n次幂,如果传入了不是2的n次幂的容量,则会通过tableSizeFor()方法转换成大于容量的最小2的n次幂,如传入14,容量会被设置成16
    • 最大容量: 1<<30,即2^32
  • 实际存放元素个数 size
    • 数组中元素个数
  • 数组扩容阈值 threshold
    • 数组元素size达到阈值,进行resize(),将容量扩大
  • 默认负载因子 loadFactor
    • 0.75

    负载因子选取0.75,是对空间和时间的权衡,如果太小,空间利用率低,导致频繁扩容;如果太大,hash冲突概率增高,影响效率,增加操作元素时间

  • 链表与红黑树转换阈值
    • 只有当数组容量达到64时,才会出现红黑树结构
    • 当链表长度达到8,链表会转换成红黑树
    • 当红黑树元素减少到6个时,会退化成链表
  • fail-fast机制保障 modCount

hash()

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

  • 通过key的hash值与hash值右移16位异或,保证了高16位的特性,同时将高16位也添加到计算当中,降低hash冲突概率
  • 异或运算预期值(0或1)的概率比为1:1

tableSizeFor()

该方法确保了map的容量必须是2的n次幂,实现算法是将n-1,然后将n-1 分别无符号右移1,2,4,8,16 位并与n的每一位相或确保,所有位数都能变成1,然后再 + 1就能得到大于当前值的最小2的n次幂

该保证了保证了当前容量首位后的值均为1,再加一,从而得到了翻倍的效果

put() 添加元素

  • 判断数组是否为空,如果是空则进行resize()扩容
  • 通过 (n-1)&hash确认元素在数组中bucket的位置
  • 如果当前数组中为null,表示没有元素,则直接将值添加到该位置
  • 如果当前数组中元素不是null
    • 如果当前hash值与当前元素相同,则将当前元素覆盖
    • 如果当前hash值不同,并且是红黑树结构,则将值添加到红黑树中
    • 如果当前hash值不同,并且非红黑树结构,即是链表,则便利链表,采用尾插法(JDK 1.8)将值添加到链表尾部
      • 在插入链表过程中,如果链表长度超过8,则将当前链表转换成红黑树
  • 如果发生了碰撞,需要替换新值,则将旧值返回。方法结束
  • fail-fast机制,++modCount
  • 检测当前元素个数是否超过阈值,需要扩容
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果map为null, 则直接扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 当前数组的位置为null, 则直接将元素添加到数组上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 当前数组中的元素的元素与当前元素key相同, 进行替换value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果是树节点, 则将p添加到红黑树中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 如果是链表, 遍历链表替换, 或者添加到尾部
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果当前元素大于64个, 并且当前链表超过了7个,进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到元素, 直接替换
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果不是更新,而是添加的话,检测是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize() 扩容

  • 扩容将之前的容量左移一位,达到2倍之前容量的数组,
  • 判断之前的数组是否为null,如果非null,说明之前存在元素,需要进行重新分配元素
  • 遍历旧数组
    • 如果当前数组元素非null,且元素的next节点为null,说明只有一个元素,则重新hash()和新数组容量取模,得到新下标位置即可
    • 否则如果当前是红黑树结构,则拆分红黑树,必要时可能会退化成链表
    • 否则说明当前元素是链表,将链表拆分,通过 (e.hash & oldCap) == 0 算法将链表元素分配到当前下标或者(当前下标+老数组容量)的位置

    (e.hash & oldCap) == 0 算法的核心就是,扩容之后,确定下标的hash值不变,但是由于扩容2倍原因,导致,参与计算的位数增加一位,为老数组容量的位置,所以,只需要判断老数组容量的位置如果是0,保持原位,为1将元素移动到(当前下标+老数组容量)位置

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 非空
    if (oldCap > 0) {
        // 超过最大容量, 不进行扩容, 直接将容量限制开到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 正常非空场景, 左移一位进行容量 * 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 初始情况, 并且设置了容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 什么也没有设置
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // map不为空
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 当前数组节点有值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 数组中的元素非链表
                if (e.next == null)
                    // 根据当前hash直接将元素设置为当前位置或者当前位置+ oldCap
                    newTab[e.hash & (newCap - 1)] = e;
                // 树节点处理
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 链表一分为二
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 放在原位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 放在原位值 + oldCap数组节点
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get() 获取元素

  • 检测数组不为null,并且当前下标元素不为null
  • 检测hash值是否与第一个元素相同
    • 如果不同,检测是否是红黑树,遍历红黑树
    • 如果是链表,遍历找到链表结尾
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

LinkedHashMap

  • 实质上与HashMap原理相同,额外的在HashMap的基础上添加了一个双链表来维护有序性
  • 节点除了next指向链表中的下一个节点外,额外添加了before,after节点,用于维护双链表中顺序
  • 有两种排序方法:HashMap中有accessOrder来标记排序顺序
    • 默认为false,表示维护插入顺序
    • true表示按照最近使用情况排序,即LRU算法,最近使用的放在链表尾部,最久未使用在链表头部

重点方法

实现思路与HashMap基本相同,我们重点看维护双向链表的方法,无外乎就是如何添加,删除,更新节点在数组中的位置

put,添加方法

LinkedHashMap中并未实现put方法,也就是说他使用的是他父类HashMap中的put(), 但是此时又分成两种情况,map中是否已经存在该节点,即要添加的节点,是需要新增还是更新即可,新增指的是bucket桶中并无元素或者bucket桶中有元素,但是当前桶中链表不存在该元素。

  • 新增节点情况
// HashMap#put()
if ((p = tab[i = (n - 1) & hash]) == null)
	// 我们来看一下在添加一个map中没有的数据时会创建新节点,LinkedHashMap重写了newNode()方法
	tab[i] = newNode(hash, key, value, null);

// LinkedHashMap#newNode(yuansu)
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);
	return p;
}

// LinkedHashMap#linkNodeLast()
// 我们可以很清晰的看出来他就是在操作双向链表,将添加的元素放在尾部
 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;
     }
 }

  • 更新节点情况 我们依旧从put方法下手,
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    ...
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        ...
        // e正常来讲已经添加到map中, 所以一定会不为null
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

这里面我们看到有两个方法,第一个是 afterNodeAccess() 一个是afterNodeInsertion(), 我们先一个一个看

  • afterNodeAccess HashMap中默认为空方法,LinkedHashMap中实现了该方法
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;
     }
 }
  • 简单总结来说就是
    • 如果使用的是LRU算法,则需要更新双向链表中的节点位置,即将当前节点的前后节点相连(相当于删除当前节点),并将当前节点添加到尾部;
    • 如果使用的是默认插入算法,则什么也不需要操作
  • afterNodeInsertion()
// 同样, 该方法也是LinkedHashMap中实现的HashMap的空方法, 但是removeEldestEntry() 方法默认为空,不会执行
// 可以看到该方法的作用就是将双向链表第一个元素,即最久没有使用的元素删除
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);
    }
}
get()
public V get(Object key) {
    Node<K,V> e;
    // getNode方法就是HashMap中获取节点方法,不赘述
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果是LRU排序,则调用上述说的afterNodeAccess方法, 进行更新双向链表;如果是默认插入顺序,则什么也不做
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
remove()
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
	Node<K,V>[] tab; Node<K,V> p; int n, index;
	if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
	    Node<K,V> node = null, e; K k; V v;
	    //查找要删除的节点
	    ...
	    if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
	        if (node instanceof TreeNode)
	            ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
	        else if (node == p)
	            tab[index] = node.next;
	        else
	            p.next = node.next;
	        ++modCount;
	        --size;
	        // 重点, 看这个方法
	        afterNodeRemoval(node);
	        return node;
	    }
	}
	return null;
 }

// afterNodeRemoval() 就是将当前节点在双向链表中的前后节点相连,即删除当前节点。
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

Hashtable

操作时,会通过synchronized 保证同步

hash

hash计算的方法通过hash后和当前数组取余

(hash & 0x7FFFFFFF) % tab.length

扩容

  • 容量设置为之前的2n + 1
  • 调整容量之后, 重置原数组位置时, 每次添加元素是添加到链表的头部

ArrayMap

gityuan.com/2019/01/13/…