赞
踩
Java集合框架无论是在工作、学习、面试中都会经常涉及到,相信各位也并不陌生,其强大也不用多说,博主最近翻阅java集合框架的源码以及搜索一些相关资料整理出Java集合框架的系列。一方面是做一个总结,方便以后查阅,另一方面希望各位小伙伴能够提出不足之处,我会及时更新修改。
博主从网上抠了一张图,觉得画得还是比较形象的,给大家参考一下。
上述类图中,实线边框的是实现类,比如ArrayList,LinkedList,HashMap等,折线边框的是抽象类,比如AbstractCollection,AbstractList,AbstractMap等,而点线边框的是接口,比如Collection,Iterator,List等。
发现一个特点,上述所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含hasNext(), next(), remove()三种方法。它的一个子接口LinkedIterator在它的基础上又添加了三种方法,分别是add(),previous(),hasPrevious()。也就是说如果是先Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会在遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap;而那些元素有序的集合,实现的一般都是LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。
本篇首先对HashMap进行详解,后续内容慢慢跟进。
如无特殊说明,本文以jdk7为准进行说明。
- package java.util;
- import java.io.*;
- public class HashMap<K,V>
- extends AbstractMap<K,V>
- implements Map<K,V>, Cloneable, Serializable{
- }
工作原理:通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Factor则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
- static final Entry<?,?>[] EMPTY_TABLE = {};
- transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保证序不随时间变化。HashMap存储着Entry(hash, key, value, next)对象。
当key==null时,存在table[0]即第一个桶中,hash值为0。HashMap对key==null的键值对会做单独处理,譬如:
- private V getForNullKey();
- private V putForNullKey(V value);
这两个方法。
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)。
Capacity的默认值为16:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
负载因子的默认值为0.75:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
简单的说,Capacity就是bucket的大小,Load factor就是bucket填满程度的最大比例。如果对迭代性能要求很高的话不要把Capacity设置过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时就需要调整bucket的大小为当前的2倍。
可以设置初始容量Capacity,但是在HashMap处理过程中,是会把Capacity扩充成2的倍数,怎么理解?比如你设置的初始值17,但是17不是2的整数倍,会扩容成32,再比如你初始设置的是15,会扩容成16,具体的实现在下面:
- private static int roundUpToPowerOf2(int number) {
- // assert number >= 0 : "number must be non-negative";
- return number >= MAXIMUM_CAPACITY
- ? MAXIMUM_CAPACITY
- : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
- }
HashMap中有一个成员变量modCount,这个用来实现“fast-fail”机制(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其结构性的修改,这是迭代器会立马感知到,并且立刻抛出ConcurrentModificationException异常,而不是等待迭代完成之后才告诉你已经出错。
举个例子来表述下Map的遍历,具体的可以参考《 Java中如何遍历Map对象》
- Map<String,Integer> map = new HashMap<>();
- map.put("s1", 1);
- map.put("s2", 2);
- map.put("s3", 3);
- map.put("s4", 4);
- map.put("s5", 5);
- map.put(null, 9);
- map.put("s6", 6);
- map.put("s7", 7);
- map.put("s8", 8);
- for(Map.Entry<String,Integer> entry:map.entrySet())
- {
- System.out.println(entry.getKey()+":"+entry.getValue());
- }
输出结果:
- null:9
- s2:2
- s1:1
- s7:7
- s8:8
- s5:5
- s6:6
- s3:3
- s4:4
存储结构如图(ppt画的图,比较简陋~):
put函数大致的思路为:
- public V put(K key, V value) {
- if (table == EMPTY_TABLE) {
- inflateTable(threshold);
- }
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key);
- 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;
- }
- void addEntry(int hash, K key, V value, int bucketIndex) {
- if ((size >= threshold) && (null != table[bucketIndex])) {
- resize(2 * table.length);
- hash = (null != key) ? hash(key) : 0;
- bucketIndex = indexFor(hash, table.length);
- }
-
- createEntry(hash, key, value, bucketIndex);
- }
注:在jdk8中,新增默认为8的閥值(TREEIFY_THRESHOLD),当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
get函数的大致思路为:
- public V get(Object key) {
- if (key == null)
- return getForNullKey();
- Entry<K,V> entry = getEntry(key);
-
- return null == entry ? null : entry.getValue();
- }
- final Entry<K,V> getEntry(Object key) {
- if (size == 0) {
- return null;
- }
-
- int hash = (key == null) ? 0 : hash(key);
- 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 != null && key.equals(k))))
- return e;
- }
- return null;
- }
hash和indexFor方法:
- final int hash(Object k) {
- int h = hashSeed;
- if (0 != h && k instanceof String) {
- return sun.misc.Hashing.stringHash32((String) k);
- }
-
- h ^= k.hashCode();
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
- public native int hashCode();
- static int indexFor(int h, int length) {
- // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
- return h & (length-1);
- }
看懂没?还是实地检验下比较形象~
譬如key==”key”,那么key的hashCode为(十进制数为106079):
0000 0000 0000 0001 1001 1110 0101 1111
执行h ^= (h >>> 20) ^ (h >>> 12);的时候
h>>>20:
0000 0000 0000 0000 0000 0000 0000 0000
h>>>12:
0000 0000 0000 0000 0000 0000 0001 1001
h ^= (h >>> 20) ^ (h >>> 12):
0000 0000 0000 0001 1001 1110 0100 0110
执行h ^ (h >>> 7) ^ (h >>> 4)的时候
h >>> 7:
0000 0000 0000 0000 0000 0011 0011 1100
h>>>4:
0000 0000 0000 0000 0001 1001 1110 0100
h ^ (h >>> 7) ^ (h >>> 4):
0000 0000 0000 0001 1000 0100 1001 1110(十进制表示为99486)
假设Capacity是默认16,那么
h&(length-1):
0000 0000 0000 0001 1000 0100 1001 1110 &
0000 0000 0000 0000 0000 0000 0000 1111 =
0000 0000 0000 0000 0000 0000 0000 1110 = 14
我们可以通过eclipse的debug工作中的Variables查看相关细节:
可以看到key=”key”果然在table[14]当中。
h & (length-1)这种取模用位运算的方式比较快,这个需要数组的大小永远是2的N次方来保证其有效性。
jdk8中hash函数的实现:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
可以使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
如下:
- package collections;
-
- public class StringOther
- {
- private String name;
-
- public StringOther(String name)
- {
- this.name = name;
- }
-
- @Override
- public int hashCode()
- {
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object obj)
- {
- if(obj == this)
- return true;
- if(!(obj instanceof StringOther))
- return false;
- StringOther so = (StringOther)obj;
- return so.getName().equals(name);
- }
-
- public String getName()
- {
- return this.name;
- }
-
- @Override
- public String toString()
- {
- return "["+this.name+":"+this.hashCode()+"]";
- }
- }
测试代码:
- Map<StringOther,String> maps = new HashMap<>(16);
- StringOther so1 = new StringOther("so1");
- StringOther so2 = new StringOther("so2");
- maps.put(so1,"1");
- maps.put(so2,"2");
-
- System.out.println(maps);
输出结果:{[so1:114005]=1, [so2:114006]=2}
注意重写equals方法的时候一定要写成:
@Override public boolean equals(Object obj)
的形式,注意注解Override和参数类型Object obj,如写成@Override public boolean equals(StringOther obj)这样会出现意想不到的错误,如果对其不解,可在下方留言。
并且在每个覆盖了equals方法的类中也必须覆盖hashCode方法,如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运转,这样的集合包括HashMap, HashSet和Hashtable.
细心的朋友可能注意到HashMap实现了Serializable接口,但是对table的定义(transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;)却是transient的。然后又自己实现了writeObject()和readObject()两个方法。
假设我们已经知道的事实:声明为transient的变量不再是对象持久化的一部分。如果不清楚java序列化相关细节,可以参考《JAVA序列化》。
那么这是为什么呢?原因有两点:
由于Java自身的序列化有明显的缺点:占用空间大以及低效,博主建议采用其他的方法进行序列化,譬如json或者protobuff等,详细可以参考《浅析若干Java序列化工具》
由于Hashtable逐渐退出历史舞台,故不会另起一篇进行重点解析,只在这里与HashMap区别一下。
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。
HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);
Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。
参考资料:
1. 《Java HashMap工作原理及实现》
2. 《关于Java集合的小抄》
3. 《HashMap和Hashtable的区别》
4. 《Java 集合类详解》
5. 《Effective Java(Second Edition)》. Joshua Bloch.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。