Featured image of post Java容器总结

Java容器总结

Java 容器分为两大类:Collection、Map,思维导图展示他们之间的关系: java 容器

Collection

list

Java List 一共三个实现类: 分别是 ArrayList、Vector 和 LinkedList。

ArrayList

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数 组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数 组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进 行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除

LinkedList 是用链表结构存储数据的,**很适合数据的动态插入和删除,随机访问和遍历速度比较慢。**另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆 栈、队列和双向队列使用。

Vector

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此, 访问它比访问 ArrayList 慢。

Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

HashSet

基于哈希表实现,存入数据是按照哈希值,所以并不是按照存入的顺序排序,为保证存入的唯一性,存入元素哈希值相同时,会使用 equals 方法比较,如果比较出不同就放入同一个哈希桶里。

TreeSet

基于红黑树实现,支持有序性操作,每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

LinkedHashSet( HashSet+LinkedHashMap )

继承于 HashSet、又是基于 LinkedHashMap 来实现的, 具有 HashSet 的查找效率,又有有序性。

Queue

ArrayBlockingQueue

底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。

####LinkedBlockingQueue 底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。

DelayQueue

是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。

SynchronousQueue

本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。

PriorityBlockingQueue

是无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。

Map

HashMap

根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快 的访问速度,但遍历顺序却是不确定的。 HashMap 非线程安全,底层实现为数组+链表+红黑树。

ConcurrentHashMap

支持并发操作的HashMap,在JDK1.7和1.8实现线程安全的方式不同。

HashTable

Hashtable 是遗留类,不建议使用,很多映射的常用功能与 HashMap 类似,通过 synchronized 把整个(table)表锁住来实现线程安全的,效率十分低下。 TreeMap: 红黑树实现,可排序,需要对一个有序的key集合进行遍历时建议使用。 LinkHashMap: 是 HashMap 的一个子类, 增加了一条双向链表, 从而可以保存记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

重点容器原理分析

ArrayList

定义

1
2
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 是基于数组实现的,所以支持快速随机访问。 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。

RandomAccess 接口标识着该类支持快速随机访问(只是一个定义了类型的接口,无作用)。 Cloneable 接口,即覆盖了函数 clone(),能被克隆。 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

数组的默认大小为 10。

扩容机制

核心方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
}

整个流程就是对旧数组位移运算得到新数组,然后把旧数组整个复制到新数组上,操作代价很高,新容量的大小为 oldCapacity + (oldCapacity » 1),也就是旧容量的 1.5 倍;如果可以预计需要容量大小情况下建议指定合适容量。

补充:

移位运算符简介:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:«(左移)、»(带符号右移)和»>(无符号右移)。作用:对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源。在阅读源码中比较常见。

添加和删除

在末尾添加元素:

1
2
3
4
5
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

这里看到ArrayList在末尾添加元素的实质就相当于为数组赋值。 在指定位置添加元素:

1
2
3
4
5
6
7
public void add(int index, E element) {    
    rangeCheckForAdd(index);   	
    ensureCapacityInternal(size + 1);  // Increments modCount!! 
    System.arraycopy(elementData, index, elementData, index + 1, size - index);    
    elementData[index] = element;    
    size++;
}

让数组自己复制自己实现让index开始之后的所有成员后移一个位置。

*删除指定元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;
}

流程是把需要删除的元素右边的元素向左移动一位,覆盖了需要删除的元素。调用了arraycopy,所以操作代价也很高。

CopyOnWriteArrayList

concurrent 并发包下的类,是ArrayList的线程安全解决方案, 通过ReentrantLock获取对象锁的方式来实现线程安全。

读:

1
2
3
4
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

写的操作需要加锁,防止并发写入导致数据丢失,不直接操作原数组,先copy一个数组进行操作,写完后setArray方法把新的复制数组赋值给旧数组。

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

缺点:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;

  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

LinkedList

内部私有类Node:

1
2
3
4
5
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

定义:

1
2
transient Node<E> first;
transient Node<E> last;

补充:transient关键字标记的成员变量不参与序列化过程。

可看出,LinkedList是由双向列表实现,使用Node存储节点信息,每个节点都有前节点(next),本节点(item),后节点(prev)。

与 ArrayList 的比较

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。
  • ArrayList插入删除元素时分两种情况:①把元素添加到末尾所需的时间复杂度是O(1),②在指定位置添加删除元素时就需要移动整个数组,操作代价就比较大了。LinkedList 对于添加删除方法只需要断链然后更改指向,所需的消耗就很小了。
  • ArrayList 支持高效的随机元素访问 而LinkedList 不支持。
  • ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

HashMap

原理分析

HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组

1
2
3
4
5
6
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
...}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。

hash:hashcode经过扰动函数得到的值, 然后通过 (n - 1) & hash 判断当前元素存放的位置,如果当前位置存在hash和key值不相同的元素就使用拉链法解决冲突。

“拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

Node<K,V> next:next就是用于链表的指向。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

底层存贮结构: HashMap

put方法分析

