概述
首先看一下集合类的继承关系
ArrayList
- List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。
- 非线程安全
- 底层使用的数据结构为数组
- 适合查改,弱于增删
方法介绍
set()
public E set(int index, E element)
将下标为 index 的元素替代为 element,并返回旧值
add()
public boolean add(E e)
在末尾添加新的成员 e,速度较快,返回是否成功
public void add(int index, E element)
在指定位置添加新的成员 e,该位置及以后的原始成员须往后移动。由于涉及大量数据的移动,速度不如上一种方式。
remove()
public E remove(int index)
删除指定位置的元素,涉及数据的移动。返回该位置旧值
ensureCapacity()
public void ensureCapacity(int minCapacity)
数组扩容扩展,主要是修改 length。注意这里是修改容量 length,而不是存储的元素个数 size。默认按照 length 的1.5倍方式扩展,如果扩展之后还是 < minCapacity,就设置 length 为 minCapacity。
LinkedList
- List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null)
- 实现 Deque 接口,为 add()、poll() 提供先进先出队列操作以及其他堆栈和双端队列操作,所以每个 Node 都有属性关于前一个节点和后一个节点的引用
- 非线程安全
- 适合增删,弱于查改
Node()
Node(Node<E> prev, E element, Node<E> next)
内部节点的构造方法,prev 为前一个 Node 的引用,element 为包含数据的元素属性,next 为后一个 Node 引用。
node()
Node<E> node(int index)
获取下标为 index 的 Node 对象
add()
public void add(int index, E element)
在指定位置添加结点
addFirst()
public void addFirst(E e)
在链表开头添加结点
addLast()
public void addLast(E e)
在链表末尾添加元素
remove()
public boolean remove(Object o)
通过对象删除某个结点
public E remove(int index)
通过下标删除某个结点
public E remove()
删除第一个结点
public E removeFirst()
删除第一个结点
public E removeLast()
删除最后一个结点
比较
综合 ArrayList 和 LinkedList 的特性,比较发现:
- ArrayList 适合查询修改数量多于增删的场合
- LinkedList 适合增删数量多于查询修改的场合
HashMap
- 基于哈希表的 Map 接口实现。提供所有可选的映射操作,并且允许使用 null 值和 null 键(只有一个键)。
- 非线程安全
- 不保证映射的顺序,特别是它不保证顺序的不变
- 实现方式是首先用 key 经过 hash 函数计算出一个 hashcode。首先不同的 hashcode 对应数组的一个元素,对应同一个数组元素的 key-value 再存到一个链表里面。
类似于如下的结构
Node()
Node(int hash, K key, V value, Node<K,V> next)
结点的构造函数
hash 为 key 的 hashcode,使用 hash() 方法计算而来,next 是下一个结点的引用
注意 HashMap 里面的链表是单向链表
put()
public V put(K key, V value)
插入一个键为 key,值为 value 的结点,如果 key 已经存在,返回旧值,否则返回 null
hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里放一下 hash() 函数的一种实现方式
static final int hash(int h) {
h ^= (h>>>20)^(h>>>12);
return h^(h>>>7)^(h>>>4);
}
上面是一种更复杂一些的 hash() 函数实现
get()
public V get(Object key)
返回键为 key 的 value,如果 hashmap 中没有 key,就会返回 null
总结
HashMap 实现的过程中 hashcode 对应数组的元素有可能会相同,这时候就会有碰撞。对应到同一个数组元素的 key-value 用一个链表存储。如果碰撞的频率太高就会严重影响查询的效率(理论上 hash 查找的复杂度最低为 O(1))。当 hashcode 数组越满的时候,碰撞率就会增高。所以当数组快满的时候就会进行数组的扩容。但是这个扩容是非常耗时的。默认当数组中 75% 的容量存储了数据的时候就会进行一次扩容,每次容量翻倍。这时就会对每一个键重新根据 hashcode 指定数组的位置,耗时是很大的。
- HashTable 和 HashMap 采用相同的存储机制,二者的实现本质是一致的
- 不允许有 null 值得存在
- HashTable 是线程安全的,内部方法基本都是同步的
- 迭代器具有强一致性
TreeMap
- Map 接口的树实现
- 不允许有 null 值得存在
- 非线程安全
- 键有序
TreeMap 使用的收据结构是红黑树,红黑树是平衡二叉树的修改版本。AVL 树(平衡二叉树)是严格的,但是插入删除的时候会涉及较多的旋转。红黑树通过将结点标记为 red 和 black 和一些手段,使得深度比 AVL 数略深,但是旋转次数更少。所以性能上可能略微优一点,但是差别不大。
结点的 key 就是红黑树用于比较的值,所以 TreeMap 天然是键有序的。
TreeMap 的优势
- 空间利用率高。由于 HashMap 每次扩容是容量翻倍的,使得一些空间会被浪费,最多有超过一半的空间会被浪费。而 TreeMap 中树的每一个结点就代表了一个元素,不会浪费空间。
- 性能稳定。Hash 碰撞的时候会导致 HashMap 的查询开销较大,HashMap 扩容的时候需要重新计算,开销较大。TreeMap 的操作均能够在 O(log n) 的时间内完成。
LinkedHashMap
- Map 接口的哈希表和链接列表的实现,提供所有可选的映射操作,允许 null 值和 null 键(一个键)
- 非线程安全
- 具有可以预知的迭代顺序
get()
public V get(Object key)
获得 key 对应的 value。如果 LinkedHashMap 中没有 key,返回 null。
总结
- HashMap 适用于一般键值映射的去求
- HashTable 适用于多线程并发的场合
- TreeMap 适用于按照键排序的迭代场合
- LinkedHashMap 适用于特殊顺序的迭代场合(如 LRU 算法)
HashSet
- 实现 Set 接口,由哈希表支持,允许使用 null 元素
- 非线程安全
- 不保证 set 的迭代顺序,特别是不保证顺序不变
实际是使用 HashMap 实现
add()
public boolean add(E e)
添加结点,返回是否添加成功
contains()
public boolean contains(Object o)
是否包含了某个结点
- HashSet 通过 HashMap 实现
- TreeSet 通过 TreeMap 实现
- LinkedHashSet 通过 LinkedHashMap实现