我们最开始接触编程的时候第一次使用的数据结构应该都是数组,数组是一块连续的内存的统称,如果拆开来看,那么就是一个一个的变量。数组的内存是静态分配的,也就是给一个数组开辟一块内存之后不能改变其大小。其实数组是变量(数据)和数字(索引)的关联,连续的内存决定数组的查找迅速,但对于删除、添加等操作则上十分麻烦。在此基础上,我们接触了java中的容器类:Map、Collection 、Iterator等接口,对于java的核心结合框架:Set、List、Map,简单的归纳如下:
List
LinkedList:对顺序访问做了优化,可以提供队列、双向队列、栈的功能
ArrayKList:一个用数组实现的List。能进行快速的随机访问,但是往列表中间插入和删除元素的时候比较慢
Set
LinkedHashSet在内部用散列来提高查询速度,但是它看上去像是用链表来保存元素的插入顺序的
HashSet:为优化查询速度而设计的Set,采用的数据结构是“专为快速查找而设计”的散列函数;
TreeSet:是一个有序的Set,其底层是一颗树。这样你就能从Set里面提取一个有序序列了,采用的是红黑树的数据结构为元素排序;
LinkedHashSet(JDK 1.4):一个在内部使用链表的Set,既有HashSet的查询速度,又能保存元素被加进去的顺序(插入顺序),LinkedHashSet在内部用散列来提高查询速度;
HashSet提供了最快的查询速度。而TreeSet则保持元素有序。LinkedHashSet保持元素的插入顺序。
Map
HashMap:采用哈希码算法,以便快速查找一个键值
关于哈希码算法:
1:Object类的hashCode.返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。
2:String类的hashCode.根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串内容相同,返回的哈希码也相同。
3:Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值
TreeMap:用树的数据结构对键值进行了排序
其实弄懂了每个类型的数据结构的内部数据保存方式,那么对它的操作也就知道了,例如用数组就知道查找快而删除添加缓慢,用链表就知道删除添加迅速而查找不如数组那么快,当然,处理数组和链表之外,我们还有一种数据结构-------Map,键值对存储数据。让我们先看一下HashSet的构造器
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity); }
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity); }
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity); }
public HashSet() {
map = new HashMap<E,Object>(); }
太明显了吧,HashSet完全是建立在HashMap之上的,那么,HashMap究竟是怎么实现的呢?
HashMap继承了AbstractMap类,实现了Map、Cloneable、Serializable接口,在HashmMap里面定义了下面的属性:
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
final float loadFactor;
transient volatile int modCount;
其中默认的大小为16,最大的容量为1<<30,装载因子为0.75f,Entry[]是保存Key对象经过hash函数计算之后得到的索引 。
再来看看构造器:
//构造器1
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);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
//构造器2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造器3
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
//构造器4
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
在用构造器1的时候,由于
while (capacity < initialCapacity) capacity <<= 1;
所以不管我们输入的创建Map的大小是多少,HashMap创建的Entry[]将是刚刚大于我们传入的数的2的整数次幂,这样有什么好处?我们再看一下HashMap的关于确定一个Entry保存位置的函数
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); static int indexFor(int h, int length) { return h & (length-1); }
i就是计算得出来的索引位置,也就是该对象的Hash码与表的大小-1相与的结果,刚开始对length-1很不理解,后来才知道这样是为了提供HashMap的效率。因为length是2的整数次幂,那么length的二进制表示就是:10000000......(指数个0),那么length-1就是1111......(指数个1),任何数与length-1相与之后仍然是原来的数,这样就能保证分布均匀,尽量减少了哈希冲突。
构造器2建立在构造器1的基础上,构造器3采用默认的参数,构造器4在一个已经建立好的HashMap上新建一个HashMap。
当我们创建好HashMap对象之后,肯定是要往里面保存东西的,那么HashMap是怎么做的呢?关于添加的方法,HashMap如下:
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;
}
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; e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(0, null, value, 0);
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); }
HashMap可以保存key为null时候的value,方法putForNullKey正是处理这个的。经过计算key的索引之后新建一个Entry对象保存key和value值,然后保存到table[i]的位置,如果哈希冲突了,就替换并返回旧值。但是当我们一直往一个HashMap里添加数据的时候,总会超过创建时候设置的大小,这个时候如何处理?当然是rehash了。在方法addEntry里面有这样一句
if (size++ >= threshold) resize(2 * table.length); //resize方法如下 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
首先, threshold = (int)(capacity * loadFactor)也就是允许保存的最大容量是表的大小乘以装载因子,当存储的数据个数超过这个数的时候,表的大小就扩充为原来的二倍
然后把原来的数据取出重新计算哈希值并保存。
保存完数据之后如何让取出?且看get方法
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;
}
key为null的时候单独处理,不为null的时候计算hash值,如果在表的hash值位置已经挂链,那么就往这条链下面继续走下去一直到找的相同的key返回value值。
那么HashMap又是怎么删除数据的呢?
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e; }
prev = e;
e = next;
}
return e;
}
当然,对于删除链表上的节点这属于链表的操作了,如果删除的是表上面的数据,就把下面剩余的挂链挂在表的空出来的位置的上。对于其他的方法clone、clear之类的就很简单了,put和get方法才是关键。
对于系统提供的HashMap虽然理解了主要的流程和数据结构,但在细微方面仍然有很多不理解:
1,hash()函数
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
这样的算法为什么能最大限度的减少哈希冲突?
2.扩充原表格的大小是解决容量的最好办法?