Java基础——集合(1)ArrayList详解 —— 悟道

创建ArrayList


创建 ArrayList 时,一般是直接创建,或者是直接将实现了 Collection 接口的集合类传入进行初始化创建

// 1 直接new
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(Arrays.toString(list.toArray()));

// 2 创建时直接添加元素 实现了collection接口的类
ArrayList<String> list1 = new ArrayList<>(list);
System.out.println(Arrays.toString(list1.toArray()));
HashSet<String> hashSet = new HashSet<>();
hashSet.add("C");
ArrayList<String> list2 = new ArrayList<>(hashSet);
System.out.println(Arrays.toString(list2.toArray()));

添加元素 -> ArrayList 的扩容机制

首先看 ArrayList 的 add 方法,当添加元素时,首先是通过 ensureCapacityInternal 方法进行

增量判断,判断后进行赋值以及返回添加元素成功。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!这里是增量机制
    elementData[size++] = e;
    return true;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final int DEFAULT_CAPACITY = 10;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

而对于 ensureCapacityInternal 方法来说,首先通过 calculateCapacity 方法来计算容量。

如果 elementData(存储数据的数组) 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,

则说明 ArrayList 是无参刚创建的,所以使用默认的容量(10)与实际数量 minCapacity(size + 1)

中最大值。通过 ensureExplicitCapacity 方法验证容量与 elementData 实际长度的大小情况,

通过 grow 方法进行扩容。同理 addAll 方法也是同样的机制。只不过多了复制数组的一步。

成员变量 modCount 则是后面再说,下面先说 grow 方法是怎么增容的。

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    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:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

grow 方法增容详解。首先根据 ArrayList 中元素的数量 + (数量 >> 1)也就大约是容量的1.5倍,

所以 newCapacity - minCapacity < 0 这个条件只有空数据第一次添加的时候能成立,

让 elementData 初始化为长度10的数组,之后都不成立。在上面的 ensureExplicitCapacity

方法中 minCapacity 是 size + 1,所以只有当 size == elementData.length 时,才会

调用 grow 方法进行扩容,将容量 增扩为 oldCapacity + (oldCapacity >> 1),约是1.5倍。

删除元素 -> ArrayList 删除元素时所需考虑问题

删除方法一般使用 remove 方法,其他较为特殊的方法暂不讨论。

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

public boolean remove(Object o) {
    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 rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

通过 index 删除时先进行下标范围检查,然后获取到删除元素,移动原数组。

为什么是 size - 1 - index,是因为 index 从0开始。numMoved 不判断=0的情况

则是因为=0时,删除元素是末尾元素,直接置 null 就好。elementData[--size] = null

就是末尾元素置 null。其他情况则移动元素通过 object 删除时则涉及到 fastRemove 方法,

原理和删除 index 的情况一致。这里判断了 o == null是因为 o 为 null 时调用

equals 方法时报 NPE,所以要判断。而 equals 方法判断相等时,为了实现现实中的

‘相等概念’需要重写 equals 和 hashCode 方法。否则 equals 只是判断是否为同一对象。

ArrayList<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("赵四");
list.add("王五");

for (int i = 0; i < list.size(); i++) {
    if ("张三".equals(list.get(i))) {
        list.remove(list.get(i));
        System.out.println("张三已被删除");
    }
}

for (String str : list) {
    if ("张三".equals(str)) {
        list.remove(str);
        System.out.println("张三已被删除1");
    }
}

/*
    张三已被删除
    张三已被删除1
    Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
    at java.util.ArrayList$Itr.next(ArrayList.java:861)
    at com.wuhaitao.base.collection.List.ArrayListInfo.main(ArrayListInfo.java:62)
*/

一般情况下,集合中删除元素时,我们都是通过遍历去删除元素。所以这里存在一些问题。

由于有fail-fast 机制,这个机制意思是:一旦检测到可能会发生错误,就立马抛出异常,

程序将不再往下执行。这个机制并不是集合中特有的,只是在集合中很容易实现,在这里

介绍很方便。比如上面的例子,看着没有任何问题,但是一旦运行起来就会报错,抛出了

ConcurrentModificationException 异常。并且还调用了911行的checkForComodification

方法。明明我们只调用了 remove 方法,remove 方法中也没有调用

checkForComodification方法,那么这些情况是为什么呢?另外,为什么 fori 没有

问题的删除了第一个‘张三’,但是为什么后面不再删除了?在 fori 的情况下,

当第一个元素被删除时,集合本身元素位置发生变化,删除完第一个‘张三’后:i = 1,但是

第二个‘张三’被移动到了 index = 0 的位置,所以下一次 get(i) 时,获取到的是被移动到

index = 1 的‘赵四’第二个‘张三’,因此被巧妙地跳过了。所以,当使用 fori 进行删除时,

虽然可以进行删除操作,但存在数据问题。我们接着来看增强 for。

String[] arr = new String[]{"张三", "张三", "赵四", "王五"};
String[] var2 = arr;
int i = arr.length;

for(int var4 = 0; var4 < i; ++var4) {
    String s = var2[var4];
    System.out.println(s);
}

List<Integer> list = new ArrayList();

for(i = 0; i < 5; ++i) {
    list.add(i);
}

Iterator var7 = list.iterator();

while(var7.hasNext()) {
    Integer integer = (Integer)var7.next();
    if ("张三".equals(str)) {
        list.remove(str);
    }
}

增强for是一个语法糖。所以在编译代码时,它会被解析成基础代码。如果是数组使用增强for

则解析过后就是 fori 的形式。如果是实现了 Iterable 的集合,则会解析成 iterator

迭代器的形式遍历集合。所以,在之前 ArrayList 的增强 for 中,会被解析成 iterator

迭代器形式遍历。所以会调用 iterator,hasNext 方法以及 next 方法。但是呢!

这里有一个我们非常需要注意的地方,删除方法我们是调用的 ArrayList 的 remove 方法。

我们接下来看下 Itr 源码。(注意:泛型也是语法糖,也会被解析成基础代码,但这里不做讲解)

public Iterator<E> iterator() {
    return new Itr();
}

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() {}

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

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        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];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

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

