List(ArrayList、LinkedList)性能分析

Array(长度固定、无修改方法)、ArrayList(长度可变、可修改)、LinkedList(长度可变、可修改):

1.读取性能分析(get())

  • Array、ArrayList 读取性能一致,头部、中部、尾部性能一致(运行时间相差10倍以内)。

  • LinkedList 读取性能:头尾性能一致,和ArrayList 一样快,中部读取性能最差。

2.插入性能分析(add())

  • ArrayList:头部插入慢(因为需要将全部元素向后移动),尾部追加(插入)快,中间比较慢。

  • LinkedList:头尾插入快,中间插入慢(因为要先从头或尾查找到中间元素后,再进行插入)。

3.删除性能分析(remove())

  • ArrayList:头部删除慢(因为需要将全部元素向后移动),尾部删除快,中间比较慢。

  • LinkedList:头尾删除快,中间删除慢(因为要先从头或尾查找到中间元素后,再进行删除)。

4.综合性能分析

  • Array 的读取性能天下无双,ArrayList内部也是数组,读取性能和Array接近.

  • LinkedList 头尾操作(读取\添加\删除)快,中部操作最慢!

5.最佳应用

  • 如果需要大量读取操作:ArrayList / Array

  • 如果头尾频繁操作(队列\栈):LinkedList

  • 综合性能 ArrayList 最佳! 无脑使用!

6.性能测试代码

 package list;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.Random;
 
 /**
  * Array、ArrayList、LinkedList性能测试
  */
 public class Demo01 {
     public static void main(String[] args) {
         //准备实验素材
         ArrayList<Integer> list = new ArrayList<>();
         Integer[] array = new Integer[100000];
         Random random = new Random();
         for (int i = 0; i < 100000; i++) {
             int n = random.nextInt();
             list.add(n);//将数据放到集合中
             array[i] = n;//将数据放到数组中
        }
         LinkedList<Integer> linkedList = new LinkedList<>();
         linkedList.addAll(list);
         //测试get方法性能
         System.out.println("**********读取性能测试结果**********");
         //get(0)
         getTest(array,list,linkedList,0);
         //get(25000)
         getTest(array,list,linkedList,25000);
         //get(50000)
         getTest(array,list,linkedList,50000);
         //get(75000)
         getTest(array,list,linkedList,75000);
         //get(100000)
         getTest(array,list,linkedList,100000-1);
         System.out.println();
 
         System.out.println("**********添加性能测试结果**********");
         //测试add方法性能
         addTest(list,linkedList,0);
         addTest(list,linkedList,50000);
         addTest(list,linkedList,100000-1);
         System.out.println();
 
         System.out.println("**********删除性能测试结果**********");
         //测试remove方法性能
         removeTest(list,linkedList,0);
         removeTest(list,linkedList,50000);
         removeTest(list,linkedList,100000-2);
    }
 
     /**
      * 测试Array、ArrayList、LinkedList查询性能
      * @param array
      * @param list
      * @param linkedList
      * @param index
      */
     public static void getTest(Integer[] array,ArrayList<Integer> list,LinkedList<Integer> linkedList,int index){
         //long t0 = System.currentTimeMillis();//获得系统当前时间毫秒数
         long t0 = System.nanoTime();
         Integer i = array[index];
         long t1 = System.nanoTime();//获得系统当前时间纳秒数 1ms=1000000ns
         i = list.get(index);
         long t2 = System.nanoTime();
         i = linkedList.get(index);
         long t3 = System.nanoTime();
         //输出结果
         System.out.println("Array ["+index+"]:"+(t1-t0));
         System.out.println("ArrayList get["+index+"]:"+(t2-t1));
         System.out.println("LinkedList get["+index+"]:"+(t3-t2));
    }
 
     /**
      * 测试ArrayList、LinkedList添加性能
      * @param list
      * @param linkedList
      * @param index
      */
     public static void addTest(ArrayList<Integer> list,LinkedList<Integer> linkedList,int index){
         long t1 = System.nanoTime();
         list.add(index,100);
         long t2 = System.nanoTime();
         linkedList.add(index,100);
         long t3 = System.nanoTime();
         System.out.println("ArrayList add("+index+"):"+(t2-t1));
         System.out.println("LinkedList add("+index+"):"+(t3-t2));
    }
 
     /**
      * 测试ArrayList、LinkedList删除性能
      * @param list
      * @param linkedList
      * @param index
      */
     public static void removeTest(ArrayList<Integer> list,LinkedList<Integer> linkedList,int index){
         long t1 = System.nanoTime();
         list.remove(index);
         long t2 = System.nanoTime();
         linkedList.remove(index);
         long t3 = System.nanoTime();
         System.out.println("ArrayList remove("+index+"):"+(t2-t1));
         System.out.println("LinkedList remove("+index+"):"+(t3-t2));
    }
 }
 

