Java容器相关(1)-- List

其中TreeMap、TreeSet有序指的是里面的元素按照大小排序,List、LinkedHashMap、LinkedHashSet有序指的是元素按照插入的顺序存储。


    队列继承图,Deque接口又有ArrayDeque和LinkedList、ConcurrentLinkedDeque实现类及BlockingDeque接口,其中BlockingDeque接口有LinkedBlockingDeque并发实现类。

一、List

 1)ArrayList:

    概括的说,ArrayList是一个动态数组,它是线程不安全的,允许元素为null。

其底层数据结构依然是数组,它实现了List, RandomAccess, Cloneable,

java.io.Serializable接口,其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。

    因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length,数组的默认大小为10),所以它也有数组的缺点,空间效率不高。由于数组的内存连续,可以根据下标以O1的时间读写(改查)元素,因此时间效率很高。

    当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率。

基本操作总结:

(1)增加元素的操作:

 增加单个元素的方法是add(E e)和add(int index, E e)。这两个方法都是向容器中添加新元素,这可能会导致capacity不足,因此在添加元素之前,都需要进行剩余空间检查,如果需要则自动扩容。

addAll()方法能够一次添加多个元素,根据位置不同也有两个版本,一个是在末尾添加的addAll(Collection c)方法,一个是从指定位置开始插入的addAll(int

index, Collection c)方法。跟add()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。 addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

public booleanadd(Ee) {

    ensureCapacityInternal(size+ 1);  // Increments modCount!!

    elementData[size++] =e;

    return true;

}

private void ensureCapacityInternal(intminCapacity) {

    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

        minCapacity=Math.max(DEFAULT_CAPACITY, minCapacity);

    }

    ensureExplicitCapacity(minCapacity);

}

private void ensureExplicitCapacity(intminCapacity) {

    modCount++;

    // overflow-conscious code

    if (minCapacity - elementData.length > 0)

        grow(minCapacity);

}

private void grow(intminCapacity) {

    // 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);

}


小结:

先判断是否越界,是否需要扩容。

如果扩容, 就复制数组。

然后设置对应下标元素值。


值得注意的是:

1 如果需要扩容的话,默认扩容一半。如果扩容一半不够,就用目标的size作为扩容后的容量。

2 在扩容成功后,会修改modCount


(2)删除元素的操作:

    会修改modCount。remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。removeAll(Collection c)方法批量删除一个列表的元素,用removeAll可以实现两个集合的差集。

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。

publicEremove(intindex) {

    rangeCheck(index);

    modCount++;

    EoldValue=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

    returnoldValue;

}



(3)修改元素的操作:

     不会修改modCount,相对增删是高效的操作。


public E set(int index, E element) {

    rangeCheck(index);//越界检查

    E oldValue =elementData(index); //取出元素

    elementData[index] =element;//覆盖元素

    return oldValue;//返回元素

}


(4)查找元素的操作:

     不会修改modCount,相对增删是高效的操作。


public E get(int index) {

    rangeCheck(index);//越界检查

    returnelementData(index); //下标取数据

}

E elementData(int index) {

    return (E)elementData[index];

}


(5)清空,clear

     会修改modCount。


public void clear() {

    modCount++;//修改modCount

    // clear to let GC do its work

    for (int i = 0; i

        elementData[i] =null;

    size = 0; //修改size

}


(6)包含contain

//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。

public boolean contains(Object o) {

    return indexOf(o) >=0;

}

//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。

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;

}

增删改查中,增导致扩容,则会修改modCount,删一定会修改。改和查一定不会修改modCount。

扩容操作会导致数组复制,批量删除会导致 找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。而改、查都是很高效的操作。


Fail-fast:

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

出现的场景:

在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。


出现的原理:

     public static voidmain(String[] args) {

          List list = new ArrayList<>();

           for (int i = 0 ;i < 10 ; i++ ) {

                list.add(i +"");

           }

          Iterator iterator = list.iterator();

           int i = 0 ;

          while(iterator.hasNext()) {

                if (i == 3){

                    list.remove(3);

                }

               System.out.println(iterator.next());

                i ++;

           }

     }

看看最关心的next()方法,看看为什么在迭代过程中,如果有线程对集合结构做出改变,就会发生fail-fast:

      @SuppressWarnings("unchecked")

        public E next() {

            checkForComodification();

            int i = cursor;

            if (i >=size)

                throw newNoSuchElementException();

            Object[]elementData = ArrayList.this.elementData;

            if (i >=elementData.length)

                throw newConcurrentModificationException();

            cursor = i + 1;

            return (E)elementData[lastRet = i];

        }

从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:

        final voidcheckForComodification() {

            if (modCount !=expectedModCount)

                throw newConcurrentModificationException();

        }


可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount

时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。


序列化时也可能发生fail-fast:

private voidwriteObject(java.io.ObjectOutputStreams)

    throwsjava.io.IOException{

    // Write out element count, and any hidden stuff

    int expectedModCount =modCount;

    s.defaultWriteObject();


    // Write out size as capacity for behavioural compatibility with clone()

    s.writeInt(size);


    // Write out all elements in the proper order.

    for (int i=0; i

        s.writeObject(elementData[i]);

    }


    if (modCount !=expectedModCount) {

        throw newConcurrentModificationException();

    }

}


