Java 常用List集合使用场景分析

过年前的最后一篇,本章通过介绍ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底层实现原理和四个集合的区别。让你清楚明白,为什么工作中会常用ArrayList和CopyOnWriteArrayList?了解底层实现原理,我们可以学习到很多代码设计的思路,开阔自己的思维。本章通俗易懂,还在等什么,快来学习吧!

知识图解:

技术:ArrayList,LinkedList,Vector,CopyOnWriteArrayList

说明:本章基于jdk1.8,github上有ArrayList,LinkedList的简单源码代码

源码:https://github.com/ITDragonBlog/daydayup/tree/master/Java/collection-stu

知识预览

ArrayList : 基于数组实现的非线程安全的集合。查询元素快,插入,删除中间元素慢。

LinkedList : 基于链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。

Vector : 基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差。

CopyOnWriteArrayList : 基于数组实现的线程安全的写时复制集合。线程安全(ReentrantLock加锁),性能比Vector高,适合读多写少的场景。

ArrayList 和 LinkedList 读写快慢的本质

ArrayList : 查询数据快,是因为数组可以通过下标直接找到元素。 写数据慢有两个原因:一是数组复制过程需要时间,二是扩容需要实例化新数组也需要时间。

LinkedList : 查询数据慢,是因为链表需要遍历每个元素直到找到为止。 写数据快有一个原因:除了实例化对象需要时间外,只需要修改指针即可完成添加和删除元素。

本章会通过源码分析,验证上面的说法。

注:这里的块和慢是相对的。并不是LinkedList的插入和删除就一定比ArrayList快。明白其快慢的本质:ArrayList快在定位,慢在数组复制。LinkedList慢在定位,快在指针修改

ArrayList

ArrayList 是基于动态数组实现的非线程安全的集合。当底层数组满的情况下还在继续添加的元素时,ArrayList则会执行扩容机制扩大其数组长度。ArrayList查询速度非常快,使得它在实际开发中被广泛使用。美中不足的是插入和删除元素较慢,同时它并不是线程安全的。

我们可以从源码中找到答案

  1. // 查询元素
  2. public E get(int index) {
  3. rangeCheck(index); // 检查是否越界
  4. return elementData(index);
  5. }
  6. // 顺序添加元素
  7. public boolean add(E e) {
  8. ensureCapacityInternal(size + 1); // 扩容机制
  9. elementData[size++] = e;
  10. return true;
  11. }
  12. // 从数组中间添加元素
  13. public void add(int index, E element) {
  14. rangeCheckForAdd(index); // 数组下标越界检查
  15. ensureCapacityInternal(size + 1); // 扩容机制
  16. System.arraycopy(elementData, index, elementData, index + 1, size - index); // 复制数组
  17. elementData[index] = element; // 替换元素
  18. size++;
  19. }
  20. // 从数组中删除元素
  21. private void fastRemove(int index) {
  22. modCount++;
  23. int numMoved = size - index - 1;
  24. if (numMoved > 0)
  25. System.arraycopy(elementData, index+1, elementData, index, numMoved);
  26. elementData[--size] = null; // clear to let GC do its work
  27. }

从源码中可以得知,

ArrayList在执行查询操作时:

第一步:先判断下标是否越界。

第二步:然后在直接通过下标从数组中返回元素。

ArrayList在执行顺序添加操作时:

第一步:通过扩容机制判断原数组是否还有空间,若没有则重新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中。

第二步:在新数组的最后一位元素添加值。

ArrayList在执行中间插入操作时:

第一步:先判断下标是否越界。

第二步:扩容。

第三步:若插入的下标为i,则通过复制数组的方式将i后面的所有元素,往后移一位。

第四步:新数据替换下标为i的旧元素。

删除也是一样:只是数组往前移了一位,最后一个元素设置为null,等待JVM垃圾回收。

从上面的源码分析,我们可以得到一个结论和一个疑问。

结论是:ArrayList快在下标定位,慢在数组复制。

疑问是:能否将每次扩容的长度设置大点,减少扩容的次数,从而提高效率?其实每次扩容的长度大小是很有讲究的。若扩容的长度太大,会造成大量的闲置空间;若扩容的长度太小,会造成频发的扩容(数组复制),效率更低。

LinkedList

LinkedList 是基于双向链表实现的非线程安全的集合,它是一个链表结构,不能像数组一样随机访问,必须是每个元素依次遍历直到找到元素为止。其结构的特殊性导致它查询数据慢。

