一,前言

​ 先来一张Collection集合图。

​ 今天分享一些关于Collection集合中的List,讲真的集合这东西在网上真是老生常谈了。说实话连本人都觉得腻了(哈哈),但是话又说回来,整个集合体系对于我们实际开发来说是非常重要的,所以还是有必要系统总结下。

​ 不过在此之前先说说两种数据结构,链表和红黑树。

1.1,链表

  • 链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表

          ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145207810-1965049975.png)

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。

  • 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素。

  • 增删元素快:

    • 增加元素:只需要修改连接下个元素的地址即可。

          ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145230328-645301513.png)
    • 删除元素:只需要修改连接下个元素的地址即可。

1.2,红黑树

​ 红黑树是一种自平衡二叉查找树,是计算机科学领域中的一种数据结构,典型的用途是实现关联数组,存储有序的数据

​ 红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。

红黑树的约束:

  • 节点可以是红色的或者黑色的。
  • 根节点是黑色的。
  • 叶子节点(特指空节点)是黑色的。
  • 每个红色节点的子节点都是黑色的。
  • 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同。

红黑树的特点:

​ 速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。

二,List接口

2.1,简介

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。

​ 那么List接口有什么特点。

  • 它是一个元素存取有序的集合。例如,存元素的顺序是1、2、3。那么集合中,元素的存储就是按照1、2、3的顺序完成的)。
  • 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  • 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

2.2,常用方法

​ List接口继承Collection接口,因此在父类接口中的方法,List子接口全部都有,并且还增加通过索引的方式操作数据。

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index):返回集合中指定位置的元素。
  • public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
public class ListDemo {
    public static void main(String[] args) {
        // 创建List集合对象
        List<String> list = new ArrayList<>();

        // 往尾部添加 指定元素
        list.add("ABC");
        list.add("张三");
        list.add("李四");

        System.out.println(list);
        // add(int index,String s) 往指定位置添加
        list.add(1,"DEF");

        System.out.println(list);
        // String remove(int index) 删除指定位置元素  返回被删除元素
        // 删除索引位置为2的元素
        System.out.println("删除索引位置为2的元素");
        System.out.println(list.remove(2));
        System.out.println(list);

        // String set(int index,String s)
        // 在指定位置进行元素替代(改)
        // 修改指定位置元素
        list.set(0, "三毛");
        System.out.println(list);

        // String get(int index)  获取指定位置元素
        // 跟size() 方法一起用来遍历的
        for(int i = 0;i<list.size();i++){
            System.out.println(list.get(i));
        }
        //还可以使用增强for
        for (String string : list) {
            System.out.println(string);
        }
    }
}

三,Array List(JDK8)

​ Array List底层是基于动态数组实现的,何以动态。在于它是一个可变的数组,根据实际情况它的容量可自动扩充,类似于在C语言中动态申请内存的原理。(题外话:记得有一次面试,面试官问我arraylist是怎么实现的,我说是一个动态数组。然后把他吓一跳,说一句让我想清楚再说。我还以为说错了,接着沉淀5秒钟,就是一个可自动扩容的动态数组)。

​ 其特点是增删慢,查询快。因为是基于数组实现那么就会存在索引,增删就是对数据的更新,导致索引也会发生相应变化。但是存在索引值,所以对于查询数据来说就很快了。如图:

3.1,数组实现

​ 为什么说底层是一个数组,请看源码:

        ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145415120-138505220.png)

​ ArrayList在初始化时默认的数组容量为10,源码如下:

        ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145425647-1874643859.png)

3.2,构造方法

​ 1,定义无参构造方法,初始化默认为10的空列表。

​ 2,有参构造,并且自定义初始化容量的大小。

3,将数据类型转换为数组类型。

分析:

​ 1,elementData = c.toArray();将元素转化成数组。

​ 2,判断数组大小是否为0,否则将默认EMPTY_ELEMENTDATA赋值给数组。

​ 3,接着里面的if判断,判断elementData是否成功转换为数组,如果成功则调用Arrays.copyOf方法将元素值,数组大小,及数组类复制值到elementData数组中。

​ 完成以上初始化步骤之后,接着我们可以调用add方法完成数据添加。

3.3,add方法

​ 先介绍没有指定索引的数据添加,请看add源码:

分析:

​ 1,ensureCapacityInternal(size + 1);这个方法是判断在添加数据之前,数组的容量是否够用。这里就看下源码的实现吧。

        ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145528853-101759753.png)

