Java集合合集

Java集合框架主要分为两种类型容器
一种是集合Collection(有3种子类型:List、Set、Queue);一种是图Map

ListSetMap
有序不唯一无序唯一无序key唯一非空
实现类ArrayList、LinkedList、Vector实现类HashSet、TreeSet实现类HashMap、
检索效率高,删除插入效率低会影响元素位置检索效率低,删除插入效率高且不会改变位置
SortedSet(TreeSet)有序唯一SortedMap(TreeMap)保持key有序

ArrayList

底层结构

ArrayList继承了AbstractList
底层结构是一个动态数组,对ArrayList的所有操作底层都是基于数组进行的
实现了RandomAccess接口(是一个标记接口),可以快速访问,具体表现为可以根据下标快速访问元素,因此读速度相对比较快。

线程安全性

ArrayList的写操作分为两个步骤,插入的时候首先在Object[size]的位置上放入新的元素,然后将size+1
就此而言,在多线程环境下已不具备原子性,所以是线程不安全的,更甚者如果是在指定位置插入元素,会涉及更多的步骤,要先移动待插入位置之后的元素,再将元素插入,然后size+1,所以写速度相对来说比较慢。

A线程进行add操作,假设size=0,将元素写入尚未修改size之前,cpu调度A暂停,B线程执行add操作,因为size尚未修改,B将元素也插入到0位置覆盖了A 的元素,之后AB都执行完修改size得到size=2,但实际集合中只有一个元素,造成线程不安全。

既然线程不安全,如何在多线程情况下保证线程安全呢

1.Vector:使用同步方法实现线程安全,扩容扩充为员数组的2倍
2.Collections.synchronizedList:使用同步代码块实现,然后调用传入的list的自己的方法,使用迭代器遍历的时候需要手动进行同步处理;
3.CopyOnWriteArrayList :Juc包下的写时复制集合

List<String> arrayList=new Vector<>();
List<String> arrayList= Collections.synchronizedList(new ArrayList<>());
//由于Collections.synchronizedList中未对迭代器遍历进行同步处理,所以当时有迭代器遍历或增强for时需要手动进行同步处理
synchronized(this.list){
    for (String s:this.list){
    	System.err.println(Thread.currentThread().getName()+":"+s);
    }
}
//查询的时候,不加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据
CopyOnWriteArrayList list=new CopyOnWriteArrayList();

Vector与synchronizedList的区别
Vector是将整个方法进行了同步,而synchronizedList是将ArrayList的方法用同步代码块包了起来,而彼此的方法又相差无几,所以在锁定范围上两者并无区别,Vector锁定对象是当前实例对象this,而synchronizedList有一个构造函数可以传入一个对象,如果没有指定就是当前实例对象this

CopyOnWriteArrayList : 采用ReentrantLock,lock.lock(); lock.unlock();
优点:适合一些读多写少的数据,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。缺点:这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。

动态扩容

官放介绍初始容量为10,提供了三个构造方法

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//参数为集合
public ArrayList(Collection<? extends E> c) {
		//集合参数转过来的数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //转换的数组类型不是Object[],拷贝为Object[],这里Arrays.copyof方法实际是调用System.arraycopy
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //返回一个空的数组,EMPTY_ELEMENTDATA是一个空的数组,并未指定默认长度10
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
//参数为初始长度
public ArrayList(int initialCapacity) {
		//校验长度参数的可用性,>0初始化指定长度,=0初始化空数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
//无参
public ArrayList() {
		//DEFAULTCAPACITY_EMPTY_ELEMENTDATA也是一个空数组,并未指定默认长度10
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

看了构造方法,并未找到初始化长度为10的影子,我们不妨先来看看初始化之后,是如何添加元素的

private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
	//如果为空,取初始值和最小值中的较大值,可以看到DEFAULT_CAPACITY =10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;//修改次数+1,这个参数主要是用在集合的Fail-Fast机制(即快速失败机制)的判断中使用的
    //如果最小长度大于当前长度,进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
	//当前容量
    int oldCapacity = elementData.length;
    //新容量扩充为当前1.5倍,>>1为位运算的右移,即二进制去掉末尾一位相当于除以2
    //比如10的2进制为1010,10>>1 即1010除掉末尾1位变为二进制的101即十进制的5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新容量小于最小容量,扩充为最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //然后再一步判断,扩充后的容量大于最大长度(Integer的最大长度-8)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //如果最小长度大于最大长度,扩容为Integer的最大长度,否则扩充为Integer的最大长度-8
    return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

我们可以看到在add的时候进行了初始容量的设置,并且进行了扩容,复制到新的数组,将新元素插入新的数组;

遍历踩坑

Iterator被创建之后,进行每一步操作都会比对当前list的操作次数modCount,如果遍历途中,对元素进行了写入或删除操作,Iterator会立即抛出ConcurrentModificationException异常。所以就有了几种骚操作:如何在遍历过程中删除元素

Iterator iterator=arrayList.iterator();
while (iterator.hasNext()){
    System.err.println(iterator.next());
    if (iterator.next().toString().endsWith("499")){
        //arrayList.remove(iterator.next());//会报错 ConcurrentModificationException
        iterator.remove();//如此甚好
    }
}  

为什么会这样呢?查看源码ArrayList.remove()会将modCount++,而Iterator创建之后modCount不再改变,每次遍历都会比对这个值,发现不同立即抛出异常,而反观Iterator自己的remove(),在删除当前元素的同时保证了modCount的一致性。另外我们还可以从末尾元素开始遍历元素并移除

//ArrayList
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;
}
//Iterator
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();
    }
}

LinkedList

底层结构

双向链表

未完待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值