我们可以从源码中找到答案

  1. // 查询元素
  2. public E get(int index) {
  3. checkElementIndex(index); // 检查是否越界
  4. return node(index).item;
  5. }
  6. Node<E> node(int index) {
  7. if (index < (size >> 1)) { // 类似二分法
  8. Node<E> x = first;
  9. for (int i = 0; i < index; i++)
  10. x = x.next;
  11. return x;
  12. } else {
  13. Node<E> x = last;
  14. for (int i = size - 1; i > index; i--)
  15. x = x.prev;
  16. return x;
  17. }
  18. }
  19. // 插入元素
  20. public void add(int index, E element) {
  21. checkPositionIndex(index); // 检查是否越界
  22. if (index == size) // 在链表末尾添加
  23. linkLast(element);
  24. else // 在链表中间添加
  25. linkBefore(element, node(index));
  26. }
  27. void linkBefore(E e, Node<E> succ) {
  28. final Node<E> pred = succ.prev;
  29. final Node<E> newNode = new Node<>(pred, e, succ);
  30. succ.prev = newNode;
  31. if (pred == null)
  32. first = newNode;
  33. else
  34. pred.next = newNode;
  35. size++;
  36. modCount++;
  37. }

从源码中可以得知,

LinkedList在执行查询操作时:

第一步:先判断元素是靠近头部,还是靠近尾部。

第二步:若靠近头部,则从头部开始依次查询判断。和ArrayList的elementData(index)相比当然是慢了很多。

LinkedList在插入元素的思路:

第一步:判断插入元素的位置是链表的尾部,还是中间。

第二步:若在链表尾部添加元素,直接将尾节点的下一个指针指向新增节点。

第三步:若在链表中间添加元素,先判断插入的位置是否为首节点,是则将首节点的上一个指针指向新增节点。否则先获取当前节点的上一个节点(简称A),并将A节点的下一个指针指向新增节点,然后新增节点的下一个指针指向当前节点。

Vector

Vector 的数据结构和使用方法与ArrayList差不多。最大的不同就是Vector是线程安全的。从下面的源码可以看出,几乎所有的对数据操作的方法都被synchronized关键字修饰。synchronized是线程同步的,当一个线程已经获得Vector对象的锁时,其他线程必须等待直到该锁被释放。从这里就可以得知Vector的性能要比ArrayList低。

若想要一个高性能,又是线程安全的ArrayList,可以使用Collections.synchronizedList(list);方法或者使用CopyOnWriteArrayList集合

  1. public synchronized E get(int index) {
  2. if (index >= elementCount)
  3. throw new ArrayIndexOutOfBoundsException(index);
  4. return elementData(index);
  5. }
  6. public synchronized boolean add(E e) {
  7. modCount++;
  8. ensureCapacityHelper(elementCount + 1);
  9. elementData[elementCount++] = e;
  10. return true;
  11. }
  12. public synchronized boolean removeElement(Object obj) {
  13. modCount++;
  14. int i = indexOf(obj);
  15. if (i >= 0) {
  16. removeElementAt(i);
  17. return true;
  18. }
  19. return false;
  20. }

CopyOnWriteArrayList

在这里我们先简单了解一下CopyOnWrite容器。它是一个写时复制的容器。当我们往一个容器添加元素的时候,不是直接往当前容器添加,而是先将当前容器进行copy一份,复制出一个新的容器,然后对新容器里面操作元素,最后将原容器的引用指向新的容器。所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。

应用场景:适合高并发的读操作(读多写少)。若写的操作非常多,会频繁复制容器,从而影响性能。

CopyOnWriteArrayList 写时复制的集合,在执行写操作(如:add,set,remove等)时,都会将原数组拷贝一份,然后在新数组上做修改操作。最后集合的引用指向新数组。

CopyOnWriteArrayList 和Vector都是线程安全的,不同的是:前者使用ReentrantLock类,后者使用synchronized关键字。ReentrantLock提供了更多的锁投票机制,在锁竞争的情况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为什么CopyOnWriteArrayList的性能在大并发量的情况下优于Vector的原因。

  1. private E get(Object[] a, int index) {
  2. return (E) a[index];
  3. }
  4. public boolean add(E e) {
  5. final ReentrantLock lock = this.lock;
  6. lock.lock();
  7. try {
  8. Object[] elements = getArray();
  9. int len = elements.length;
  10. Object[] newElements = Arrays.copyOf(elements, len + 1);
  11. newElements[len] = e;
  12. setArray(newElements);
  13. return true;
  14. } finally {
  15. lock.unlock();
  16. }
  17. }
  18. private boolean remove(Object o, Object[] snapshot, int index) {
  19. final ReentrantLock lock = this.lock;
  20. lock.lock();
  21. try {
  22. Object[] current = getArray();
  23. int len = current.length;
  24. ......
  25. Object[] newElements = new Object[len - 1];
  26. System.arraycopy(current, 0, newElements, 0, index);
  27. System.arraycopy(current, index + 1, newElements, index, len - index - 1);
  28. setArray(newElements);
  29. return true;
  30. } finally {
  31. lock.unlock();
  32. }
  33. }

总结

看到这里,如果面试官问你ArrayList和LinkedList有什么区别时

如果你回答:ArrayList查询快,写数据慢;LinkedList查询慢,写数据快。面试官只能算你勉强合格。