输出结果:

 **********读取性能测试结果**********
 Array [0]:411
 ArrayList get[0]:4105
 LinkedList get[0]:5748
 Array [25000]:1232
 ArrayList get[25000]:821
 LinkedList get[25000]:367846
 Array [50000]:411
 ArrayList get[50000]:1231
 LinkedList get[50000]:899910
 Array [75000]:821
 ArrayList get[75000]:1642
 LinkedList get[75000]:138353
 Array [99999]:411
 ArrayList get[99999]:1232
 LinkedList get[99999]:821
 
 **********添加性能测试结果**********
 ArrayList add(0):131784
 LinkedList add(0):7390
 ArrayList add(50000):46802
 LinkedList add(50000):314065
 ArrayList add(99999):3695
 LinkedList add(99999):2052
 
 **********删除性能测试结果**********
 ArrayList remove(0):111258
 LinkedList remove(0):10674
 ArrayList remove(50000):48855
 LinkedList remove(50000):314065
 ArrayList remove(99998):2053
 LinkedList remove(99998):1642

7.利用Debug分析数据ArrayList结构

ArrayList:数组实现的线性表(List)

LinkedList:双向链表实现的线性表

  • Java 8 之前的LinkedList 内部是双向循环链表

  • Java 8 之后的LinkedList 内部是双向链表

测试代码:

 package list;
 
 import java.util.ArrayList;
 
 /**
  * 利用Debug研究ArrayList数据结构
  */
 public class Demo02 {
     public static void main(String[] args) {
         /**
          * Debug调试过程:
          * Debug‘Demo02’-->在list上右键-->View as-->选择Object-->list出现下三角
          * -->出现elementData、size、modCount三个参数
          */
         ArrayList<String> list = new ArrayList<>();
         list.add("一月");//默认开辟10个元素的空间
         list.add("二月");
         list.add("三月");
         list.add("四月");
         list.add("五月");
         list.add("六月");
         list.add("七月");
         list.add("八月");
         list.add("九月");
         list.add("十月");
         list.add("十一月");//第11个元素,触发扩容,扩容为原数组长度的一半:10+5=15
         list.add("十二月");
         list.add("星期一");
         list.add("星期二");
         list.add("星期三");
         list.add("星期四");//第16个元素,触发扩容,扩容为原数组长度的一半:15+15/2=22,以此类推
         list.add("星期五");
         list.add("星期六");
         list.add("星期七");
         System.out.println(list);
    }
 }
 

利用Debug进行调试:

8.手写 ArrayList

