当前位置:qq发红包最多的群号资讯网首页>冒充高富帅骗四女>威海招筒回汽车用品有限公司

刷卡买包烟欠3亿

书名:阿里投资人离职|作者:笑无语|本书类别:古言|更新时间:09:18:58|字数:3896字

  关于 JDK 的集合类的整体介绍可以看这张图,本篇博客我们不系统的介绍整个集合的构造,重点是介绍 ArrayList 类是如何实现的。

   guan yu JDK de ji he lei de zheng ti jie shao ke yi kan zhe zhang tu, ben pian bo ke wo men bu xi tong de jie shao zheng ge ji he de gou zao, zhong dian shi jie shao ArrayList lei shi ru he shi xian de.

1、ArrayList 定义

  ArrayList 是一个用数组实现的集合,支持随机访问,元素有序且可以重复。

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable

  

  ①、实现 RandomAccess 接口

  这是一个标记接口,一般此标记接口用于?List?实现,以表明它们支持快速(通常是恒定时间)的随机访问。该接口的主要目的是允许通用算法改变其行为,以便在应用于随机或顺序访问列表时提供良好的性能。

  比如在工具类 Collections(这个工具类后面会详细讲解)中,应用二分查找方法时判断是否实现了 RandomAccess 接口:

1     int binarySearch(Listextends Comparablesuper T>> list, T key) {
2         if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
3             return Collections.indexedBinarySearch(list, key);
4         else
5             return Collections.iteratorBinarySearch(list, key);
6     }
View Code

  ②、实现 Cloneable 接口

  这个类是 java.lang.Cloneable,前面我们讲解深拷贝和浅拷贝的原理时,我们介绍了浅拷贝可以通过调用 Object.clone() 方法来实现,但是调用该方法的对象必须要实现 Cloneable 接口,否则会抛出 CloneNoSupportException异常。

  Cloneable 和 RandomAccess 接口一样也是一个标记接口,接口内无任何方法体和常量的声明,也就是说如果想克隆对象,必须要实现 Cloneable 接口,表明该类是可以被克隆的。

  ③、实现 Serializable 接口

  这个没什么好说的,也是标记接口,表示能被序列化。

  ④、实现 List 接口

  这个接口是 List 类集合的上层接口,定义了实现该接口的类都必须要实现的一组方法,如下所示,下面我们会对这一系列方法的实现做详细介绍。

  

2、字段属性

        //集合的默认大小
        private static final int DEFAULT_CAPACITY = 10;
        //空的数组实例
        private static final Object[] EMPTY_ELEMENTDATA = {};
        //这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        //存储 ArrayList集合的元素,集合的长度即这个数组的长度
        //1、当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList
        //2、当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10
        transient Object[] elementData;
        //表示集合的长度
        private int size;

3、构造函数

1     public ArrayList() {
2         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
3     }
View Code

  此无参构造函数将创建一个?DEFAULTCAPACITY_EMPTY_ELEMENTDATA 声明的数组,注意此时初始容量是0,而不是大家以为的 10。

  注意:根据默认构造函数创建的集合,ArrayList list = new ArrayList();此时集合长度是0.

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
View Code

  初始化集合大小创建 ArrayList 集合。当大于0时,给定多少那就创建多大的数组;当等于0时,创建一个空数组;当小于0时,抛出异常。

 1     public ArrayList(Collectionextends E> c) {
 2         elementData = c.toArray();
 3         if ((size = elementData.length) != 0) {
 4             // c.toArray might (incorrectly) not return Object[] (see 6260652)
 5             if (elementData.getClass() != Object[].class)
 6                 elementData = Arrays.copyOf(elementData, size, Object[].class);
 7         } else {
 8             // replace with empty array.
 9             this.elementData = EMPTY_ELEMENTDATA;
10         }
11     }
View Code

  这是将已有的集合复制到 ArrayList 集合中去。