如果你回答:ArrayList查询快是因为底层是由数组实现,通过下标定位数据快。写数据慢是因为复制数组耗时。LinkedList底层是双向链表,查询数据依次遍历慢。写数据只需修改指针引用。

如果你继续回答:ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。

因为CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。

面试官会认为你是一个基础扎实,内功深厚的人才!!!

到这里Java 常用List集合使用场景分析就结束了。过年前的最后一篇博客,有点浮躁,可能身在职场,心在老家!最后祝大家新年快乐!!!狗年吉祥!!!大富大贵!!!可能都没人看博客了 ⊙﹏⊙‖∣ 哈哈哈哈(ಡωಡ)hiahiahia

Java 常用List集合使用场景分析的更多相关文章

  1. Java 常用Set集合和常用Map集合

    目录 常用Set集合 Set集合的特点 HashSet 创建对象 常用方法 遍历 常用Map集合 Map集合的概述 HashMap 创建对象 常用方法 遍历 HashMap的key去重原理 常用Set ...

  2. JAVA常用的集合转换

    在Java应用中进行集合对象间的转换是非常常见的事情,有时候在处理某些任务时选择一种好的数据结构往往会起到事半功倍的作用,因此熟悉每种数据结构并知道其特点对于程序员来说是非常重要的,而只知道这些是不够 ...

  3. Java开发实践 集合框架 全面分析

    http://www.open-open.com/lib/view/open1474167415464.html

  4. java基础之集合:List Set Map的概述以及使用场景

    本文的整体思路以及部分文字来源:来源一 和 来源二 Java集合类的基本概念: 首先大家要明白集合为什么会出现: 在编程中,常常需要集中存放多个数据.从传统意义上讲,数组是我们的一个很好的选择,前提是 ...

  5. java 常用集合list与Set、Map区别及适用场景总结

     转载请备注出自于:http://blog.csdn.net/qq_22118507/article/details/51576319                  list与Set.Map区别及 ...

  6. JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...

  7. JAVA常用集合源码解析系列-ArrayList源码解析(基于JDK8)

    文章系作者原创,如有转载请注明出处,如有雷同,那就雷同吧~(who care!) 一.写在前面 这是源码分析计划的第一篇,博主准备把一些常用的集合源码过一遍,比如:ArrayList.HashMap及 ...

  8. (6)Java数据结构-- 转:JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析  http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ...

  9. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

随机推荐

  1. 2017ICPC/广西邀请赛1001(水)HDU6181

    A Math Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)To ...

  2. 在虚拟机(VMware)中安装Linux CentOS 6.4系统(图解) 转

    一.下载最新版本Linux CentOS     1.打开官网地址:http://www.centos.org/,点击Downloads->Mirrors         2.点击CentOS ...

  3. Android中与task相关的几个属性

    1.与任务相关的属性 taskAffinity :修改任何给定Activity的关联 系统使用包名标识应用的默认任务关联: taskAffinity属性取字符串值,必须不同于包名: taskAffin ...

  4. C++ 初始化列表(转)

    转载自:http://www.cnblogs.com/graphics/archive/2010/07/04/1770900.html 何谓初始化列表 与其他函数不同,构造函数除了有名字,参数列表和函 ...

  5. 平安E行销扫脸打卡/人寿国寿e店云参会钉钉考勤,原来这么轻易被破解!

    由于近几年人们的保险意识越来越强,身边有很多朋友都在中国人寿,中国平安等保险公司上班薪水高,工作自由,又可以学习很多保险理财的知识,每天早会个2~3个小时,剩下的基本都是自己的时间,(vx:99508 ...

  6. 【开发技术】web.xml vs struts.xml

    web.xml用来配置servlet,监听器(Listener),过滤器(filter),还有404错误跳转页面,500,等还配置欢迎页面等,总之一句话,就是系统总配置方案写在web.xml中 str ...

  7. Mybatis问题:There is no getter for property named 'unitId' in 'class java.lang.String'

    Mybatis遇到的问题 问题: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.re ...

  8. 图像插值:OpenCV_remap

    此为opencv中remap函数移植和分析,整理了双线性的插值部分的代码尚未完全移植,但最困难的部分已经完成,而恰巧在这时,发现其实现并不是那么的令我满意,于是终止,改为自己实现.考虑到以后可能会用到 ...

  9. Web Magic 简介

    WebMagic in Action Little book of WebMagic. WebMagic是我业余开发的一款简单灵活的爬虫框架.基于它你可以很容易的编写一个爬虫. 这本小书以WebMag ...

  10. TCP/IP详解 卷1 第二十章 TCP的成块数据流

    先补充一个知识: 1.停止等待协议:是tcp保证传输可靠的重要途径,"停止等待"就是指发送完一个分组就停止发送,等待对方确认之后,才能继续发送下一个分组 停止等待协议的优点是简单, ...