前言
动力源于兴趣,如果对于技术有足够的兴趣的话,那么欣赏源码也是一件很有乐趣的事情,欣赏大神的代码虽然开始会很困难,但是理解了之后,当你自己写代码的时候就会不自觉的想要将你看到的一些设计模式和风格应用起来。而jdk和spring等一些熟悉的框架无疑就是"大神写的代码"。先从jdk的HashMap开始...
问题从论坛上的一个问题"HashMap的实现原理"开始。刚看见这个问题的时候下意识的想去回答,但是仔细一想,想要完整的说清楚这个问题还是需要整理一番,并且也会引申出一些新的问题
HashMap涉及到hash算法,链表,HashTable和HashSet的区别,新版本的jdk在链表中加入了红黑树,这些都是我思考HashMap所引申到的问题。
HashMap
hashmap是非线程安全的,多线程下可以用concurrentHashmap或者Collections.synchronized()来代替。
首先两种数据结构
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
而HashMap结合了数组和链表。从网上找了一张图,涵盖了整个数据插入的过程。
对上图的理解就是,当你想HashMap中put一个entry(包含key和value)的时候,首先会判断数组是否为空或者长度是否为0,不是的话,就开始通过key来计算数组的下标。这个计算过程就是hash函数,例如f(key)。这个算法的结果最好是可以将不同的key均匀的分散到数组中。同时也有一定的几率不同的key,算出的hash值是相同的,这就产生了hash冲突,此时两个entry在数组的同一个位置产生链表。这样看来,一个hash函数的好坏,主要是看它能否均匀的分布key和产生hash冲突的几率大小。在jdk1.8之后,链表里加入了红黑树算法,加快了链表的存取速度。在上图的右半部分可以看到,当链表长度大于8,转换红黑树,插入键值对。(说道红黑树,这就不得不回忆起当初被数据结构支配的恐惧了。左旋右旋变色调整等等一些理论知识。)
同时HashMap的初始容量(16),阈值(0.75*16),扩容大小(2的次幂)都是很小的知识点。
例如初始容量默认是16,扩容因子是0.75,那么当数组长度是16*0.75时,就会自动扩容,扩大为2倍也就是32.
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 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;
HashTable
hashtable的数据结构和hashmap几乎相同
hashtable和hashmap的主要区别是
1.hashtable线程安全,内部的相关读写操作添加了锁
hashmap非线程安全。
2.hashtable不允许key为空值,当key为空时,会抛出空指针
hashmap允许一个为空的key,放在index为0的位置
3.迭代器不一样。
fail-fast迭代器:如果有其它线程对集合进行的添加/删除元素,将会立刻抛出ConcurrentModificationException.
HashMap的迭代器(Iterator
)是fail-fast迭代器,Hashtable的迭代器(enumerator
)不是fail-fast的。
4.hashtable初始容量11,扩容因子0.75
HashSet
作为Set集合的一种,首先是无序的,不可重复的;
允许存放null值;
底层封装HashMap;
实现是不同步的,线程不安全;
hashset底层封装了hashmap,将值c存放在hashmap的key中,hashset的add方法底层调用了hashmap的put,而判断key是否相同,是通过hashcode和equals方法,所以如果hashset中想要存放object并且不重复的话,需要重写hashcode和equals方法。
总结
最常用或者说最重要的是hashmap这个类,理解了hashmap的实现原理hashtable和hashset自然也就明白了。