点击上方“Java团长”,选择“置顶公众号”
干货文章第一时间送达!
继上一篇文章Java集合框架综述后,今天正式开始分析具体集合类的代码,首先以既熟悉又陌生的HashMap开始。
本文源码分析基于Oracle JDK 1.7.0_71,请知悉。
$ java -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)
签名(signature)
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
可以看到HashMap
继承了
标记接口Cloneable,用于表明
HashMap
对象会重写java.lang.Object#clone()
方法,HashMap实现的是浅拷贝(shallow copy)。标记接口Serializable,用于表明
HashMap
对象可以被序列化
比较有意思的是,HashMap
同时继承了抽象类AbstractMap
与接口Map
,因为抽象类AbstractMap
的签名为
public abstract class AbstractMap<K,V> implements Map<K,V>
Stack Overfloooow上解释到:
在语法层面继承接口
Map
是多余的,这么做仅仅是为了让阅读代码的人明确知道HashMap
是属于Map
体系的,起到了文档的作用
AbstractMap
相当于个辅助类,Map
的一些操作这里面已经提供了默认实现,后面具体的子类如果没有特殊行为,可直接使用AbstractMap
提供的实现。
Cloneable接口
It's evil, don't use it.
Cloneable
这个接口设计的非常不好,最致命的一点是它里面竟然没有clone
方法,也就是说我们自己写的类完全可以实现这个接口的同时不重写clone
方法。
关于Cloneable
的不足,大家可以去看看《Effective Java》一书的作者给出的理由,在所给链接的文章里,Josh Bloch也会讲如何实现深拷贝比较好,我这里就不在赘述了。
Map接口
在eclipse中的outline面板可以看到Map
接口里面包含以下成员方法与内部类:
Map_field_method
可以看到,这里的成员方法不外乎是“增删改查”,这也反映了我们编写程序时,一定是以“数据”为导向的。
在上篇文章讲了Map
虽然并不是Collection
,但是它提供了三种“集合视角”(collection views),与下面三个方法一一对应:
Set<K> keySet()
,提供key的集合视角Collection<V> values()
,提供value的集合视角Set<Map.Entry<K, V>> entrySet()
,提供key-value序对的集合视角,这里用内部类Map.Entry
表示序对
AbstractMap抽象类
AbstractMap
对Map
中的方法提供了一个基本实现,减少了实现Map
接口的工作量。
举例来说:
如果要实现个不可变(unmodifiable)的map,那么只需继承
AbstractMap
,然后实现其entrySet
方法,这个方法返回的set不支持add与remove,同时这个set的迭代器(iterator)不支持remove操作即可。相反,如果要实现个可变(modifiable)的map,首先继承
AbstractMap
,然后重写(override)AbstractMap
的put方法,同时实现entrySet
所返回set的迭代器的remove方法即可。
设计理念(design concept)
哈希表(hash table)
HashMap
是一种基于哈希表(hash table)实现的map,哈希表(也叫关联数组)一种通用的数据结构,大多数的现代语言都原生支持,其概念也比较简单:key经过hash函数作用后得到一个槽(buckets或slots)的索引(index),槽中保存着我们想要获取的值
,如下图所示
hash table demo
很容易想到,一些不同的key经过同一hash函数后可能产生相同的索引,也就是产生了冲突,这是在所难免的。
所以利用哈希表这种数据结构实现具体类时,需要:
设计个好的hash函数,使冲突尽可能的减少
其次是需要解决发生冲突后如何处理。
后面会重点介绍HashMap
是如何解决这两个问题的。
HashMap的一些特点
线程非安全,并且允许key与value都为null值,
HashTable
与之相反,为线程安全,key与value都不允许null值。不保证其内部元素的顺序,而且随着时间的推移,同一元素的位置也可能改变(resize的情况)
put、get操作的时间复杂度为O(1)。
遍历其集合视角的时间复杂度与其容量(capacity,槽的个数)和现有元素的大小(entry的个数)成正比,所以如果遍历的性能要求很高,不要把capactiy设置的过高或把平衡因子(load factor,当entry数大于capacity*loadFactor时,会进行resize,reside会导致key进行rehash)设置的过低。
由于HashMap是线程非安全的,这也就是意味着如果多个线程同时对一hashmap的集合试图做迭代时有结构的上改变(添加、删除entry,只改变entry的value的值不算结构改变),那么会报ConcurrentModificationException,专业术语叫
fail-fast
,尽早报错对于多线程程序来说是很有必要的。Map m = Collections.synchronizedMap(new HashMap(...));
通过这种方式可以得到一个线程安全的map。
源码剖析
首先从构造函数开始讲,HashMap
遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为Map的构造函数。除此之外,还提供了两个构造函数,用于设置HashMap
的容量(capacity)与平衡因子(loadFactor)。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
从代码上可以看到,容量与平衡因子都有个默认值,并且容量有个最大值
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
可以看到,默认的平衡因子为0.75,这是权衡了时间复杂度与空间复杂度之后的最好取值(JDK说是最好的