4、添加元素

  通过前面的字段属性和构造函数,我们知道 ArrayList 集合是由数组构成的,那么向 ArrayList 中添加元素,也就是向数组赋值。我们知道一个数组的声明是能确定大小的,而使用 ArrayList 时,好像是能添加任意多个元素,这就涉及到数组的扩容。

  扩容的核心方法就是调用前面我们讲过的Arrays.copyOf 方法,创建一个更大的数组,然后将原数组元素拷贝过去即可。下面我们看看具体实现:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  //添加元素之前,首先要确定集合的大小
        elementData[size++] = e;
        return true;
    }

  如上所示,在通过调用 add 方法添加元素之前,我们要首先调用 ensureCapacityInternal 方法来确定集合的大小,如果集合满了,则要进行扩容操作。

 1     private void ensureCapacityInternal(int minCapacity) {//这里的minCapacity 是集合当前大小+1
 2         //elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小
 3         ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 4     }
 5     private static int calculateCapacity(Object[] elementData, int minCapacity) {
 6         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果数组为空,则从size+1的值和默认值10中取最大的
 7             return Math.max(DEFAULT_CAPACITY, minCapacity);
 8         }
 9         return minCapacity;//不为空,则返回size+1
10     }
11     private void ensureExplicitCapacity(int minCapacity) {
12         modCount++;
13 
14         // overflow-conscious code
15         if (minCapacity - elementData.length > 0)
16             grow(minCapacity);
17     }
View Code

  在?ensureExplicitCapacity 方法中,首先对修改次数modCount加一,这里的modCount给ArrayList的迭代器使用的,在并发操作被修改时,提供快速失败行为(保证modCount在迭代期间不变,否则抛出ConcurrentModificationException异常,可以查看源码865行),接着判断minCapacity是否大于当前ArrayList内部数组长度,大于的话调用grow方法对内部数组elementData扩容,grow方法代码如下:

 1     private void grow(int minCapacity) {
 2         int oldCapacity = elementData.length;//得到原始数组的长度
 3         int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍
 4         if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacity
 5             newCapacity = minCapacity;
 6         //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639
 7         if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组
 8             newCapacity = hugeCapacity(minCapacity);
 9         //调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)
10         elementData = Arrays.copyOf(elementData, newCapacity);
11     }
12     
13     private static int hugeCapacity(int minCapacity) {
14         if (minCapacity < 0) // 
15             throw new OutOfMemoryError();
16         return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUE
17             Integer.MAX_VALUE :
18             MAX_ARRAY_SIZE;
19     }
View Code

  对于 ArrayList 集合添加元素,我们总结一下:

  ①、当通过?ArrayList() 构造一个空集合,初始长度是为0的,第 1 次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。

  ②、第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。

  ③、第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15 的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。

  ④、第?Integer.MAX_VALUE - 8 = 2147483639,然后 2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个?1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。

  ⑤、第?Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为?Integer.MAX_VALUE 的数组,在进行元素添加。

  ⑥、第?Integer.MAX_VALUE + 1 次添加元素时,抛出?OutOfMemoryError 异常。

  注意:能向集合中添加 null 的,因为数组可以有 null 值存在。

1 Object[] obj = {null,1};
2 
3 ArrayList list = new ArrayList();
4 list.add(null);
5 list.add(1);
6 System.out.println(list.size());//2
View Code

5、删除元素

  ①、根据索引删除元素

 1     public E remove(int index) {
 2         rangeCheck(index);//判断给定索引的范围,超过集合大小则抛出异常
 3 
 4         modCount++;
 5         E oldValue = elementData(index);//得到索引处的删除元素
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)//size-index-1 > 0 表示 0<= index < (size-1),即索引不是最后一个元素
 9             //通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved的元素拷贝到从index开始的位置
10             System.arraycopy(elementData, index+1, elementData, index,
11                              numMoved);
12         elementData[--size] = null; //将数组最后一个元素置为 null,便于垃圾回收
13 
14         return oldValue;
15     }
View Code

  remove(int index) 方法表示删除索引index处的元素,首先通过 rangeCheck(index) 方法判断给定索引的范围,超过集合大小则抛出异常;接着通过 System.arraycopy 方法对数组进行自身拷贝。关于这个方法的用法可以参考这篇博客。

  ②、直接删除指定元素

 1     public boolean remove(Object o) {
 2         if (o == null) {//如果删除的元素为null
 3             for (int index = 0; index < size; index++)
 4                 if (elementData[index] == null) {
 5                     fastRemove(index);
 6                     return true;
 7                 }
 8         } else {//不为null,通过equals方法判断对象是否相等
 9             for (int index = 0; index < size; index++)
10                 if (o.equals(elementData[index])) {
11                     fastRemove(index);
12                     return true;
13                 }
14         }
15         return false;
16     }
17 
18 
19     private void fastRemove(int index) {
20         modCount++;
21         int numMoved = size - index - 1;
22         if (numMoved > 0)
23             System.arraycopy(elementData, index+1, elementData, index,
24                              numMoved);
25         elementData[--size] = null; // 
26     }
View Code

  remove(Object o)方法是删除第一次出现的该元素。然后通过System.arraycopy进行数组自身拷贝。

