• 集合学习笔记——Collection 全家桶


    Collection是我们日常开发中使用频率非常高的集合,它的主要实现有ListSet,区别是List是有序的,元素可以重复;Set是无序的,元素不可以重复,我们简单看下继承关系:

    1. List的实现类主要线程不安全的ArrayListLinkedList以及线程安全的CopyOnWriteArrayList;
    2. Set的实现类主要有线程不安全的HashSetTreeSet以及线程安全的CopyOnWriteArraySet

    个人觉得,以上集合组件类其实并不难,所以我不打算分析源码,其中HashSetTreeSet底层使用分别是HashMapTreeMap, 所以只要看下之前的文章就比较容易理解了。

    1.ArrayList VS LinkedList

    ArrayList:

    1. 底层是通过数组(内存地址连续)实现的,直接可以通过下标找到目标元素,时间复杂度是O(1),而LinkedList需要移动指针遍历每个元素,时间复杂度是O(N),所以 Arraylist查找元素的速度比Linkedlist快.
    2. Arraylist在新增和删除元素时,可能扩容和复制数组。而Linkedlist实例化对象只需要修改指针即可.
    3. Arraylist 可能会造成一定空间的浪费,因为它要预先开辟空间
    4. 默认的初始容量是10个,每次扩容为原来的1.5倍(当数组满了才会扩容,不像HashMap有扩容因子)

    LinkedList:

    1. 底层是通过线性表(链表)(内存空间不连续)来实现的,是一个双向链表
    2. LinkedList不支持高效的随机访问,它需要移动指针遍历每个元素
    3. 没有初始容量,没有扩容机制

    时间复杂度对比

    操作数组链表说明
    随机访问O(1)O(N)随机访问数组比链表快
    头部插入O(N)O(1)插入:链表快于数组,因为数组要移动其他元素的位置
    头部删除O(N)O(1)删除:链表快于数组,因为数组要移动其他元素的位置
    尾部插入O(1)O(1)插入速度一样快,但是数组有可能是触发扩容动作
    尾部删除O(1)O(1)删除速度一样快
    指定位置插入O(N)O(N)数组:在第几个元素后面插入,后面的元素需要向后移动,链表:需要先找到第几个元素,然后修改指针指向操作

    总体来说:ArrayList查询速度快,LinkedList插入删除速度快。

    使用场景

    1. 如果应用程序对数据有较多的随机访问,ArrayList性能要优于LinkedList
    2. 如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList性能要优于ArrayList
    3. 不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在ArrayList靠近末尾的地方插入,那么ArrayList只需要移动较少的数据(或者无需移动数据),此时二者耗时几乎差不多(LinkedList是双向链表,可以快速定位头尾部的节点)

    2.HashSet VS TreeSet

    HashSetTreeSet 都是元素不能重复的集合,其中TreeSet具有排序的功能。

    HashSet源码分析

    1. public class HashSet<E>
    2. extends AbstractSet<E>
    3. implements Set<E>, Cloneable, java.io.Serializable
    4. {
    5. static final long serialVersionUID = -5024744406713321676L;
    6. /**
    7. *
    8. * HashSet底层用的就是HashMap,只不过只使用了HashMap的key
    9. */
    10. private transient HashMap<E,Object> map;
    11. // Dummy value to associate with an Object in the backing Map
    12. private static final Object PRESENT = new Object();
    13. /**
    14. * 无参构造器:内部创建一个HashMap
    15. */
    16. public HashSet() {
    17. map = new HashMap<>();
    18. }
    19. /**
    20. * 参数是集合
    21. */
    22. public HashSet(Collection<? extends E> c) {
    23. map = new HashMap<>(Math.max((int) (c.size()/0.75f) + 1, 16));
    24. addAll(c);
    25. }
    26. /**
    27. * 指定容量的构造器,这个容量将传递给HashMap
    28. */
    29. public HashSet(int initialCapacity) {
    30. map = new HashMap<>(initialCapacity);
    31. }
    32. /**
    33. * add元素,发现它确实是添加到了HashMap中
    34. */
    35. public boolean add(E e) {
    36. return map.put(e, PRESENT)==null;
    37. }
    38. //.....省略其他代码
    39. }
    40. 复制代码

    不难发现,HashSet底层使用的就是HashMap,只不过是只使用了HashMapkey,根据HashMap的特点,key如果相同,则旧值覆盖新值,所以达到去重的效果。

    TreeSet源码分析:

    1. public class TreeSet<E> extends AbstractSet<E>
    2. implements NavigableSet<E>, Cloneable, java.io.Serializable
    3. {
    4. /**
    5. * 内存真正存储元素的组件:TreeMap, 只不过是只使用了key
    6. */
    7. private transient NavigableMap<E,Object> m;
    8. // Dummy value to associate with an Object in the backing Map
    9. private static final Object PRESENT = new Object();
    10. /**
    11. * 空参数构造器:其内部创建了TreeMap, 但是只使用TreeMap的key
    12. */
    13. public TreeSet() {
    14. this(new TreeMap<E,Object>());
    15. }
    16. /**
    17. * 可以传入一个比较器,这个比较器将交给TreeMap
    18. */
    19. public TreeSet(Comparator<? super E> comparator) {
    20. this(new TreeMap<>(comparator));
    21. }
    22. /**
    23. * 添加元素:将添加到TreeMap中
    24. */
    25. public boolean add(E e) {
    26. return m.put(e, PRESENT)==null;
    27. }
    28. }
    29. 复制代码

    不难发现,TreeSet底层使用的是TreeMap,只不过是只使用了TreeMapkey,根据TreeMap的特点,默认是按照自然排序(key必须要实现Comparable接口),或者指定比较器Comparator,自定义比较规则。

    3.线程安全的CopyOnWriteArrayList

    CopyOnWriteArraySet 底层使用的也是CopyOnWriteArrayList

    先简单总结下它的底层原理:

    1. CopyOnWriteArrayList内部也是通过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行(写时复制的思想)
    2. 写操作会加锁,防止并发写入造成数据丢失的问题
    3. 写操作结束后会把原数组指向新数组
    4. CopyOnWriteArrayList允许在进行写操作的同时来读取数据,大大提高了读的性能,因此适合读多写少的场景(写多读少的话,会大量复制数组,非常耗费内存),但是CopyOnWriteArrayList可能读到的数据不是最新的数据,所以不适合实时性要求高的场景(数据不一致的问题)。

    只能保证最终一致,不能保证实时一致

    我们看下源码:

    1. public class CopyOnWriteArrayList<E>
    2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    3. private static final long serialVersionUID = 8673264195747942595L;
    4. /**
    5. * 通过 ReentrantLock 来保证写时的线程安全
    6. */
    7. final transient ReentrantLock lock = new ReentrantLock();
    8. /**
    9. * 底层仍然是使用数组来存储数据
    10. */
    11. private transient volatile Object[] array;
    12. /**
    13. * 读取数据,没有加锁,直接读就可以
    14. */
    15. public E get(int index) {
    16. return get(getArray(), index);
    17. }
    18. private E get(Object[] a, int index) {
    19. return (E) a[index];
    20. }
    21. /**
    22. * 添加元素(写)
    23. *
    24. * 1.首先加锁
    25. * 2.获取原数组
    26. * 3.根据原数据复制一个新的数组
    27. * 4.然后对新数组进行写操作(这中间读的话,仍然读的是原数组)
    28. * 5.将新数组赋给原数组
    29. */
    30. public boolean add(E e) {
    31. final ReentrantLock lock = this.lock;
    32. lock.lock();
    33. try {
    34. Object[] elements = getArray();
    35. int len = elements.length;
    36. Object[] newElements = Arrays.copyOf(elements, len + 1);
    37. newElements[len] = e;
    38. setArray(newElements);
    39. return true;
    40. } finally {
    41. lock.unlock();
    42. }
    43. }
    44. /**
    45. * 删除元素(写):和 add一样
    46. *
    47. * 1.首先加锁
    48. * 2.获取原数组
    49. * 3.根据原数据复制一个新的数组
    50. * 4.然后对新数组进行写操作(这中间读的话,仍然读的是原数组)
    51. * 5.将新数组赋给原数组
    52. */
    53. public E remove(int index) {
    54. final ReentrantLock lock = this.lock;
    55. lock.lock();
    56. try {
    57. Object[] elements = getArray();
    58. int len = elements.length;
    59. E oldValue = get(elements, index);
    60. int numMoved = len - index - 1;
    61. if (numMoved == 0)
    62. setArray(Arrays.copyOf(elements, len - 1));
    63. else {
    64. Object[] newElements = new Object[len - 1];
    65. System.arraycopy(elements, 0, newElements, 0, index);
    66. System.arraycopy(elements, index + 1, newElements, index,
    67. numMoved);
    68. setArray(newElements);
    69. }
    70. return oldValue;
    71. } finally {
    72. lock.unlock();
    73. }
    74. }
    75. }
    76. 复制代码

    源码并不复杂,就是写时复制的思想,我们简单用一张图来展示下:



    好了,关于Collection集合下常用的组件就分析到这里吧,对比Map,这些就显得比较简单了,源码也很容易理解。

     

  • 相关阅读:
    继承和static关键字
    Logstash、Mysql、Elasticsearch实现数据互通
    自然语言生成技术现状调查:核心任务、应用和评估(2)
    国标视频平台搭建(七)配置https访问
    人工神经网络理论及应用pdf,人工智能的相关书籍
    数学建模 (一)赛前准备
    ubuntu 设置最大带宽
    按键中断控制实验
    两种常见矩形框旋转方法推导及其C++实现
    Linux中国开源社区停止运营
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128061893