避免fail-fast:

方法1

在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码:

        public void remove(){

            if (lastRet <0)

                throw newIllegalStateException();

           checkForComodification();


            try {

               ArrayList.this.remove(lastRet);

                cursor =lastRet;

                lastRet =-1;

               expectedModCount = modCount;

            } catch(IndexOutOfBoundsException ex) {

                throw newConcurrentModificationException();

            }

        }

可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。


方法2

使用java并发包(java.util.concurrent)中的类来代替ArrayList 和hashMap。

比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。


替代方案:

可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List list = new ArrayList<>();

List synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List list = new CopyOnWriteArrayList<>();


Vector

* Vector与其的区别在于Vector在API上都加了synchronized所以它是线程安全的,Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;

* Vector扩容时,是翻倍size,而ArrayList是扩容到1.5倍。


2)LinkedList:

概括的说,LinkedList是线程不安全的,允许元素为null的双向链表。

其底层数据结构是双向链表,它实现List, Deque, Cloneable,

java.io.Serializable接口,它实现了Deque,所以它也可以作为一个双端队列。和ArrayList比,没有实现RandomAccess所以其以下标,随机访问元素速度较慢。

因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高。不需要批量扩容,也不需要预留空间,所以空间效率比ArrayList高。

缺点就是需要随机访问元素时,时间效率很低,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询,以提升时间效率。不过随着n的增大,总体时间效率依然很低。


基本操作总结:

节点Node结构:

private static class Node {

    E item;//元素值

    Node next;//后置节点

    Node prev;//前置节点


    Node(Node prev,E element, Node next) {

        this.item = element;

        this.next = next;

        this.prev = prev;

    }

}

(1)增加元素的操作:

    增加单个元素的add()方法有两个版本,一个是add(E e),该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间。只需要简单修改几个相关引用即可;另一个是add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。

    批量增加元素的addAll()的方法:

    链表批量增加,是靠for循环遍历待插入数组,依次执行插入节点操作。对比Array

List是通过System.arraycopy完成批量增加的;

通过下标获取某个node的时候(增、查),会根据index处于前半段还是后半段进行一个折半,以提升查询效率。


(2)删除元素的操作:

remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove

(Object o),另一个是删除指定下标处的元素remove(int index)。

两个删除操作都要:1.先找到要删除元素的引用,2.修改相关引用,完成删除操作。

在寻找被删元素引用的时候remove(Object o)调用的是元素的equals方法,而remove(int index)使用的是下标计数,两种方式都是线性时间复杂度。在步骤2中,两个revome()方法都是通过unlink(Node x)方法完成的。这里需要考虑删除元素是第一个或者最后一个时的边界情况。


总结:

链表批量增加,是靠for循环遍历待插入数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount;

通过下标获取某个node的时候(增、查),会根据index处于前半段还是后半段进行一个折半,以提升查询效率;

删也一定会修改modCount。按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node;

改也是先根据index找到Node,然后替换值。改不修改modCount;

查本身就是根据index找到Node。


为什么Java里的Arrays.asList生成的List不能用addremove方法?

在平时的开发过程中,我们知道可以将一个Array的对象转化为List。这样的操作,我们只要采用Arrays.asList这个方法就行了。前段时间一直用这个方法,有一天,我发现通过Arrays.asList得到的List无法进行add和remove等操作。

下面是一段很简单的测试代码:

    public class MainFacade{

        public static voidmain(String[] args) {

           List list = Arrays.asList(1,2,3);

            list.add(5);

           System.out.print(list.toString());

        }

}

不过上面的代码会throw出一个UnsupportedOperationException这样的异常:

Exception in thread "main"java.lang.UnsupportedOperationException atjava.util.AbstractList.add(AbstractList.java:148) atjava.util.AbstractList.add(AbstractList.java:108) atorg.popkit.MainFacade.main(MainFacade.java:14) atsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:606) atcom.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

终其原因是Arrays.asList方法返回的ArrayList是继承自AbstractList同时实现了RandomAccess和Serializable接口,定义如下: 

    private static classArrayList extends AbstractList

        implementsRandomAccess, java.io.Serializable

我们再来看看AbstractList这个类的定义:

public abstract class AbstractList extendsAbstractCollection implements List

这时我们发现AbstractList这个类的set add remove方法定义如下:

    public void add(intindex, E element) {

        throw newUnsupportedOperationException();

    }


    public E set(int index,E element) {

        throw newUnsupportedOperationException();

    }


    public E remove(intindex) {

        throw newUnsupportedOperationException();

}

现在知道了它throw UnsupportedOperationException异常的原因了。 

通过上面的分析,我们知道,其实通过asList方法得到的List是只读的,那么平时我们怎样避免这样的错误发生?我们可以采用如下方法: 

List list = newArrayList<>(Arrays.asList(1,2,3));


为什么通过new生成的List可以正常执行add、remove等操作呢?

public class ArrayList extends AbstractList

        implementsList, RandomAccess, Cloneable, java.io.Serializable

因为在ArrayList中覆盖和重新实现了父类AbstractList中的add和remove等方法。

你可能感兴趣的:(Java容器相关(1)-- List)