java集合ArrayList与LinkedList源码分析

Array源码分析

首先分析new ArrayList<>()
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
public ArrayList() {
        // 初始化数组的容量为{}
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

new ArrayList()时,会创建一个Object[] elementData = {} 的数组。由此可以知道list底层是通过Object[]数量来存储数量的

分析通过无参构造函数初始化ArrayList时第一次调用add()方法
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;  // 存储list数据的数组
private int size;  //当前arrayList的size(集合容量)
private static final int DEFAULT_CAPACITY = 10;

public boolean add(E e) {
        // 判断容器是否够大,如果超出容器大小需要进行扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
// 计算容器需要多大的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果  elementData 数组的容器为{}     
// 如果通过new ArrayList<>()无参构造函数初始化时,第一次调用add()方法时会进行扩容,将容量变从{}为10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // DEFAULT_CAPACITY=10  大于 minCapacity = 1,所以会取DEFAULT_CAPACITY
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
        }

private void ensureExplicitCapacity(int minCapacity) {
// 这个字段用于判断是否执行快速失败原则(fail-fast)
//相当于乐观锁的版本号,因为arraylist是线程不安全的,
//当在进行Iterator迭代遍历时,首先获取modCount的值保存到本地变量中,当遍历时会判断本地变量和modCount(全局变量)是否相等,
//如果不相等(有其他线程改变了集合的结构,会修改modCount的值),就会抛出ConcurrentModificationException异常
        modCount++;  

        // overflow-conscious code
        // 这里传进来的minCapacity是当前add元素时需要的最小容器(size+1)
        // 当需要的最小容器大于当前容器的容量时,就需要进行扩容
        if (minCapacity - elementData.length > 0)
            // 对容器进行扩容
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  // 当前容量大小
// 需要扩容到多大    >>1表示/2     
//相当于oldCapacity + (oldCapacity / 2),也就是需要扩展到原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);  0+0/2=0
// 因为上面方法调用时将minCapacity变为10传过来的
  // 0-10<0,所以会进入下面的第一个if
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;    // 10
// int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 最大容量可设置为Integer.MAX_VALUE (2的32次方)
            newCapacity = hugeCapacity(minCapacity);
        // 这方法会进行扩容,会将原数据copy到一个容器大小为newCapacity新的list中,然后返回
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

当通过无参构造函数初始化ArrayList时,在第一次add时会将list的容量扩容到10,之后每次扩容都是原来的1.5倍,比如当add到第11个元素时,会再次进行扩容,将容量扩容到15

分析get()方法
public E get(int index) {
        // 判断坐标是否越界
        rangeCheck(index);
        // 获取元素并返回
        return elementData(index);
    }
private void rangeCheck(int index) {
      // size为list中元素的数量
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 获取Object数组中index坐标的值,强转成E类型并返回
E elementData(int index) {
        return (E) elementData[index];
    }

get方法很简单,先判断坐标是否越界,然后获取elementData数组中元素并返回
这里要提一下size与elementData.length的区别
就相当于new arrayList(10) 与Object[] o = new Object[10]的区别
在没进对arrayList进行add添加元素的时候,不管容器大小是多少 size都为0的,所以就算new arrayList(10),在get(0)时都会报坐标越界。
而new Object[10]在没有添加元素是,o[0]获取到的是null

分析remove方法
public E remove(int index) {
        // 判断坐标是否越界
        rangeCheck(index);

        modCount++;
        // 获取需要删除的元素,删除成功后会方法返回
        E oldValue = elementData(index);
        
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 对数组进行元素并对坐标进行重新排序,这个方法是jvm底层的native方法
            // 需要5个参数,
            1. src:源数组; 
            2.srcPos:源数组要复制的起始位置;
            3.dest:目标数组; 
            4.destPos:目标数组放置的起始位置; 
            5.length:复制的长度。

            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 数组元素的坐标重新排序后,需要对最后一个坐标设置为null,因为删除了一个元素,它后面的元素就会向前移动一位
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }
ArrayList总结

1.list集合底层是通过Object[]数组来存储的
2.new ArrayList()时,初始化的容器是没有容量大小(相当于0)的,在第一次add时会将容器扩容到10,之后每次扩容数为原来的1.5倍。因为new ArrayList()初始化的容器是没有容量的,在第一次add时会进行扩容,而扩容会降低效率,所以阿里巴巴规范也有提到,一般在使用arrayList时,都会估算需要容量,初始化容器的大小,比如new ArrayList(10),这样在add时会减少扩容的次数
3.在删除元素时,会将元素删除,并将这个元素后面的元素向前移动,然后置null最后一个位置
比如第一个图为原数组,删除 C后,会将D和E向前移动,然后将最后一个位置置为null

image.png

4.arrayList是线程不安全的,在Iterator迭代遍历时,如果modCount被修改了,会执行快速失败原则(fail-fast),抛出ConcurrentModificationException异常
5.如果想解决4问题,可以使用Vector集合或者Collections.synchronizedList(new ArrayList<>());或者CopyOnWriteArrayList集合类
6.查询速度快,查询时间复杂度小(O(1),直接通过index到数组上找),空间复杂度大

LinkedList源码分析

    transient int size = 0;   // size表示当前集合元素的大小,add会size++  remove会size--
    transient Node<E> first;  // 当前集合的第一个节点
    transient Node<E> last;  // 当前集合的最后一个节点
    public LinkedList() {
    }

new LinkedList只是初始化一个空的list,LinkedList是通过链表结构的node(节点)来存储的,首先看看Node<E>的结构

private static class Node<E> {
        E item;    // 当前节点的元素
        Node<E> next;  // 当前节点的下一个节点
        Node<E> prev;  // 当前节点的上一个节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

在LinkedList中增加A B C三个元素,它的存储结构如下图,每个元素对会封装成Node节点对象来存储


image.png
LinkedList的add方法
transient Node<E> first;  // 当前集合的第一个节点
transient Node<E> last;  // 当前集合的最后一个节点
public boolean add(E e) {
        // 在链表的最后增加元素
        linkLast(e);
        return true;
    }

void linkLast(E e) {
        final Node<E> l = last;   // 获取当前集合的最后一个节点
        final Node<E> newNode = new Node<>(l, e, null);  // 封装当前需要添加的节点
        last = newNode; // 将需要添加的节点设置为当前链表的最后一个节点
        if (l == null)  // 如果last为null(表示当前元素没有node对象)
            first = newNode;  第一个node设置为当前增加的节点对象
        else
            l.next = newNode;  // 否则在最后一个节点的下一个位置加上当前需要增加的节点对象
        size++;
        modCount++;
    }

调用add时会在链表的最后增加一个节点来存储这个元素,这里提一点linkedList.add(null);在增加是会添加一个Node对象来存储,这个null值是赋值在node.item=null,不要理解成node=null

LinkedList的get方法
public E get(int index) {
        // 检查坐标是否越界
        checkElementIndex(index);
        // 获取这个坐标的元素并返回
        return node(index).item;
    }

Node<E> node(int index) {
         // 通过折半查找(二分查找)法来查询node节点所在的位置
        // 如果index小于size的一半,就从第一个元素开始找,不断向下一个元素找
        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;
        }
    }

LinkedList不像数组那样每个元素都有一个索引标识,只能从第一个node对象或者最后一个node对象遍历index次(first.next或last.next执行index次)找到这个元素,比如链表长度为100 需要查询index 60的元素 如果从first节点开始找,就需要循环遍历60次。linkedList底层通过折半查找(二分查找)法,60大于50(一半),从100开始循环遍历40次就能定位到,提高查询效率

LinkedList的remove方法
public E remove(int index) {
         // 检查坐标是否越界
        checkElementIndex(index);
        // 先查询出node节点,然后删除这个节点
        return unlink(node(index));
    }

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

LinkedList删除元素,其实就是将这个元素的node节点对象的prev(上一下)node节点对象和next(下一个)节点对象的所关联的prev和node的指向修改,然后将当前node节点的属性都置空(便于GC回收)

LinkedList总结

1.LinkedList底层是通过双向链表结构node节点对象来存储的,node对象有三个属性item(当前元素值),prev(上一个node对象),next(下一个node对象)。
2.linkedList.add(null),存储的是node.item=null,而不是node=null。
3.增加和删除的速度快,空间复杂度小(不需要记录节点的位置),查询时间复杂度大(查询相对慢 O(n),比如有100个元素,找第30个。就需要把链表的node从第一个开始node.next循环30次才能找出来)

set集合
public HashSet() {
        map = new HashMap<>();
    }
public TreeSet() {
        this(new TreeMap<E,Object>());
    }

private static final Object PRESENT = new Object();
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

set集合底层用的是map,HashSet用的是HashMap,TreeSet用的是TreeMap
set.add时会把元素值存到map的key中,value为new Object()对象
因为map中的key是唯一的,所以set集合可以保存元素的唯一性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容