插入一个元素:

 package list;
 
 import java.util.Arrays;
 
 /**
  * 手写ArrayList
  * @param <T>
  */
 public class MyArrayList<T> {
     //声明一个Object类型的空数组
     private Object[] elementData = {};
     //声明数组长度为0
     private int size = 0;
 
     /**
      * 无参构造
      */
     public MyArrayList() {
 
    }
 
     /**
      * 确认数组容量
      * 确认当前数组时候能后继续添加数据,如果容量不够,就扩容1.5倍
      */
     private void ensureCapacity(){
         //如何判断数组是否满了?
         if(size==elementData.length){
             //旧数组
             int oldCapacity = elementData.length;
             //新数组 注意:+的优先级高于>>,需要给移位运算符加括号
             int newCapacity = oldCapacity + (oldCapacity>>1);
             //数组扩容
             elementData = Arrays.copyOf(elementData,newCapacity);
        }
    }
 
     /*
         1.第一次添加元素的时候,创建长度为10个元素的数组
         2.每次添加以后,size加1
         3.增加成功后返回true
         4.第二次添加时候按照顺序添加到数组中
         5.如果数组满了,就扩容1.5倍
      */
     public boolean add(T e){
         //如何判断第一次添加?
         if(size==0){//数组长度为0
             //新建一个长度为10的数组
             elementData = new Object[10];
        }
         //确认数组容量
         ensureCapacity();
         //添加元素
         elementData[size++] = e;
         return true;
    }
 
     /**
      * 获取指定下标的数组元素
      * @param index
      * @return
      */
     public T get(int index){
         //强制类型转换
         return (T)elementData[index];
    }
 
     /**
      * 重写toString方法
      * @return
      */
     @Override
     public String toString() {
         //1.数组长度为零时返回[]
         if(size==0){
             return "[]";
        }
         //2.数组长度不为零时,频繁修改字符串要使用StringBuilder工具类,+效果不好
         //左括号[
         StringBuilder buf = new StringBuilder("[");
         //先处理第一个元素,使后面的元素成对出现:,xxx,
         buf.append(elementData[0]);
         //从第二个元素开始遍历,直至size,如果使用elementData,会出现null值
         for (int i = 1; i < size; i++) {
             //先拼接,,再拼接第i个元素
             buf.append(",").append(elementData[i]);
        }
         //加上有括号],利用toString将结果以字符串形式输出
         return buf.append("]").toString();
    }
 
     /**
      * 在指定下标位置插入元素
      * @param index
      * @param e
      * @return
      */
     public boolean add(int index,T e){
         //1.如果数组下标小于0或者大于size,抛出数组下标越界异常
         if(index<0 || index>size) {
             throw new IndexOutOfBoundsException("index:"+index);
        }
 
         //2.如果在最后位置插入元素,就认为是追加
         if(index==size){
             return add(e);
        }
 
         //3.在中间其他位置插入元素
         //先检查容量是否够,如果不够,就先扩容再追加
         ensureCapacity();
         //指定下标元素及以后元素向右移动
         for (int i = size-1; i >= index; i--) {
             elementData[i+1] = elementData[i];
        }
         //空出指定位置,在该位置插入元素
         elementData[index] = e;
         //增加了新元素之后,size要增加1
         size++;
         return true;
    }
 
     /**
      * 删除指定下标位置元素
      * @param index
      * @return
      */
     public T remove(int index){
         return null;
    }
 
     /**
      * 获取指定元素的索引下标
      * @param e
      * @return
      */
     public int indexOf(T e){
         return 0;
    }
 }

测试手写的MyArrayList:

 package list;
 
 /**
  * 测试新建的MyArrayList
  */
 public class Demo03 {
     public static void main(String[] args) {
         /**
          * Debug调试过程:
          * Debug‘Demo02’-->在list上右键-->View as-->选择Object-->list出现下三角
          * -->出现elementData、size、modCount三个属性
          */
         MyArrayList<String> list = new MyArrayList<>();
         list.add("一月");//默认开辟10个元素的空间
         list.add("二月");
         list.add("三月");
         list.add("四月");
         list.add("五月");
         list.add("六月");
         list.add("七月");
         list.add("八月");
         list.add("九月");
         list.add("十月");
         list.add("十一月");//第11个元素,触发扩容,扩容为原数组长度的一半:10+5=15
         list.add("十二月");
         list.add("星期一");
         list.add("星期二");
         list.add("星期三");
         list.add("星期四");//第16个元素,触发扩容,扩容为原数组长度的一半:15+15/2=22,以此类推
         list.add("星期五");
         list.add("星期六");
         list.add("星期七");
         list.add(0,"小超超");//开始位置插入元素
         list.add(10,"刘壮实");//中间位置插入元素
         list.add(21,"光头强");//最后位置插入元素
         //list.add(-1,"熊大");//数组下标为负,下标越界异常
         //list.add(25,"熊二");//数组下标大于数组长度,下标越界异常
         System.out.println(list);
    }
 }
 

