您的位置:首页 > 编程语言 > Java开发

Java 集合源码分析及总结

2015-01-05 15:28 381 查看
Java的集合容器这块是平时开发中使用到最多的Java类库了,什么情况下应该使用那些容器,这些容器的实现机制又都是怎样的?

最近重新仔细地研究了Java的集合这块知识以及JDK的源码,总结一下自己的心得。

下图中对于Java的集合api,有一个比较全面的展示。



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存储结构的图如下:



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循环
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息