6、修改元素

  通过调用?set(int index, E element) 方法在指定索引 index 处的元素替换为 element。并返回原数组的元素。

1     public E set(int index, E element) {
2         rangeCheck(index);//判断索引合法性
3 
4         E oldValue = elementData(index);//获得原数组指定索引的元素
5         elementData[index] = element;//将指定所引处的元素替换为 element
6         return oldValue;//返回原数组索引元素
7     }

  通过调用 rangeCheck(index) 来检查索引合法性。

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

  当索引为负数时,会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。当索引大于集合长度时,会抛出?IndexOutOfBoundsException 异常。

7、查找元素

  ①、根据索引查找元素

1     public E get(int index) {
2         rangeCheck(index);
3 
4         return elementData(index);
5     }
View Code

  同理,首先还是判断给定索引的合理性,然后直接返回处于该下标位置的数组元素。

  ②、根据元素查找索引

 1     public int indexOf(Object o) {
 2         if (o == null) {
 3             for (int i = 0; i < size; i++)
 4                 if (elementData[i]==null)
 5                     return i;
 6         } else {
 7             for (int i = 0; i < size; i++)
 8                 if (o.equals(elementData[i]))
 9                     return i;
10         }
11         return -1;
12     }
View Code

  注意:indexOf(Object o) 方法是返回第一次出现该元素的下标,如果没有则返回 -1。

  还有?lastIndexOf(Object o) 方法是返回最后一次出现该元素的下标。

8、遍历集合

  ①、普通 for 循环遍历

  前面我们介绍查找元素时,知道可以通过get(int index)方法,根据索引查找元素,那么遍历同理:

1 ArrayList list = new ArrayList();
2 list.add("a");
3 list.add("b");
4 list.add("c");
5 for(int i = 0 ; i < list.size() ; i++){
6     System.out.print(list.get(i)+" ");
7 }
View Code

  ②、迭代器 iterator

  先看看具体用法:

1 ArrayList list = new ArrayList<>();
2 list.add("a");
3 list.add("b");
4 list.add("c");
5 Iterator it = list.iterator();
6 while(it.hasNext()){
7     String str = it.next();
8     System.out.print(str+" ");
9 }

  在介绍 ArrayList 时,我们知道该类实现了 List 接口,而 List 接口又继承了 Collection 接口,Collection 接口又继承了 Iterable 接口,该接口有个?Iterator iterator() 方法,能获取 Iterator 对象,能用该对象进行集合遍历,为什么能用该对象进行集合遍历?我们再看看 ArrayList 类中的该方法实现:

1     public Iterator iterator() {
2         return new Itr();
3     }

  该方法是返回一个 Itr 对象,这个类是 ArrayList 的内部类。

 1     private class Itr implements Iterator {
 2         int cursor;       //游标, 下一个要返回的元素的索引
 3         int lastRet = -1; // 返回最后一个元素的索引; 如果没有这样的话返回-1.
 4         int expectedModCount = modCount;
 5 
 6         //通过 cursor != size 判断是否还有下一个元素
 7         public boolean hasNext() {
 8             return cursor != size;
 9         }
10 
11         @SuppressWarnings("unchecked")
12         public E next() {
13             checkForComodification();//迭代器进行元素迭代时同时进行增加和删除操作,会抛出异常
14             int i = cursor;
15             if (i >= size)
16                 throw new NoSuchElementException();
17             Object[] elementData = ArrayList.this.elementData;
18             if (i >= elementData.length)
19                 throw new ConcurrentModificationException();
20             cursor = i + 1;//游标向后移动一位
21             return (E) elementData[lastRet = i];//返回索引为i处的元素,并将 lastRet赋值为i
22         }
23 
24         public void remove() {
25             if (lastRet < 0)
26                 throw new IllegalStateException();
27             checkForComodification();
28 
29             try {
30                 ArrayList.this.remove(lastRet);//调用ArrayList的remove方法删除元素
31                 cursor = lastRet;//游标指向删除元素的位置,本来是lastRet+1的,这里删除一个元素,然后游标就不变了
32                 lastRet = -1;//lastRet恢复默认值-1
33                 expectedModCount = modCount;//expectedModCount值和modCount同步,因为进行add和remove操作,modCount会加1
34             } catch (IndexOutOfBoundsException ex) {
35                 throw new ConcurrentModificationException();
36             }
37         }
38 
39         @Override
40         @SuppressWarnings("unchecked")
41         public void forEachRemaining(Consumersuper E> consumer) {//便于进行forEach循环
42             Objects.requireNonNull(consumer);
43             final int size = ArrayList.this.size;
44             int i = cursor;
45             if (i >= size) {
46                 return;
47             }
48             final Object[] elementData = ArrayList.this.elementData;
49             if (i >= elementData.length) {
50                 throw new ConcurrentModificationException();
51             }
52             while (i != size && modCount == expectedModCount) {
53                 consumer.accept((E) elementData[i++]);
54             }
55             // update once at end of iteration to reduce heap write traffic
56             cursor = i;
57             lastRet = i - 1;
58             checkForComodification();
59         }
60 
61         //前面在新增元素add() 和 删除元素 remove() 时,我们可以看到 modCount++。修改set() 是没有的
62         //也就是说不能在迭代器进行元素迭代时进行增加和删除操作,否则抛出异常
63         final void checkForComodification() {
64             if (modCount != expectedModCount)
65                 throw new ConcurrentModificationException();
66         }
67     }
View Code

  注意在进行 next() 方法调用的时候,会进行?checkForComodification() 调用,该方法表示迭代器进行元素迭代时,如果同时进行增加和删除操作,会抛出 ConcurrentModificationException 异常。比如:

 1 ArrayList list = new ArrayList<>();
 2 list.add("a");
 3 list.add("b");
 4 list.add("c");
 5 Iterator it = list.iterator();
 6 while(it.hasNext()){
 7     String str = it.next();
 8     System.out.print(str+" ");
 9     list.remove(str);//集合遍历时进行删除或者新增操作,都会抛出 ConcurrentModificationException 异常
10     //list.add(str);
11     list.set(0, str);//修改操作不会造成异常
12 }
View Code

  

  解决办法是不调用 ArrayList.remove() 方法,转而调用 迭代器的 remove() 方法:

1 Iterator it = list.iterator();
2 while(it.hasNext()){
3     String str = it.next();
4     System.out.print(str+" ");
5     //list.remove(str);//集合遍历时进行删除或者新增操作,都会抛出 ConcurrentModificationException 异常
6     it.remove();
7 }
View Code

  注意:迭代器只能向后遍历,不能向前遍历,能够删除元素,但是不能新增元素。

  ③、迭代器的变种 forEach

1 ArrayList list = new ArrayList<>();
2 list.add("a");
3 list.add("b");
4 list.add("c");
5 for(String str : list){
6     System.out.print(str + " ");
7 }
View Code

  这种语法可以看成是 JDK 的一种语法糖,通过反编译 class 文件,我们可以看到生成的 java 文件,其具体实现还是通过调用 Iterator 迭代器进行遍历的。如下:

1         ArrayList list = new ArrayList();
2         list.add("a");
3         list.add("b");
4         list.add("c");
5         String str;
6         for (Iterator iterator1 = list.iterator(); iterator1.hasNext(); System.out.print((new StringBuilder(String.valueOf(str))).append(" ").toString()))
7             str = (String)iterator1.next();
View Code

  ④、迭代器?ListIterator

  还是先看看具体用法:

 1 ArrayList list = new ArrayList<>();
 2 list.add("a");
 3 list.add("b");
 4 list.add("c");
 5 ListIterator listIt = list.listIterator();
 6 
 7 //向后遍历
 8 while(listIt.hasNext()){
 9     System.out.print(listIt.next()+" ");//a b c
10 }
11 
12 //向后前遍历,此时由于上面进行了向后遍历,游标已经指向了最后一个元素,所以此处向前遍历能有值
13 while(listIt.hasPrevious()){
14     System.out.print(listIt.previous()+" ");//c b a
15 }
View Code

  还能一边遍历,一边进行新增或者删除操作:

 1 ArrayList list = new ArrayList<>();
 2 list.add("a");
 3 list.add("b");
 4 list.add("c");
 5
            
打赏
神奇推荐位