9.利用Debug分析LinkedList结构

 package list;
 
 import java.util.LinkedList;
 
 public class Demo04 {
     public static void main(String[] args) {
         /**
          * Debug调试过程:
          * Debug‘Demo02’-->在list上右键-->View as-->选择Object-->list出现下三角
          * -->出现size、first、last、modCount四个属性
          */
         LinkedList<String> list = new LinkedList<>();
         list.add("星期一");
         list.add("星期二");
         list.add("星期三");
         list.add("星期四");
         list.add("星期五");
         list.add("星期六");
         list.add("星期七");
         System.out.println(list);
    }
 }

调试过程:

10.手写 LinkedList

添加元素:

获取元素:

重写toString方法:

 package list;
 
 /**
  * 手动创建LinkedList
  * @param <T>
  */
 public class MyLinkedList<T> {
     //前后对象
     private Node first;
     private Node last;
     //元素个数
     private int size;
 
     /**
      * 内部类Node
      * @param <T>
      */
     private static class Node<T>{
         //前后节点
         T item;
         private Node next;
         private Node prev;
 
         /**
          * 构造方法
          * 初始化item
          * @param e
          */
         public Node(T e){
             item = e;
        }
    }
 
     /**
      * 添加元素
      * @param e
      * @return
      */
     public boolean add(T e){
         //1.没有元素时候,增加第一个节点(Node)
         Node node = new Node(e);
         if(size==0){
             //增加一个元素的情况:item=e、first=node、node=node
             first = node;
             last = node;
        }else{
             //2.如果有Node,就添加到最后
             last.next = node;
             node.prev = last;
             last = node;
        }
         //元素个数加1
         size++;
         return true;
    }
 
     /**
      * 获取指定位置元素
      * @param index
      * @return
      */
     public T get(int index){
         //下标不能小于0或者大于元素总长度
         if(index<0 || index>=size){
             throw new IndexOutOfBoundsException("index="+index);
        }
         //找到要插入的节点
         Node node = findNode(index);
         return (T)node.item;
    }
 
     /**
      * 查找指定下标位置的节点
      * @param index
      * @return
      */
     private Node findNode(int index){
         //声明node
         Node node;
         //判断长度的一半属于左侧或者右侧
         if(index<(size>>1)){//左侧
             node = first;
             for (int i = 0; i != index; i++) {
                 node = node.next;
            }
        }else{//右侧
             node = last;
             for (int i = size-1; i != index; i--) {
                 node = node.prev;
            }
        }
         return node;
    }
 
     /**
      * 在指定下标位置插入元素
      * @param index
      * @param e
      * @return
      */
     public boolean add(int index,T e){
         //在尾部追加元素
         if(index == size){
             return add(e);
        }
         //下标小于0或者大于元素总个数,抛出数组下标越界异常
         if(index<0 || index>size){
             throw new IndexOutOfBoundsException("index:"+index);
        }
         Node newNode = new Node(e);
         //在头部插入元素
         if(index == 0){
             newNode.next = first;
             first.prev = newNode;
             first = newNode;
        }else {//中间位置
             //查找指定索引下标元素
             Node node = findNode(index);
             //建立前后节点引用关系
             Node prev = node.prev;
             prev.next = newNode;
             newNode.next = node;
             node.prev = newNode;
             newNode.prev = prev;
        }
         //元素个数+1
         size++;
         return true;
    }
 
     /**
      * 重写toString方法
      * @return
      */
     @Override
     public String toString() {
         //元素个数为0
         if(size == 0){
             return "[]";
        }
         //进行字符串拼接,先拼接[
         StringBuilder buf = new StringBuilder("[");
         //先拼接第一个元素
         buf.append(first.item);
         //指向第二个元素
         Node node = first.next;
         while (node!=null){//进行后续拼接
             buf.append(", ").append(node.item);
             //node移位
             node = node.next;
        }
         //拼接最后的],以字符串形式返回
         return buf.append("]").toString();
    }
 }

