集合相关的知识点

ArrayList 的扩容规则

newElementSize - elementData.size > 0 ,说明需要扩容了,否则的话不需要扩容。
一般的情况下,扩容大小为原大小的 1.5 倍 , 这里使用了一个小技巧,elementSize + (elementSize >> 1) ,>> 1 其实就是除2的操作,但是 >>1 的执行效率比较高。取比例扩容的值和实际需要的大小的最大值作为本次扩容值。

如果使用 add , 基本上使用比例扩容就可了。如果 addAll 的话,就需要使用第二个逻辑了。其实两种逻辑是写在一起的,分场景记忆一下。

fail-fast & fail-safe

fail-fast 和 fail-safe 是在检查到线程安全性问题的时候,如何处理这些问题的策略。

fail-fast 是一旦检查到线程安全性问题,就会抛异常。这个是在 ArrayList 的 Iterator 中使用到了。
fail-safe 是检查到线程安全性问题,继续执行,牺牲一下一致性。这个是在 ArrayListCopyOnwrite 中有使用。

ArrayList 的 Iterator 在遍历 ArrayList 的之前会先记录一下修改 elementData 的次数(modifyCnt),在之后的遍历过程中,如果监测到 modifyCnt 已经改变了,则说明存在线程安全的风险,然后抛出异常——ConcurrentModificationException。

这就牵扯出来一道经典的面试题了:

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer next = iterator.next();
    list.remove(next);
}

题目中,使用 iterator 遍历,然后使用 list#remove 参数删除里面的元素,这样是不行的,原因是啥呢?list 和 iterator 公用了 modCount 变量,然后 iterator 里面还有一个 expectedModCount ,每次 next ,都会检查 modCount == expectedModCount ,如果两个变量不相等,则抛出 ConcurrentModificationException 。 然后,我们在调用 list#remove 的时候,改变了 modCount 的值。在 iterator#next 被执行的时候,则 modCount 和 expectedModCount 就不相等,抛出异常了。

在 CopyOnWriteArrayList 中使用的是java.util.concurrent.CopyOnWriteArrayList.COWIterator在它的next 方法中,并没有进行安全监测。里面使用的 snapshot 是 ArrayList 中的一个副本。CopyOnWriteArrayList 中的数据有调整,也不会影响到 snapshot 中的数据,这也是 COW 的核心思想。牺牲一定的一致性。

public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}

ArrayList 和 LinkedList 的比较

首先,这两个的数结构是不相同的,前者是数组,后者是链表。然后从下面几个方面来总结他们的差异。

**随机访问的效率。**ArrayList 要优于 LinkedList 的效率,通过查看两个类的 get 方法就能知道。LinkedList 是双向链表,执行 get(5), 需要在链表的第一个或者最后一个进行遍历,这是一个 O(k) 的时间复杂度。

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;
    }
}

ArrayList 则不需要,数组 a[5] 在底层通过偏移量就能状况 a[5] 的地址。这是一个 O(1) 的时间复杂度。

随机增删的效率。

随机增删经历了两个过程,一个是定位,另外一个是增删。
插入来说,如果是插入最后位置上,两者的效率是相似的。如果是插入到中间的位置,两者各有优势吧, 前者定位起来比较方便,倒是需要移动元素的位置。后者需要移动指针定位到对应的位置,插入的时候比较方便。改变 Node 前后指针的指向就可以了。

删除和插入的效率差不多。

局部性

局部性是说的 CPU 的 L1 L2 L3 的缓存结构造成预加载机制的影响。CPU在加载数据到缓存的时候,会加载所需数据相邻的数据都加载到缓存。这都是基于一个假设,当 CPU 使用地址 A 的时候,也可能马上会用到 A + offset 中的数据,这个缓存在大多数情况下是正确的,所以 CPU 采取了这种更新策略。

基于这个原理,当我们使用链表的时候,数据可能不是连续的,这样就不能享受 CPU 缓存带来的便利了。举个例子,比说 ,a 和 b 是在一个链表中的数据,它们在逻辑上是相邻的,但是物理上隔着非常大的距离,在加载完, a 后,如果想加载 b ,很可能会把 a 的那部分缓存清理掉,然后才能加载到 b 。

内存占用

array 的定位是根据偏移量就能定位了,但是链表的话,就要使用额外的空间来存储前后指针,所以相同元素个数的 array 和链表,链表占用的内存空间比数组要大。

有序表

可以使用 TreeMap来实现。也可以使用 Arrays.sort 来排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值