Java基础-容器List

160 阅读8分钟

1 容器架构图

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、 List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是 collection的子接口。

Collection集合主要有List和Set两大接口

  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重 复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、 LinkedHashSet 以及 TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap

image.png

2 容器底层数据结构

2.1 List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向循环链表

2.2 Set

  • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基 于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

2.3 Map

  • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主 体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是 基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面 结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。 同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为 了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

3 List

3.1 ArrayList

3.1.1 简介

ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess(随机访问), Cloneable(克隆), java.io.Serializable(可序列化)这些接口。

image.png
  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。   
  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。   
  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。   
  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输
  • 和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList

3.1.2 源码解读

// 默认容量是10
private static final int DEFAULT_CAPACITY = 10;
// 如果容量为0的时候,就返回这个数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 使用默认容量10时,返回这个数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素存放的数组
transient Object[] elementData;
// 元素的个数
private int size;

// 记录被修改的次数
protected transient int modCount = 0;
// 数组的最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
  • 如果不传入初始容量,就使用默认容量,并设置elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA

  • 如果传入初始容量,会判断这个传入的值,如果大于0,就new一个新的Object数组,如果等于0,就直接设置elementDataEMPTY_ELEMENTDATA

  • 如果传入一个Collection,则会调用toArray()方法把它变成一个数组并赋值给elementData。同样会判断它的长度是否为0,如果为0,设置elementDataEMPTY_ELEMENTDATA

3.1.3 扩容

// 扩容一个
private Object[] grow() {
	return grow(size + 1);
}

// 保证扩容到期望容量minCapacity及以上
private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));
}

// 根据期望容量minCapacity计算实际需要扩容的容量
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length; // 得到旧容量
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 设置新容量为旧容量的1.5倍
    if (newCapacity - minCapacity <= 0) { // 如果新容量仍然小于期望容量
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // 如果是使用的默认容量
            return Math.max(DEFAULT_CAPACITY, minCapacity); // 取默认容量和期望容量较大值返回
        if (minCapacity < 0) // overflow // 检查期望容量是否越界(int 的范围)
            throw new OutOfMemoryError();
        return minCapacity; // 返回期望容量
    }
    // 如果新容量大于期望容量,判断一下新容量是否越界
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

3.1.4 缩容

ArrayList没有缩容。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。

3.1.5 遍历

  1. 第一种,通过迭代器遍历。即通过Iterator去遍历。
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}
复制代码
  1. 第二种,随机访问index,通过索引值去遍历。 由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
    value = (Integer)list.get(i);        
}
复制代码
  1. 第三种,增强for循环遍历。如下:
Integer value = null;
for (Integer integ:list) {
    value = integ;
}
复制代码

遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低

3.2 LinkedList

3.2.1 简介

LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList的本质是双向链表。

image.png
  • LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
  • LinkedList包含两个重要的成员:header 和 size。 header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量:previous, next, element。(其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。)
  • size是双向链表中节点的个数。   
  • LinkedList 实现List接口,能对它进行数组操作。  
  • LinkedList 实现 Deque接口,即能将LinkedList当作双端队列使用。   
  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。   
  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  • LinkedList 是非同步的。(若要实现同步 List list = Collections.synchronizedList(new LinkedList(...));)

3.2.2 分析

  • 访问性 LinkedList实际上是通过双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低
  • 根据索引值操作 既然LinkedList是通过双向链表的,但是它也实现了List接口,也就是说,它实现了get(int index)remove(int index)等根据索引值来获取、删除节点的函数。
  • LinkedList是如何实现List的这些接口的,如何将双向链表和索引值联系起来的?其实,它是通过一个计数索引值来实现的。例如,当程序调用get(int index)方法时,首先会比较location双向链表长度的1/2;如果前者大,则从链表头开始向后查找,直到location位置;否则,从链表末尾开始向前查找,直到location位置

3.2.3 遍历

支持多种遍历方式。建议不要采用随机访问的方式去遍历LinkedList,而采用逐个遍历的方式。

  1. 第一种,通过迭代器遍历。即通过Iterator去遍历。
for(Iterator iter = list.iterator(); iter.hasNext();)
    iter.next();
复制代码
  1. 通过快速随机index访问遍历LinkedList
int size = list.size();
for (int i=0; i<size; i++) {
    list.get(i);        
}
复制代码
  1. 通过另外一种增强版for循环来遍历LinkedList
for (Integer ele: list) {

}
复制代码
  1. 通过pollFirst()来遍历LinkedList,获取并移除此列表的第一个元素;如果此列表为空,则返回 null
while(list.pollFirst() != null){

}
复制代码
  1. 通过pollLast()来遍历LinkedList,获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
while(list.pollLast() != null) {

 }
复制代码
  1. 通过removeFirst()来遍历LinkedList,移除并返回此列表的第一个元素。 NoSuchElementException - 如果此列表为空。
try {
    while(list.removeFirst() != null) {
	
	}
} catch (NoSuchElementException e) {
}
复制代码
  1. 通过removeLast()来遍历LinkedList,移除并返回此列表的最后一个元素。NoSuchElementException - 如果此列表为空。
try {
    while(list.removeLast() != null) {
     
     }
} catch (NoSuchElementException e) {
}

3.3 CopyOnWriteArrayList

3.3.1 Copy-On-Write

顾名思义,在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行操作,而是将内存拷贝一份,在新的内存中进行操作,完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉嘛!

3.3.2 简介

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayListCopyOnWriteArraySetCopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

3.3.3 源码解读

add()方法源码:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;//重入锁
        lock.lock();//加锁啦
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组
            newElements[len] = e;
            setArray(newElements);//将引用指向新数组  1
            return true;
        } finally {
            lock.unlock();//解锁啦
        }
    }

原来add()在添加集合的时候加上了锁,保证了同步,避免了多线程写的时候会Copy出N个副本出来。CopyOnWriteArrayList是怎么解决线程安全问题的?答案就是----写时复制,加锁

3.3.4 优缺点

缺点:

  • 1、耗内存(集合复制)
  • 2、实时性不高 优点:
  • 1、数据一致性完整,为什么?因为加锁了,并发数据不会乱
  • 2、解决了像ArrayListVector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!

3.3.5 适应场景

  • 1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为写的时候会复制新集合
  • 2、集合不大,为什么?因为写的时候会复制新集合
  • 实时性要求不高,为什么,因为有可能会读取到旧的集合数据