测试代码:

 package list;
 
 /**
  * 测试MyLinkedList
  */
 public class Demo05 {
     public static void main(String[] args) {
         MyLinkedList<String> list = new MyLinkedList<>();
         list.add("星期一");
         list.add("星期二");
         list.add("星期三");
         list.add("星期四");
         list.add("星期五");
         list.add("星期六");
         list.add("星期七");
         System.out.println(list);
         list.add(0,"光头强");
         System.out.println(list);
         System.out.println(list.get(1));
    }
 }

输出结果:

[星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期七] [光头强, 星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期七] 星期一

11.List 的并发访问问题

  • ArrayList 和 LinkedList 是线程不安全的集合, 适合单线程使用;

  • Vector是早期Java提供的List, 所有方法都是同步方法, 线程安全的.

  • 在Collections类上提供了包裹方法,可以将List转换为线程安全的List

     List list = Collections.synchronizedList(new ArrayList());
    • 但当迭代时候更新集合时,不能彻底解决线程安全性问题:

  • Java 5 提供了CopyOnWriteArrayList 专门为解决高并发情况下使用线程安全的集合问题.

    • CopyOnWriteArrayList 在修改时候, 会创建数组的副本, 在修改完成以后, 会将数组进行替换, 方法都有锁, 线程安全.

    • 由于修改时候需要复制, 会占用比较多内存, 速度会慢一些!

  • foreach方法是并发安全的: foreach方法会先将集合数据复制一份, 然后再遍历集合的副本!

测试案例:

 package list;
 
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * 测试List并发访问问题
  */
 public class Demo06 {
     public static void main(String[] args) {
         /*
          * 在集合遍历期间更新集合存在线程并发安全问题
          * 运行时抛出java.util.ConcurrentModificationException异常
          */
 //       ArrayList<String> list = new ArrayList<>();//存在上述异常
 //       List<String> list = Collections.synchronizedList(new ArrayList<>());//仍旧不能彻底解决问题
 //       Vector<String> list = new Vector<>();//仍旧不能彻底解决问题
         CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();//可以解决问题,但占用内存大,速度降低
         list.add("星期一");
         list.add("星期二");
         list.add("星期三");
         list.add("星期四");
         list.add("星期五");
         list.add("星期六");
         list.add("星期七");
         //新建一个线程进行追加元素
         Thread t1 = new Thread(){
             @Override
             public void run() {
                 try {
                     sleep(200);
                     list.add(0,"熊大");
                } catch (InterruptedException e) {
                     e.printStackTrace();
                }
            }
        };
         t1.start();
         //输出要遍历的内容:list原始内容
         for (String str : list) {
             System.out.println(str);
             try {
                 Thread.sleep(100);
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }
        }
         System.out.println("******************************************");
         //输出插入后的内容:使用CopyOnWriteArrayList复制后的内容
         System.out.println(list);
    }
 }
 

输出结果:

星期一 星期二 星期三 星期四 星期五 星期六 星期七


[熊大, 星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期七]

图示:

最佳应用:

  • 单线程尽量使用 ArrayList 或者 LinkedList;

  • 多线程并发使用 CopyOnWriteArrayList ;

  • 老API(Vector、synchronizedList() )不建议使用了。

    线程安全的List:Vector、Collections.synchronizedList(new ArrayList<>())、CopyOnWriteArrayList

 

posted @ 2021-08-17 19:41  Coder_Cui  阅读(174)  评论(0编辑  收藏  举报