一、前言
上面这幅图是Java
集合框架涉及到的类的继承关系,从集合类的角度来看,它分为两个大类:
Collection
和
Map
。
1.1 Collection
Collection
是List
和Set
抽象出来的接口,它包含了这些集合的基本操作。
(1) List
List
接口通常表示一个列表(数组、队列、链表,栈等),其中的元素可以重复,常用的实现类为ArrayList
、LinkedList
和Vector
。
(2) Set
Set
接口通常表示一个集合,集合中的元素不允许重复(通过hashCode
和equals
函数保证),常用的实现类有HashSet
和TreeSet
,HashSet
是通过Map
中的HashMap
来实现的,而TreeSet
则是通过Map
中的TreeMap
实现的,另外TreeSet
还实现了SortedSet
接口,因此是有序的集合。
(3) List 和 Set 的区别
Set
接口存储的是无序的、不重复的数据List
接口存储的是有序的、可以重复的数据Set
检索效率低,删除和插入效率高,插入和删除不会引起元素位置改变。List
查找元素效率高,删除和插入效率低,List
和数组类似,可以动态增长,根据实际存储的长度自动增长List
的长度。
(4) 使用的设计模式
抽象类AbstractCollection
、AbstractList
和AbstractSet
分别实现了Collection
、List
和Set
接口,这就是在Java
集合框架中用的很多的适配器设计模式,用这些抽象类去实现接口,在抽象类中实现接口中的若干或全部方法,这样下面的一些类只需直接继承该抽象类,并实现自己需要的方法即可,而不用实现接口中的全部抽象方法。
1.2 Map
Map
是一个映射接口,其中的每个元素都是一个Key-Value
键值对,同样抽象类AbstractMap
通过适配器模式实现了Map
接口的大部分函数,TreeMap
、HashMap
和WeakHashMap
等实现类都通过继承AbstractMap
来实现。
1.3 Iterator
Iterator
是遍历集合的迭代器,它可以用来遍历Collection
,但是不能用来遍历Map
。Collection
的实现类都实现了iterator()
函数,它返回一个Iterator
对象,用来遍历集合,ListIterator
则专门用来遍历List
。而Enumeration
则是JDK 1.0
时引入的,作用与Iterator
相同,但它的功能比Iterator
要少,它只能在Hashtable
、Vector
和Stack
中使用。
1.4 Arrays 和 Collections
Arrays
和Collections
是用来操作数组、集合的两个工具类,例如在ArrayList
和Vector
中大量调用了Arrays.Copyof()
方法,而Collections
中有很多静态方法可以返回各集合类的synchronized
版本,即线程安全的版本,当然了,如果要用线程安全的集合类,首选concurrent
并发包下的对应的集合类。
二、ArrayList
ArrayList
是基于一个能动态增长的数组实现,ArrayList
并不是线程安全的,在多线程的情况下可以考虑使用Collections.synchronizedList(List T)
函数返回一个线程安全的ArrayList
类,也可以使用并发包下的CopyOnWriteArrayList
类。
ArrayList<T>
类继承于AbstractList<T>
,并实现了以下四个接口:
List<T>
RandomAccess
:支持快速随机访问Cloneable
:能够被克隆Serializable
:支持序列化
ArrayList 的扩容
由于ArrayList
是基于数组实现的,因此当我们通过addXX
方法向数组中添加元素之前,都要保证有足够的空间容纳新的元素,这一过程是通过ensureCapacityInternal
来实现的,传入的参数为所要求的数组容量:
- 如果当前数组为空,并且要求的容量小于
10
,那么将要求的容量设为10
- 接着尝试将数组大小扩充为当前大小的
2.5
倍 - 如果仍然无法满足要求,那么将数组大小设为要求的容量
- 如果要求的容量大于预设的整型的最大值减
8
,那么调用hugeCapacity
方法,将数组的容量设为整型的最大值 - 最后,调用
Arrays.copyOf
将原有数组中的元素复制到新的数组中。
Arrays.copyOf
最终会调用到System.arraycopy()
方法。该Native
函数实际上最终调用了C
语言的memmove()
函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组,Java
强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
ArrayList 转换为静态数组
ArrayList
中提供了两种转换为静态数组的方法:
Object[] toArray()
该方法有可能会抛出java.lang.ClassCastException
异常,如果直接用向下转型的方法,将整个ArrayList
集合转变为指定类型的Array
数组,便会抛出该异常,而如果转化为Array
数组时不向下转型,而是将每个元素向下转型,则不会抛出该异常,显然对数组中的元素一个个进行向下转型,效率不高,且不太方便。T[] toArray(T[] a)
该方法可以直接将ArrayList
转换得到的Array
进行整体向下转型,且从该方法的源码中可以看出,参数a
的大小不足时,内部会调用Arrays.copyOf
方法,该方法内部创建一个新的数组返回,因此对该方法的常用形式如下:
public static Integer[] vectorToArray2(ArrayList<Integer> v) {
Integer[] newText = (Integer[])v.toArray(new Integer[0]);
return newText;
}
复制代码
元素访问方式
ArrayList
基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。
在查找给定元素索引值等的方法中,源码都将该元素的值分为null
和不为null
两种情况处理,ArrayList
中允许元素为null
。
三、LinkedList
LinkedList
是基于双向循环链表实现的,除了可以当作链表来操作外,它还可以当作栈,队列和双端队列来使用。
LinkedList
同样是非线程安全的,在多线程的情况下可以考虑使用Collections.synchronizedList(List T)
函数返回一个线程安全的LinkedList
类,LinkedList
继承于AbstractSequentialList
类,同时实现了以下四个接口:
List<T>
Deque
和Queue
:双端队列Cloneable
:支持克隆操作Serializable
:支持序列化
链表节点
LinkedList的
实现是基于双向循环链表的,且头结点voidLink
中不存放数据,所以它也不存在扩容的方法,只需改变节点的指向即可,每个链表节点包含该节点的数据,以及前驱和后继节点的引用,其定义如下所示:
private static final class Link<ET> {
//该节点的数据。
ET data;
//前驱节点和后继节点。
Link<ET> previous, next;
Link(ET o, Link<ET> p, Link<ET> n) {
data = o;
previous = p;
next = n;
}
}
复制代码
查找和删除操作
当需要根据位置寻找对应节点的数据时,会先比较待查找位置和链表的大小,如果小于一半,那么从头节点的后继节点开始向后寻找,反之则从头结点的前驱节点开始往前寻找,因此对于查找操作来说,它的效率很低,但是向头尾节点插入和删除数据的效率较高。
四、Vector
Vector
也是基于数组实现的,其容量能够动态增长。它的许多实现方法都加入了同步语句,因此是 线程安全 的。
Vector
继承于AbstractList
类,并且实现了下面四个接口:
List<E>
RandomAccess
:支持随机访问Cloneable, java.io.Serializable
:支持Clone
和序列化。
Vector
的实现大体和ArrayList
类似,它有以下几个特点:
Vector
有四个不同的构造方法,无参构造方法的容量为默认值10
,仅包含容量的构造方法则将容量增长量置为0
。- 当
Vector
的容量不足以容纳新的元素时,将进行扩容操作。首先判断容量增长值是否为0
,如果为0
,那么就将新容量设为旧容量的两倍,否则就设置新容量为旧容量加上容量增长值。假如新容量还不够,那么就直接设置新量容量为传入的参数。 - 在存入和读取元素时,会根据元素值是否为
null
进行处理,也就是说,Vector
允许元素为null
。
五、HashSet
HashSet
具有以下特点:
- 不能保证元素的排列顺序,顺序有可能发生变化
- 不是同步的
- 集合元素可以是
null
,但只能放入一个null
当向HashSet
集合中存入一个元素时,HashSet
会调用该对象的hashCode()
方法来得到该对象的hashCode
值,然后根据hashCode
值来决定该对象在HashSet
中存储位置。 简单的说,HashSet
集合判断两个元素相等的标准是两个对象通过equals
方法比较相等,并且两个对象的hashCode()
方法返回值相等。
注意,如果要把一个对象放入HashSet
中,重写该对象对应类的equals
方法,也应该重写其hashCode()
方法。其规则是如果两个对象通过equals
方法比较返回true
时,其hashCode
也应该相同。另外,对象中用作equals
比较标准的属性,都应该用来计算hashCode
的值。
六、TreeSet
TreeSet
是SortedSet
接口的唯一实现类,TreeSet
可以确保集合元素处于排序状态。TreeSet
支持两种排序方式,自然排序 和 定制排序,其中自然排序为默认的排序方式。
向TreeSet
中加入的应该是同一个类的对象。TreeSet
判断两个对象不相等的方式是两个对象通过equals
方法返回false
,或者通过CompareTo
方法比较没有返回0
。
自然排序
自然排序使用要排序元素的CompareTo(Object obj)
方法来比较元素之间大小关系,然后将元素按照升序排列。 Java
提供了一个Comparable
接口,该接口里定义了一个compareTo(Object obj)
方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)
方法如果返回0
,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1
大于obj2
,如果是负数,则表明obj1
小于obj2
。如果我们将两个对象的equals
方法总是返回true
,则这两个对象的compareTo
方法返回应该返回0
.
定制排序
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator
接口,实现int compare(T o1,T o2)
方法。
TreeSet
是二叉树实现的,Treeset
中的数据是自动排好序的,不允许放入null
值。HashSet
是哈希表实现的,HashSet中
的数据是无序的,可以放入null
,但只能放入一个null
,两者中的值都不能重复,就如数据库中唯一约束。HashSet
要求放入的对象必须实现hashCode()
方法,放入的对象,是以hashcode()
码作为标识的,而具有相同内容的String
对象,hashcode
是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
七、HashMap
HashMap
是基于哈希表实现的,每一个元素都是一个key-value
对,其内部通过单链表解决冲突问题,容量不足时,同样会自动增长。HashMap
是非线程安全的,只是用于单线程环境下,多线程环境下可以采用并发包下的ConcurrentHashMap
。
HashMap
继承于AbstractMap
,同时实现了Cloneable
和Serializable
接口,因此,它支持克隆和序列化。
HashMap 的整体结构
HashMap
是基于数组和链表来实现的:
- 首先根据
Key
的hashCode
方法,计算出在数组中的坐标。
//计算 Key 的 hash 值。
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//根据 Key 的 hash 值和链表的长度来计算下标。
int i = indexFor(hash, table.length);
复制代码
- 判断在数组的当前位置是否已经有元素,如果没有,那么就将
Key/Value
封装成HashMapEntry
数据结构,并将其作为数组在该位置上的元素。否则就先从头节点开始遍历该链表,如果 满足下面的两个条件,那么就替换链表该节点的Value
//Value 替换的条件
//条件1:hash 值完全相同
//条件2:key 指向同一块内存地址 或者 key 的 equals 方法返回为 true
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
复制代码
- 遍历完整个链表都没有找到可替代的节点,那么将这个新的
HashMapEntry
作为链表的头节点,并且也是数组在该位置上的元素,原先的头节点则作为它的后继节点。
HashMapEntry 的数据结构
HashMapEntry
的定义如下:
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
//Key
final K key;
//Value
V value;
//后继节点。
HashMapEntry<K,V> next;
//如果 Key 不为 null ,那么就是它的哈希值,否则为0。
int hash;
//....
}
复制代码
元素写入
在第一小节中,我们简要的计算了HashMap
的整体结构,由此我们可以推断出在设计的时候应当尽可能地使元素均匀分布,使得数组每个位置上的链表尽可能地短,避免从链表头结点开始遍历的过程。
而元素是否分布均匀就取决于根据Key
的Hash
值计算数组下标的过程,首先我们看一下Hash
值的计算,这里首先调用对象的hashCode
方法,再通过二次Hash
算法获得一个Hash
值:
public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
}
private static int secondaryHash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
复制代码
之后,再通过这个计算出来Hash
值 与上当前数组长度减一 进行取余,获得对应的数组下标:
hash & (tab.length - 1)
复制代码
由于HashMap
在扩容的时候,保证了数组的长度适中为2
的n
幂,因此length - 1
的二进制表示始终为全1
,因此进行&
操作的结果既保证了最终的结果不会超过数组的长度范围,同时也保证了两个Hash
值相同的元素不会映射到数组的同一位置,再加上上面二次Hash
的过程加上了高位的计算优化,从而使得数据的分布尽可能地平均。
元素读取
理解了上面存储的过程,读取自然也就很好理解了,其实通过Key
计算数组下标,遍历该位置上数组元素的链表进行查找的过程。
扩容
当HashMap
中的元素越来越多的时候,hash
冲突的几率也就越来越高,因为数组的长度是固定的,所以为了提高查询的效率,就要对HashMap
的数组进行扩容。
当HashMap
中的元素个数超过数组大小 * loadFactor
时,loadFactor
的默认值为0.75,就会进行数组扩容,扩容后的大小为原先的2
倍,然后重新计算每个元素在数组中的位置,原数组中的数据必须重新计算其在新数组中的位置,并放进去。
扩容是一个相当耗费性能的操作,因此如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap
的性能。
Fail-Fast 机制
HashMap
并不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map
,那么将抛出ConcurrentModificationException
,这就是所谓fail-fast
策略。
这一策略在源码中的实现是通过modCount
域,modCount
顾名思义就是修改次数,对HashMap
内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount
。
在迭代过程中,判断modCount
跟expectedModCount
是否相等,如果不相等就表示已经有其他线程修改了Map
,那么就会通过下面的方法抛出异常:
HashMapEntry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//省略...
}
复制代码
modCount
声明为volatile
,保证了多线程情况下的内存可见性。
在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的remove
方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException
。因此,面对并发的修改,迭代器很快就会完全失败,而不保证在将来不确定的时间发生任意不确定行为的风险
八、HashTable
HashTable
经常用来和HashMap
进行比较,前者是线程安全的,而后者则不是,其实HashTable
要比HashMap
出现得要早,它实现线程安全的原理并没有什么高级的地方,只不过是在写入和读取时加上了synchronized
关键字用于同步,并且也不推荐使用了,因为在并发包中提供了更好的解决方案ConcurrentHashMap
,它内部的实现比较复杂,之后我们再通过一篇文章进行分析。
这里简单地总结一下它和HashMap
之间的区别:
HashTable
基于Dictionary
类,而HashMap
是基于AbstractMap
。Dictionary
是任何可将键映射到相应值的类的抽象父类,而AbstractMap
基于Map
接口的实现,它以最大限度地减少实现此接口所需的工作。HashMap
的key
和value
都允许为null
,而Hashtable
的key
和value
都不允许为null
。HashMap
遇到key
为null
的时候,调用putForNullKey
方法进行处理,而对value
没有处理,Hashtable
遇到null
,直接返回NullPointerException
。Hashtable
方法是同步,而HashMap
则不是。我们可以看一下源码,Hashtable
中的几乎所有的public
的方法都是synchronized
的,而有些方法也是在内部通过synchronized
代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用HashTable
,没有涉及就采用HashMap
,但是在Collections
类中存在一个静态方法:synchronizedMap()
,该方法创建了一个线程安全的Map
对象,并把它作为一个封装的对象来返回。
九、TreeMap
TreeMap
是一个有序的key-value
集合,它是通过 红黑树 实现的。TreeMap
继承于AbstractMap
,所以它是一个Map
,即一个key-value
集合。TreeMap
实现了NavigableMap
接口,意味着它支持一系列的导航方法,比如返回有序的key集合。TreeMap
实现了Cloneable
和Serializable
接口,意味着它可以被Clone
和序列化。
TreeMap
基于红黑树实现,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator
进行排序,具体取决于使用的构造方法。TreeMap
的基本操作containsKey
、get
、put
和remove
的时间复杂度是log(n)
,另外,TreeMap
是非同步的, 它的iterator
方法返回的迭代器是Fail-Fastl
的。
十、LinkedHashMap
LinkedHashMap
是HashMap
的子类,与HashMap
有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put
到LinkedHashmap
的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。LinkedHashMap
可以用来实现LRU
算法。LinkedHashMap
同样是非线程安全的,只在单线程环境下使用。
十一、LinkedHashSet
LinkedHashSet
是具有可预知迭代顺序的Set
接口的哈希表和链接列表实现。此实现与HashSet
的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
LinkedHashSet
的实现:对于LinkedHashSet
而言,它继承与HashSet
、又基于LinkedHashMap
来实现的。