Java
集合框架涉及到的类的继承关系,从集合类的角度来看,它分为两个大类:
Collection
和
Map
。
Collection
是List
和Set
抽象出来的接口,它包含了这些集合的基本操做。html
List
接口一般表示一个列表(数组、队列、链表,栈等),其中的元素能够重复,经常使用的实现类为ArrayList
、LinkedList
和Vector
。java
Set
接口一般表示一个集合,集合中的元素不容许重复(经过hashCode
和equals
函数保证),经常使用的实现类有HashSet
和TreeSet
,HashSet
是经过Map
中的HashMap
来实现的,而TreeSet
则是经过Map
中的TreeMap
实现的,另外TreeSet
还实现了SortedSet
接口,所以是有序的集合。算法
Set
接口存储的是无序的、不重复的数据List
接口存储的是有序的、能够重复的数据Set
检索效率低,删除和插入效率高,插入和删除不会引发元素位置改变。List
查找元素效率高,删除和插入效率低,List
和数组相似,能够动态增加,根据实际存储的长度自动增加List
的长度。抽象类AbstractCollection
、AbstractList
和AbstractSet
分别实现了Collection
、List
和Set
接口,这就是在Java
集合框架中用的不少的适配器设计模式,用这些抽象类去实现接口,在抽象类中实现接口中的若干或所有方法,这样下面的一些类只需直接继承该抽象类,并实现本身须要的方法便可,而不用实现接口中的所有抽象方法。数据库
Map
是一个映射接口,其中的每一个元素都是一个Key-Value
键值对,一样抽象类AbstractMap
经过适配器模式实现了Map
接口的大部分函数,TreeMap
、HashMap
和WeakHashMap
等实现类都经过继承AbstractMap
来实现。设计模式
Iterator
是遍历集合的迭代器,它能够用来遍历Collection
,可是不能用来遍历Map
。Collection
的实现类都实现了iterator()
函数,它返回一个Iterator
对象,用来遍历集合,ListIterator
则专门用来遍历List
。而Enumeration
则是JDK 1.0
时引入的,做用与Iterator
相同,但它的功能比Iterator
要少,它只能在Hashtable
、Vector
和Stack
中使用。数组
Arrays
和Collections
是用来操做数组、集合的两个工具类,例如在ArrayList
和Vector
中大量调用了Arrays.Copyof()
方法,而Collections
中有不少静态方法能够返回各集合类的synchronized
版本,即线程安全的版本,固然了,若是要用线程安全的集合类,首选concurrent
并发包下的对应的集合类。安全
ArrayList
是基于一个能动态增加的数组实现,ArrayList
并非线程安全的,在多线程的状况下能够考虑使用Collections.synchronizedList(List T)
函数返回一个线程安全的ArrayList
类,也可使用并发包下的CopyOnWriteArrayList
类。数据结构
ArrayList<T>
类继承于AbstractList<T>
,并实现了如下四个接口:多线程
List<T>
RandomAccess
:支持快速随机访问Cloneable
:可以被克隆Serializable
:支持序列化因为ArrayList
是基于数组实现的,所以当咱们经过addXX
方法向数组中添加元素以前,都要保证有足够的空间容纳新的元素,这一过程是经过ensureCapacityInternal
来实现的,传入的参数为所要求的数组容量:并发
10
,那么将要求的容量设为10
2.5
倍8
,那么调用hugeCapacity
方法,将数组的容量设为整型的最大值Arrays.copyOf
将原有数组中的元素复制到新的数组中。Arrays.copyOf
最终会调用到System.arraycopy()
方法。该Native
函数实际上最终调用了C
语言的memmove()
函数,所以它能够保证同一个数组内元素的正确复制和移动,比通常的复制方法的实现效率要高不少,很适合用来批量处理数组,Java
强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
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
一样是非线程安全的,在多线程的状况下能够考虑使用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
继承于AbstractList
类,而且实现了下面四个接口:
List<E>
RandomAccess
:支持随机访问Cloneable, java.io.Serializable
:支持Clone
和序列化。Vector
的实现大致和ArrayList
相似,它有如下几个特色:
Vector
有四个不一样的构造方法,无参构造方法的容量为默认值10
,仅包含容量的构造方法则将容量增加量置为0
。Vector
的容量不足以容纳新的元素时,将进行扩容操做。首先判断容量增加值是否为0
,若是为0
,那么就将新容量设为旧容量的两倍,不然就设置新容量为旧容量加上容量增加值。假如新容量还不够,那么就直接设置新量容量为传入的参数。null
进行处理,也就是说,Vector
容许元素为null
。HashSet
具备如下特色:
null
,但只能放入一个null
当向HashSet
集合中存入一个元素时,HashSet
会调用该对象的hashCode()
方法来获得该对象的hashCode
值,而后根据hashCode
值来决定该对象在HashSet
中存储位置。 简单的说,HashSet
集合判断两个元素相等的标准是两个对象经过equals
方法比较相等,而且两个对象的hashCode()
方法返回值相等。
注意,若是要把一个对象放入HashSet
中,重写该对象对应类的equals
方法,也应该重写其hashCode()
方法。其规则是若是两个对象经过equals
方法比较返回true
时,其hashCode
也应该相同。另外,对象中用做equals
比较标准的属性,都应该用来计算hashCode
的值。
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
是基于哈希表实现的,每个元素都是一个key-value
对,其内部经过单链表解决冲突问题,容量不足时,一样会自动增加。HashMap
是非线程安全的,只是用于单线程环境下,多线程环境下能够采用并发包下的ConcurrentHashMap
。
HashMap
继承于AbstractMap
,同时实现了Cloneable
和Serializable
接口,所以,它支持克隆和序列化。
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
的定义以下:
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
的性能。
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
常常用来和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
是一个有序的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
是HashMap
的子类,与HashMap
有着一样的存储结构,但它加入了一个双向链表的头结点,将全部put
到LinkedHashmap
的节点一一串成了一个双向循环链表,所以它保留了节点插入的顺序,可使节点的输出顺序与输入顺序相同。LinkedHashMap
能够用来实现LRU
算法。LinkedHashMap
一样是非线程安全的,只在单线程环境下使用。LinkedHashSet
是具备可预知迭代顺序的Set
接口的哈希表和连接列表实现。此实现与HashSet
的不一样之处在于,后者维护着一个运行于全部条目的双重连接列表。此连接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
LinkedHashSet
的实现:对于LinkedHashSet
而言,它继承与HashSet
、又基于LinkedHashMap
来实现的。