关闭

[Jdk源码阅读]ArrayList实现

标签: jdkarraylist阅读源码
376人阅读 评论(0) 收藏 举报
分类:

本文来自mrcode markdown博客:http://www.mrcode.cn/zhuqiang/article/details/20.html

关键属性:

属性 说明
transient Object[] elementData; 底层使用该Object数组进行存储元素,并且该属性的访问权限是:同类同包,且不序列化
modCount 从java.util.AbstractList 继承的字段;记录的应该是被修改的次数,比如add一次就等于修改了一次

入门

用以下简单的示例进行分析

        ArrayList<String> list = new ArrayList<String>();

        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("1");
        list.add("6");
        list.add("7");
        list.add("8");
        list.add("9");
        list.add("10");
        list.add("11");

        list.remove("1");
        list.remove(null);
        list.remove(1);
        list.contains("5");
        list.ensureCapacity(5);

1.1. add 添加元素方法分析

    // 步骤1:空参
    public ArrayList() {
        // 全局默认的空元素数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    // 步骤2:添加元素
    public boolean add(E e) {
        //负责扩容elementData仓库,当前列表长度+1.为当前数据所需要的最小容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //把元素添加到列表(对外我们看到的arraylist列表)末尾,并且让列表的大小加1
        elementData[size++] = e; //由于支持的最大容量是Integer.MAX_VALUE,这里如果超过了最大容量是不是就会抛出下标越界异常呢?
        return true;
    }

    //该方法负责扩容elementData仓库;该类add重载的方法都调用了该方法,在下图可以看到
    private void ensureCapacityInternal(int minCapacity) {
        // 当仓库为默认空数组的时候,计算最小容量
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //默认容量=10;
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    // 记录修改的次数;判断是否需要进行扩容; 在jdk文档中也说了:在添加大量元素之前可以使用ensureCapacity方法来增加数组仓库的容量,减少copy的次数,提高性能
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        // 这个最小容量相当于预判端 已有的容量仓库是否能装得下增加的数据
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    // 负责扩容
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 每次扩容是源存储仓库的一半,/2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 特殊判断:当this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA的时候,上面的newCapacity算出来会=0,所以这里需要初始化一个最小容量。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新的容量大于了最大的容量,则进行判定,获取到适合的容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //把旧的存储仓库进行copy到一个新的数组中,并返回
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    // 这里针对最大容量超过了 默认的容量的处理返回容量:MAX_ARRAY_SIZE在源码中是Integer.MAX_VALUE-8,也就是说arraylist的最大容量是Integer.MAX_VALUE,
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

所有添加元素的方法都调用了该方法进行判断扩容

1.2.remove 移除元素方法分析

    // 步骤 3:移除元素
    public boolean remove(Object o) {
        //针对null元素的移除,和非null元素的代码差不多
        if (o == null) {
            for (int index = 0; index < size; index++)
                //只是在判断 目标元素是否匹配 上使用了 ==
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    //根据匹配元素的索引进行删除该元素
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    //在数组仓库中删除 指定索引的元素
    // 这里的原理是:需要把删除元素之后的所有元素都往前进行挪一步
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1; //算出需要移动的元素个数
        if (numMoved > 0)
            //从数组仓库中 index+1 到 列表size-1 之前的所有元素都重新复制到 数组仓库中:从index索引开始覆盖,最后就会存在多出来一个元素。这里也就是把删除掉元素后面的所有元素都往前进行了挪动,并且是以覆盖的方式进行挪动。
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //把多出来的元素置为null,并且把列表大小-1;
        elementData[--size] = null; // clear to let GC do its work
    }

1.3. contains 是否包含指定的元素方法分析

如果认真看了上面两个方法的分析,这里的就很简单了。能看出来,判断什么的几乎上都是一样的;

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    // 返回此列表中首次出现的指定元素的索引
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

1.4 Iterator 遍历

    private class Itr implements Iterator<E> {
        //当前游标
        int cursor;       // index of next element to return; 记录下一个元素的索引
        int lastRet = -1; // index of last element returned; -1 if no such 记录最后一次的索引(也就是上一次元素的索引)
        int expectedModCount = modCount; //获取itr对象最后修改的次数(相当于版本吧,用来判断在itr期间该list是否被更改,用来进行快速失败,因为在迭代获取的时候,如果list被更改,那么获取到的数据不准不说,还会导致程序出错)

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification(); //检查list是否被更改,只是判断了修改版本,快速失败
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        //itr 的删除
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification(); //同样再次检查是否修改

            try {
                ArrayList.this.remove(lastRet); //调用原来的删除方法删除元素,在元素删除的时候数组都会被copy一次,那么元素中的原来的索引就会发生变化
                cursor = lastRet;// 重置游标到当前的元素,因为当前的被删除掉了,那么下一次获取的时候就是下一个元素的索引
                lastRet = -1; //该参数是在获取的时候 根据cursor游标进行赋值的,所以这里重置为-1; 是因为如果都删除完了还调用next方法,就会抛出异常。
                expectedModCount = modCount; //修正最新的修改次数。
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

1.4.1. 总结

  1. 为什么在itr中删除元素比在fori中删除list中的元素 要方便很多,在fori中删除list元素,你要考虑到当前的索引是否超过了大小,而在itr中不用考虑呢?
    因为itr中记录了当前的游标,删除操作还是委托的原来的删除方法,那么也会导致list的数组大小和元素索引发生变化,正是因为有了这个游标,才解决了上面的问题。
  2. 在其他方面,防止多线程对外修改导致itr内部数据计算出错,使用了检测修改次数的方式进行快速失败

总结

第一次认真的阅读Jdk的源码,网上说该工具类是相对简单的实现,先从这个类看起。现在看来果然不错。
从上面三个方法,基本上就把该类给整明白了。

  1. 线程是不安全的
  2. 底层使用一个Object数组进行存储元素
  3. 可自动扩容
  4. 允许null元素
  5. 由于扩容策略,在大量插入元素的时候可以使用ensureCapacity方法来增加数组容量
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:112955次
    • 积分:1918
    • 等级:
    • 排名:千里之外
    • 原创:81篇
    • 转载:27篇
    • 译文:0篇
    • 评论:40条
    关于博客
    • 本博客记录一些学习笔记和工作开发中遇到的一些问题
    • QQ:99299684
    相关博客和项目
    1. mrcode 博客
    2016.02-2016.10为了学习技术而写的一个多人使用的博客
    2. 微信SDK自用封装
    工作中封装了一部分用到的微信api,搭成了一个架子,扩展方便。
    3. GitBook
    最近发现Gitbook能更好的记录笔记,以后就转移到这里了
    4. GitBook 自开发插件
    在使用GitBook过程中,方便使用,模仿开发了一些插件。
    最新评论