Java 集合源码分析及总结
2015-01-05 15:28
381 查看
Java的集合容器这块是平时开发中使用到最多的Java类库了,什么情况下应该使用那些容器,这些容器的实现机制又都是怎样的?
最近重新仔细地研究了Java的集合这块知识以及JDK的源码,总结一下自己的心得。
下图中对于Java的集合api,有一个比较全面的展示。
![](http://img.my.csdn.net/uploads/201304/07/1365345192_2284.jpg)
Java容器库中分为两类(基本接口)
4000
1、集合(Collection):通过一个或多个规则存储的一序列的元素。必须知道它是线性结构存储的。
2、图(Map):一组“键-值”绑定的成对对象,可以通过键去搜索对应的值。
其中List常用的有两种:
ArrayList
其存储结构是通过数组来存储,所以它的随机存储十分的优秀(get,set),可是由于数组的特性,扩容消耗大,也导致它插入和删除的操作效率相对其他类型来着低(insert、remove)
ArrayList相对来说还是比较简单的,它的属性代码如下:
对这个List的操作都是对这对象数组的操作,所以说如果是get,set的操作,数组是相当高效的,因为不需要动数组的结构,能直接操作。
而在Add操作的时候,会先检查对象数组剩余空间是否还够加入新数据,如果不够,则将旧的对象数组复制一份到新的长度更大的数组中去。如果是要在数组中add或者remove的话,则需要将操作的位置(index)后面数组整个移动,这样的代价是很大的,所以如果insert、remove操作多的话,不建议采用ArrayList来存储。
LinkedList
该类不仅实现了List接口,同时还实现了Queue接口,它也是优秀的Queue实现,而且还是个双向队列(Deques)。
它和ArrayList不同之处在于使用一个内部对象存储数据的,将这些对象链式连接起来,LinkedList对象里的header,不存储数据,只是一个引导的作用,这个header的next是存储第一个元素对象,previous是存储的最后一个对象:
以上两种List各有优点,平时我们使用的时候,需要根据需求来判断到底采用哪个比较好。判断的方法比较简单,这个集合的随机存储的操作多,还是插入删除的比较多。
题外话:我们常用Arrays.asList(T…
a)来快速生成一个List。注意这里生成的List是Arrays的内部类ArrayList(不是通常用的java.util.ArrayList),数据保存在该对象的一个常量数组(final E[] a)中,所以这个List是无法改变的,即使用add()或delete()方法会抛出异常。另外一个常用到的容器工具类是Collections。
Set的特点是不允许重复的数据存储在集合中。Set常用的实现类有三种:HashSet、LinkedHashSet、TreeSet。Set中的对象是否重复,是根据对象的“值”进行判断(equals()和hashCode()比较)。
HashSet
HashSet的效率很高,它使用哈希函数(散列法)来提高检索速度(后面会介绍哈希的机制)。如果你去看它的源码,你会发现,其实HashSet就是HashMap,只不过一个只存值key,一个能存键值(key/value)对应的数据。
从上面的结果看出,其实HashSet的数据只是存到了HashMap的key上(HashMap的key唯一),HashSet的操作都是直接调用了HashMap的方法罢了。后面就会讲到HashMap。
LinkedHashSet
LinkedHashSet继承自HashSet,区别只是在于它的属性HashMap<E,Object>
map是一个LinkedHashMap实例,保证插入的顺序。这样,其实秘密也在LinkedHashMap那块了。
TreeSet
TreeSet使用红黑树的数据存储结构(相对上升的顺序),这样当数据存进来的时候就是有序的了。所以如果结果需要排序的话,使用TreeSet比较合适。它的实现机制和上面的Set基本类似,有个TreeMap的实例属性,最终调用的也是TreeMap的实现方法。
Queue队列是“先进先出”的容器,Queue在并发编程中特别重要。LinkedList和PriorityQueue实现了Queue接口,作为Queue的实现类使用。
value)。常用的Map也有三种:
HashMap
使用哈希的方法提供最快的检索速度,所包含的元素集没有顺序。
HashMap实现机制关键在put和get方法中的哈希值计算。HashMap的数据以内部类Entry的数组形式存储的,通过对key的hash运算的,得到一个下标值(index),将数据存储到Entry数组对应的位置,不过hash运算也有可能得到重复的值,这时,为了解决冲突,Entry本身是链式的存储结构,将这个数据存储在Entry数组对应的Entry链表中去。有一个能很好说明HashMap存储结构的图如下:
![](http://img.my.csdn.net/uploads/201304/07/1365345666_2681.jpg)
HashMap中的数据结构:
从put方法中看到先是通过hash运算得到数组所在索引i,查找table数组中i位置的entry,如果hash和equal都相等,说明key是一样的,覆盖原有的value值。否则添加一个entry,从addEntry方法中看出,table数组中原来的entry链到了新的entry的next位置(原来没有的话就是null)。最后还有段逻辑用于扩容,如果元素数量超过阀值(通过容量*负载因子算出,负载因子默认是0.75)则进行扩容为2倍。
索引i的计算方法如下显示,hash方法对key的hashCode重新计算一次散列,防止一些key的质量较差的hashCode方法,能使哈希值计算的冲突碰撞减到很小(保证每一个bit位的不同常数背的有限的碰撞次数)。
indexFor方法将新计算出的散列值和数组长度进行与运算(h & (length - 1)),能让索引i均匀分布在数组中。即将散列值在大于数组索引的二进制位上置0,让索引值小于length-1。形成链表的几率减少,查询效率上就更快了。
另外HashMap支持空值null的key,通过上面说的方法算出的hash值都是非0的,这时元素数组第0个元素就空出来了,这个位置就是存储null的key的值。代码如下:
以上就是HashMap的基本逻辑结构。
TreeMap
以相对上升的顺序保存key,也是红黑树结构。通过红黑二叉树的原理来保存数据。以后在专门写个文章说明下红黑二叉树的原理。
LinkedHashMap
LinkedHashMap继承自HashMap,它在HashMap的实现基础之上添加了链式结构,根据上面介绍的LinkedList和HashMap就已经很好理解了。以插入时的顺序保存key,也使用HashMap的检索方法,效率只比HashMap稍微慢点。
哈希:
每个Java对象都是生成一个哈希码,HashMap就是利用这个哈希码快速检索到key的对象的。hashCode()方法为每个对象产生一个哈希码,默认是使用对象的地址作为哈希码
HashMap的key值判断,是根据hashCode产生的哈希码对比,然后使用equal方法对比判断。哈希的全部意义就是为了快速检索。也可以说是为了让key能快速检索到,以一种特殊的方式存储key。
此外还有一些Map实现类WeakHashMap、ConcurrentHashMap、IdentityHaspMap在特定情况下使用的。
老代码中的类,有些类由于Java早期(Java
1.0/1.1)的设计不合理,现在已经被新的类代替了,所以如果是新写的代码中不应该出现这些类了。不过如果为了兼容老代码,依然可以使用:Stack、Vector、Hashtable。
我在循环容器的时候常用到迭代器,在另外一篇文章中讲了使用迭代器循环容器。《Java中的迭代器Iterator和for-each循环》
最近重新仔细地研究了Java的集合这块知识以及JDK的源码,总结一下自己的心得。
下图中对于Java的集合api,有一个比较全面的展示。
![](http://img.my.csdn.net/uploads/201304/07/1365345192_2284.jpg)
Java容器库中分为两类(基本接口)
4000
1、集合(Collection):通过一个或多个规则存储的一序列的元素。必须知道它是线性结构存储的。
2、图(Map):一组“键-值”绑定的成对对象,可以通过键去搜索对应的值。
集合(Collection)
Colletion可以分为三种:List、Set、Queue。List是按照插入顺序存储元素,Set不能有重复的元素,Queue按照队列的规则入列出列元素。其中List常用的有两种:
ArrayList
其存储结构是通过数组来存储,所以它的随机存储十分的优秀(get,set),可是由于数组的特性,扩容消耗大,也导致它插入和删除的操作效率相对其他类型来着低(insert、remove)
ArrayList相对来说还是比较简单的,它的属性代码如下:
class ArrayList { private Object[] elementData; private int size; }用一个对象数组用来存储数据,一个整型存储集合的范围索引。
对这个List的操作都是对这对象数组的操作,所以说如果是get,set的操作,数组是相当高效的,因为不需要动数组的结构,能直接操作。
而在Add操作的时候,会先检查对象数组剩余空间是否还够加入新数据,如果不够,则将旧的对象数组复制一份到新的长度更大的数组中去。如果是要在数组中add或者remove的话,则需要将操作的位置(index)后面数组整个移动,这样的代价是很大的,所以如果insert、remove操作多的话,不建议采用ArrayList来存储。
LinkedList
该类不仅实现了List接口,同时还实现了Queue接口,它也是优秀的Queue实现,而且还是个双向队列(Deques)。
它和ArrayList不同之处在于使用一个内部对象存储数据的,将这些对象链式连接起来,LinkedList对象里的header,不存储数据,只是一个引导的作用,这个header的next是存储第一个元素对象,previous是存储的最后一个对象:
class LinkedList { private Entry header = new Entry(null,null,null); private int size = 0; private static class Entry { E element; Entry next; Entry previous; } }由此可以看出LinkedList是个环形链,他的add和remove的操作,就是循环到相应的位置将新的Entry对象链进去,这样的代价是十分小的,效率很高。而set和get操作则需要循环到相应的位置找到Entry对象获取数据,这样对比数组的话就要多了循环的操作,效率比ArrayList就低了不少。
以上两种List各有优点,平时我们使用的时候,需要根据需求来判断到底采用哪个比较好。判断的方法比较简单,这个集合的随机存储的操作多,还是插入删除的比较多。
题外话:我们常用Arrays.asList(T…
a)来快速生成一个List。注意这里生成的List是Arrays的内部类ArrayList(不是通常用的java.util.ArrayList),数据保存在该对象的一个常量数组(final E[] a)中,所以这个List是无法改变的,即使用add()或delete()方法会抛出异常。另外一个常用到的容器工具类是Collections。
Set的特点是不允许重复的数据存储在集合中。Set常用的实现类有三种:HashSet、LinkedHashSet、TreeSet。Set中的对象是否重复,是根据对象的“值”进行判断(equals()和hashCode()比较)。
HashSet
HashSet的效率很高,它使用哈希函数(散列法)来提高检索速度(后面会介绍哈希的机制)。如果你去看它的源码,你会发现,其实HashSet就是HashMap,只不过一个只存值key,一个能存键值(key/value)对应的数据。
class HashSet { private HashMap<E,Object> map; }
从上面的结果看出,其实HashSet的数据只是存到了HashMap的key上(HashMap的key唯一),HashSet的操作都是直接调用了HashMap的方法罢了。后面就会讲到HashMap。
LinkedHashSet
LinkedHashSet继承自HashSet,区别只是在于它的属性HashMap<E,Object>
map是一个LinkedHashMap实例,保证插入的顺序。这样,其实秘密也在LinkedHashMap那块了。
TreeSet
TreeSet使用红黑树的数据存储结构(相对上升的顺序),这样当数据存进来的时候就是有序的了。所以如果结果需要排序的话,使用TreeSet比较合适。它的实现机制和上面的Set基本类似,有个TreeMap的实例属性,最终调用的也是TreeMap的实现方法。
Queue队列是“先进先出”的容器,Queue在并发编程中特别重要。LinkedList和PriorityQueue实现了Queue接口,作为Queue的实现类使用。
二、图(Map)
使用一个关联key来增加一个value。Map.put(key,value)。常用的Map也有三种:
HashMap
使用哈希的方法提供最快的检索速度,所包含的元素集没有顺序。
HashMap实现机制关键在put和get方法中的哈希值计算。HashMap的数据以内部类Entry的数组形式存储的,通过对key的hash运算的,得到一个下标值(index),将数据存储到Entry数组对应的位置,不过hash运算也有可能得到重复的值,这时,为了解决冲突,Entry本身是链式的存储结构,将这个数据存储在Entry数组对应的Entry链表中去。有一个能很好说明HashMap存储结构的图如下:
![](http://img.my.csdn.net/uploads/201304/07/1365345666_2681.jpg)
HashMap中的数据结构:
class HashMap { Entry[] table; int size; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; } }HashMap的hash运算方法下面再列出,下面是put和get的逻辑代码:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
从put方法中看到先是通过hash运算得到数组所在索引i,查找table数组中i位置的entry,如果hash和equal都相等,说明key是一样的,覆盖原有的value值。否则添加一个entry,从addEntry方法中看出,table数组中原来的entry链到了新的entry的next位置(原来没有的话就是null)。最后还有段逻辑用于扩容,如果元素数量超过阀值(通过容量*负载因子算出,负载因子默认是0.75)则进行扩容为2倍。
索引i的计算方法如下显示,hash方法对key的hashCode重新计算一次散列,防止一些key的质量较差的hashCode方法,能使哈希值计算的冲突碰撞减到很小(保证每一个bit位的不同常数背的有限的碰撞次数)。
indexFor方法将新计算出的散列值和数组长度进行与运算(h & (length - 1)),能让索引i均匀分布在数组中。即将散列值在大于数组索引的二进制位上置0,让索引值小于length-1。形成链表的几率减少,查询效率上就更快了。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
另外HashMap支持空值null的key,通过上面说的方法算出的hash值都是非0的,这时元素数组第0个元素就空出来了,这个位置就是存储null的key的值。代码如下:
private V putForNullKey(V value) { for (Entry<K, V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; return oldValue; } } addEntry(0, null, value, 0); return null; }
以上就是HashMap的基本逻辑结构。
TreeMap
以相对上升的顺序保存key,也是红黑树结构。通过红黑二叉树的原理来保存数据。以后在专门写个文章说明下红黑二叉树的原理。
class TreeMap<K,V> { private Entry<K,V> root = null; static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; } }
LinkedHashMap
LinkedHashMap继承自HashMap,它在HashMap的实现基础之上添加了链式结构,根据上面介绍的LinkedList和HashMap就已经很好理解了。以插入时的顺序保存key,也使用HashMap的检索方法,效率只比HashMap稍微慢点。
class LinkedHashMap<K,V> extends HashMap<K,V> { private Entry<K,V> header; private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; } }
哈希:
每个Java对象都是生成一个哈希码,HashMap就是利用这个哈希码快速检索到key的对象的。hashCode()方法为每个对象产生一个哈希码,默认是使用对象的地址作为哈希码
HashMap的key值判断,是根据hashCode产生的哈希码对比,然后使用equal方法对比判断。哈希的全部意义就是为了快速检索。也可以说是为了让key能快速检索到,以一种特殊的方式存储key。
此外还有一些Map实现类WeakHashMap、ConcurrentHashMap、IdentityHaspMap在特定情况下使用的。
老代码中的类,有些类由于Java早期(Java
1.0/1.1)的设计不合理,现在已经被新的类代替了,所以如果是新写的代码中不应该出现这些类了。不过如果为了兼容老代码,依然可以使用:Stack、Vector、Hashtable。
我在循环容器的时候常用到迭代器,在另外一篇文章中讲了使用迭代器循环容器。《Java中的迭代器Iterator和for-each循环》
相关文章推荐
- Thinking in Java之集合相关整理(源码分析)
- Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)
- 从源码分析java集合【HashMap】
- Java集合系列之HashSet源码分析
- Java集合系列之LinkedList源码分析
- Thinking in Java之集合相关整理(源码分析)
- Java集合之ArrayList源码分析
- Java集合之ArrayList源码分析
- Java集合之HashMap源码实现分析...
- Java集合之HashSet源码分析
- Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和性能分析)
- java核心之集合框架——ArrayList源码分析
- java 集合ArrayList及LinkList源码分析
- Java集合系列之ArrayList源码分析
- java 集合ArrayList及LinkList源码分析
- 关于java中ReentrantLock类的源码分析以及总结与例子
- java 并发 ConcurrentHashMap 与 HashTable源码分析总结
- java核心之集合框架——HashMap源码分析
- Java集合之HashMap源码分析
- Java集合之LinkedList源码分析