map.put(“a”,“b”)的整个流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
1. 先判断散列表是否没有初始化或者为空,如果是就扩容
2. 根据键值 key 计算 hash 值,得到要插入的数组索引 
3. 判断要插入的那个数组是否为空:
  	1. 如果为空直接插入。
  	2. 如果不为空,判断 key 的值是否是重复(用 equals 方法):
       	1. 如果是就直接覆盖
       	2. 如果不重复就再判断此节点是否已经是红黑树节点:
            	1. 如果是红黑树节点就把新增节点放入树中
            	2. 如果不是,就开始遍历链表:
                 	1. 循环判断直到链表最底部,到达底部就插入节点,然后判断是否大于链表长度是否大于8:
                      1. 如果大于8就转换为红黑树
                      2. 如果不大于8就继续下一步
                	2. 到底部之前发现有重复的值,就覆盖。
4. 判断是否需要扩容,如果需要就扩容。

对应源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //tab: 引用当前hashMap的散列表
	//p: 表示当前散列表的元素
	//n: 表示散列表数组的长度
	//i: 表示路由寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容(采用了延时初始化,第一次put才会初始化散列表。)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 寻址找到的桶位为null
    //(n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 寻址找到的桶位已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等,也就是判断是否是重复的值。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 完全一致
            	//e:找到的一个与当前要插入的元素一直的元素
                e = p;
        // hash值不相等,即key不相等;且为红黑树结点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //替换
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
} 

综上put的方法流程图为: HashMap put

备注: JDK1.7之前的put方法和现在流程不同的地方就是采用头插法插入元素。

扩容(resize方法)

扩容会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 源码在下面,仔细阅读即可理解此方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //散列表已经被初始化了,是一次正常扩容
    if (oldCap > 0) {
        // 超过最大值就不再扩充了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold:翻倍
    }
     //散列表没有被初始化
    else if (oldThr > 0) // 初始化的容量是已经指定了的
        newCap = oldThr;
    else { 
        // 默认初始化容量
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
    }
    // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //当前桶位有数据,但不清楚是什么数据。
            if ((e = oldTab[j]) != null) {
                //方便 GC
                oldTab[j] = null;
                //如果是单个元素
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是红黑树
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //桶位已经形成链表
                    Node<K,V> loHead = null, loTail = null;//低位链表
                    Node<K,V> hiHead = null, hiTail = null;//高位链表
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引,给低位链表赋值
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap,给高位链表赋值
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到桶里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到桶里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

在扩容时看原hash值新增的那个bit位是1还是0就好了,是0的话索引没有变,是1的话索引变成“原索引+oldCap(旧数组大小)”,下图位resize()方法示意图: 扩容

ConcurrentHashMap

JDK1.7:

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。 Segment 继承自 ReentrantLock ,** 默认的并发级别为16 **。

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每 个 Segment 是线程安全的,也就实现了全局的线程安全。

结构如图:

JDK1.8:

使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

HashSet

实现原理: HashSet底层由HashMap实现 ,值存放于HashMap的key上 ,HashMap的value统一为PRESENT 。

检查重复: 先对插入的元素的hashcode值和现有的元素的hashcode作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,直接插入。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。 如果两者相同,HashSet就不会让加入操作成功 。

LinkedHashMap

LinkedHashMap 实现LRU(least recently used)

  • 设定最大缓存空间 MAX_ENTRIES 为 3;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}

TreeMap

是一个有序的key-value集合,它是通过红黑树实现的。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

通过阅读 TreeMap 的 put 方法的源码发现:TreeMap 实现元素不重复就是通过调用 compareTo 方法,而要使用 compareTo 方法就必须实现Compare接口。

源码分析看到,相比HashMap来说,TreeMap多继承了一个接口NavigableMap,也就是这个接口,决定了TreeMap与HashMap的不同:HashMap的key是无序的,TreeMap的key是有序的

接口NavigableMap

NavigableMap继承了SortedMap。SortedMap就像其名字那样,说明这个Map是有序的。这个顺序一般是指由Comparable接口提供的keys的自然序(natural ordering),或者也可以在创建SortedMap实例时,指定一个Comparator来决定。 当我们在用集合视角(collection views,与HashMap一样,也是由entrySet、keySet与values方法提供)来迭代(iterate)一个SortedMap实例时会体现出key的顺序。 这里引申下关于Comparable与Comparator的区别:

  • Comparable一般表示类的自然序,比如定义一个Student类,学号为默认排序;
  • Comparator一般表示类在某种场合下的特殊分类,需要定制化排序。比如现在想按照Student类的age来排序;

插入SortedMap中的key的类类都必须继承Comparable类(或指定一个comparator),这样才能确定如何比较(通过k1.compareTo(k2)或comparator.compare(k1, k2))两个key,否则,在插入时,会报ClassCastException的异常。

此为,SortedMap中key的顺序性应该与equals方法保持一致。也就是说k1.compareTo(k2)或comparator.compare(k1, k2)为true时,k1.equals(k2)也应该为true。

介绍完了SortedMap,再来回到我们的NavigableMap上面来。 NavigableMap是JDK1.6新增的,在SortedMap的基础上,增加了一些“导航方法”(navigation methods)来返回与搜索目标最近的元素。例如下面这些方法:

  • lowerEntry,返回所有比给定Map.Entry小的元素;

  • floorEntry,返回所有比给定Map.Entry小或相等的元素;

  • ceilingEntry,返回所有比给定Map.Entry大或相等的元素;

  • higherEntry,返回所有比给定Map.Entry大的元素;

Built with Hugo     主题 StackJimmy 设计