在 ArrayList 实现的 iterator 方法中,返回了内部类 Itr 实例对象。Itr 则实现了

Iterator 接口,所以会重写 hasNext,next 方法。在 hasNext 中判断了指针是否还指向元素。

而在 next 方法中掉了用我们前面提到的报错方法 checkForComodification,这个方法抛出了

ConcurrentModificationException 异常。而这个异常是根据 modCount != expectedModCount

这个条件达成的。好,现在我们就要来聊聊这个一开始就提到的 modCount 了。

protected transient int modCount = 0;
// add 方法调用
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// remove 方法调用
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

首先 modCount 并不在 ArrayList 中定义的,而是在 AbstractList 中定义的,其记录的是

集合被修改的次数。所以,在 add,remove 等方法中会见到 modCount++ 这个操作。在‘张三’

的例子增强for中,创建 Itr 内部类时 int expectedModCount=modCount,expectedModCount

这个内部类成员变量已被赋值,因为做了四次 add、一次 remove 操作, 所以

expectedModCount = modCount = 5。当增强 for第一次遍历时,checkForComodification

方法判断成功,删除第二个‘张三’(第一个‘张三’是 fori 中删除)上面说过,增强 for 中调用的是

ArrayList的删除方法。所以这时modCount++变成了6,所以在下一次删除时,

checkForComodification 方法判断失败,所以报了 ConcurrentModificationException 异常。

那么我们来看正常的 iterator 迭代器遍历。

List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("赵四");
list.add("王五");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if ("张三".equals(next)) {
        iterator.remove();
        System.out.println("张三已被删除1");
    }
}
// itr 的删除源码
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

在迭代器遍历删除时,我们调用的时 Itr 内部类的删除方法,我们明显看到了

expectedModCount = modCount,这也就解释了为什么迭代器遍历不会报错而增强 for 会报错

最后,阿里守则中,也明确提到了,不要在 foreach 循环里删除元素。若需删除元素,

请使用 Iterator 方式。在并发操作时,需对 Iterator 对象加锁。

查找元素 -> 如果有序,可以二分查找提高效率

如果要正序查找一个元素,可以使用 indexOf 方法;如果要倒序查找一个元素,可以使用

lastIndexOf 方法contains方法可以判断 ArrayList 中是否包含某个元素,内部调用indexOf方法

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;
}
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快

ArrayList<String> copy = new ArrayList<>(alist);
copy.add("1");
copy.add("2");
copy.add("2");
copy.add("4");
Collections.sort(copy);
System.out.println(copy);

int index = Collections.binarySearch(copy, "b");
System.out.println(index);

更新元素

来看一下 set() 方法的源码

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

更新元素时,先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值


(。・∀・)ノ゙嗨!如果这篇文章小小帮助到你,可以帮小弟我点个赞呀。点赞是对我最大的支持与认可,感谢!

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

推荐阅读更多精彩内容