判断是否要扩容。

说明:

  • 首先size默认为0,然后size+1将参数传过来。
  • 判断if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)是不是第一次添加元素,如果是,则选取默认的容量大小10,正式将数组容量初始化为10.
  • 接着调用ensureExplicitCapacity(minCapacity);这里是判断是否需要进行扩容。这里我们假设不需要扩容,接着说完add方法最后一步。

​ 2,elementData[size++] = e;最后执行这一步,将元素e添加到elementData数组中,并且大小加1,并完成元素添加。

3.4,扩容机制

​ 上面我们说到在数组不扩容情况下元素是如何进行添加的,再来说说如何进行扩容。接着上面ensureExplicitCapacity(int minCapacity)这个方法,判断如果超出默认容量大小时,便调用grow(minCapacity);方法开始进行扩容。

在这里先指出扩容的机制其实就是数组的复制,创建一个新的数组,将小容量数组的元素复制到容量较大的数组中,以此完成数组的扩容。

​ 接着我们看源码的实现:

        ​![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145606359-1979185667.png)

分析:

​ 1,获取旧数组的大小。

​ 2,将大小扩大1.5倍。

​ 3,新容量小于参数指定容量,修改新容量。

​ 4,新容量大于最大容量,并指定新容量。

​ 5,调用copyOf进行复制。

        ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145614351-580655469.png)

​ 完成扩容。

四,线程不安全

​ Array List是线程不安全的,这点很重要,也是面试最常问的问题。那为什么是不安全的呢,下面简单总结以下。

情况一:

​ 假设现在有A,B两个线程同时执行add方法,而现在size = 9,于是:

​ 1,A经过以上步骤发现初始化容量为10,不需要进行数组扩容。

​ 2,同时B也在执行add方法,它判断数组初始化容量也是10(size的值还是9),于是不进行数组扩容。接着A便执行add方法,元素添加成功后,size = 10;此时B开始执行add方法,但这个时候数组元素已经添加满了,B再添加就会造成数组下标越界异常。

情况二:

elementData[size++] = e;这一步也有可能出现问题。

​ 列表大小为0,即size=0。
​ 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
​ 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
​ 而最终size大小为:

  • 线程A开始将size的值增加为1
  • 线程B开始将size的值增加为2

​ 针对这种情况,可以使用以下方式解决:

List<String> list1 = Collections.synchronizedList(new ArrayList<String>());

五,Linked List

​ linked list数据存储的结构是双向链表结构。方便元素添加、删除的集合,但是查询较慢。

        ![](https://img2018.cnblogs.com/blog/1655301/201909/1655301-20190907145630389-1690521785.png)

LinkedList中常用方法:

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public E pop():从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e):将元素推入此列表所表示的堆栈。
  • public boolean isEmpty():如果列表不包含元素,则返回true。
public class LinkedListDemo {
    public static void main(String[] args) {
        LinkedList<String> link = new LinkedList<String>();
        //添加元素
        link.addFirst("abc1");
        link.addFirst("abc2");
        link.addFirst("abc3");
        System.out.println(link);
        // 获取元素
        System.out.println(link.getFirst());
        System.out.println(link.getLast());
        // 删除元素
        System.out.println(link.removeFirst());
        System.out.println(link.removeLast());

        while (!link.isEmpty()) { //判断集合是否为空
            System.out.println(link.pop()); //弹出集合中的栈顶元素
        }
        System.out.println(link);
    }
}

六,总结

​ 1,arraylist增删慢,查询快,基于动态数组。

​ 2,linkedlist增删快,查询慢,基于双向链表。

​ 3,线程都不安全。

​ 关于list接口中常用的两个集合就分享到此,对于两个集合要根据实际开发业务进行合理的选择,并且要注意线程安全的问题。本来想把set接口也一起写上,但是我觉得太长了,所以就留到下一篇博客。

​ 最后对于以上博文如有不适之处,欢迎留言指正。

感谢阅读!

Array List和Linked List实现分析的更多相关文章

  1. Lintcode489-Convert Array List to Linked List-Easy

    489. Convert Array List to Linked List Convert an array list to a linked list. Example Example 1: In ...

  2. 手工实现Array List和Linked List

    Array List样例: /** * 增加泛型 * 自动增加数组容量 * 增加set.get方法:增加数组边界的检查 * 增加remove方法 */package cn.study.lu.four; ...

  3. 【荐】PDO防 SQL注入攻击 原理分析 以及 使用PDO的注意事项

    我们都知道,只要合理正确使用PDO,可以基本上防止SQL注入的产生,本文主要回答以下几个问题: 为什么要使用PDO而不是mysql_connect? 为何PDO能防注入? 使用PDO防注入的时候应该特 ...

  4. [转]PDO防注入原理分析以及使用PDO的注意事项

    原文:http://zhangxugg-163-com.iteye.com/blog/1835721 好文章不得不转. 我们都知道,只要合理正确使用PDO,可以基本上防止SQL注入的产生,本文主要回答 ...

  5. PDO防注入原理分析以及使用PDO的注意事项

    我们都知道,只要合理正确使用PDO,可以基本上防止SQL注入的产生,本文主要回答以下两个问题: 为什么要使用PDO而不是mysql_connect? 为何PDO能防注入? 使用PDO防注入的时候应该特 ...

  6. PDO防注入原理分析以及使用PDO的注意事项 (转)

    我们都知道,只要合理正确使用PDO,可以基本上防止SQL注入的产生,本文主要回答以下两个问题: 为什么要使用PDO而不是mysql_connect? 为何PDO能防注入? 使用PDO防注入的时候应该特 ...

  7. LeetCode OJ 154. Find Minimum in Rotated Sorted Array II

    Follow up for "Find Minimum in Rotated Sorted Array":What if duplicates are allowed? Would ...

  8. LeetCode 922 Sort Array By Parity II 解题报告

    题目要求 Given an array A of non-negative integers, half of the integers in A are odd, and half of the i ...

  9. LeetCode 905 Sort Array By Parity 解题报告

    题目要求 Given an array A of non-negative integers, return an array consisting of all the even elements ...

随机推荐

  1. Netty源码分析-- ThreadLocal分析(九)

    为了更好地探讨Netty的内存模型,后面会用到,这里我还是决定跟大家一起看下ThreadLocal和FastThreadLocal的源码,有的时候我们在看源码的时候会一层层的遇到很多之前没有看过的内容 ...

  2. git之coding.net的使用

    先在Coding上创建个项目     现在是这样,我本地有个项目Project(/Users/huang/Desktop/Project),我想把它上传到刚创建的项目里,以后就用git代码托管.可我之 ...

  3. 夯实Java基础(九)——final关键字

    1.前言 Java语言中的final关键字,想必大家都不是很陌生,我们自己用的最多的应该是用来定义常量吧,那么今天我们就来了解final这个关键字的用法,这个关键字还是非常简单的. final从字面意 ...

  4. BFS DFS模板

    转载于https://blog.csdn.net/alalalalalqp/article/details/9155419 BFS模板: #include<cstdio> #include ...

  5. ABP 配置全局数据过滤器 II

    第一篇 那种写法有些复杂, 简单办法是直接注入 切换到 ***.EntityFramework 项目 在Uow 里面创建 ***EfUnitOfWork.cs 类 public class Coope ...

  6. (17)ASP.NET Core EF基于数据模型创建数据库

    1.简介 使用Entity Framework Core构建执行基本数据访问的ASP.NET Core MVC应用程序.使用迁移(Migrations)基于数据模型创建数据库,你可以在Windows上 ...

  7. 如何将自己的代码发布到Maven中央仓库?

    去年在公司做工作流相关业务时,当时使用flowable做引擎,中途涉及到一些业务上的需求,自己整理了一些代码,考虑到开源精神,当时就想着将于公司业务无关的代码抽离出来,放到Maven中央仓库中,以供别 ...

  8. 如何使用python records 库优雅的操作数据库

    今天要介绍的这个python第三方库非常厉害,完美操作各种数据库.名字叫 records, 在网上很少有这个库的相关资料,但是在开源社区可是很火热的哦.如果这还不能打消你的顾虑,再告诉你一件事:如果你 ...

  9. Linux运维记 - 重启网卡后,网络不通

    前言 不小心重启了线上服务器的网卡,结果整个网络不通了,就算使用127.0.0.1访问都不行,第一次遇到这种问题,当时就六神无主了,两个人排查了好久也没找到原因,万分火急.排查内核日志发现网卡状态不断 ...

  10. Math和Date

    Math和Date 一.对象 1.对象的概念 对象的本质:键值对,属性名和属性值 对象的意义:存储数据,编程 对象中的变量:属性 对象中的函数:方法 2.对象的赋值 var obj = {}; var ...