HashMap总结
hashmap的基于数组和链表及红黑树实现的哈希表。采用数组作为键值对的容器,数组中存放的是单链表的头节点。hashmap采用链表的方式(链地址法)处理hash冲突,当发生冲突时,将冲突的元素链接到链表上去。hashmap的key和value都支持null值,而hashtable则不支持。它不是同步的,但是可以通过适当的方法变为支持同步的。
哈希表
1、 散列表,是根据关键码值(Key)进行访问的数据结构,也就是说,通过将Key映射到表中一个位置来获取记录,加快查找的速度,这个映射函数叫做撒案列函数,存放记录的结构 称之为散列表
寻址容易,删除插入也容易的数据结构
张三 15376467384 ZS 90+83 = 173 //key value
李四 18548787753 LS 76+83 = 159
王五 19847875837 WW 87+87 = 174
张帅 14338477838 ZS 90+83 = 173
class People{
private String name;
private String number;
}
2、 查找的时间复杂度:
链表 O(N)
二叉排序树 O(log2 N)
散列表 key - value O(1)
key -》 hash函数 -》 name所对应的首字母的ASCII值 -> 散列码
3、哈表冲突
通常以下几种方式构造哈希函数
1)直接定址法 key address(key) = a*key + b
2)平方取中法 key 108 109 110 = 108^2 109^2 110^2
3)折叠法 key -》 拆分为几部分 区域号-书架号-图书编号
4)除留取余法 key -》 hash表的最大长度m 取不大于m的质数 key%质数
4、解决冲突
1)开放地址法 12 13 25 23 hash表的长度14 hash函数key%13
2)链地址法
HashMap基本用法
static void main(String[] args) {
//声明HashMap对象
Map<String, Integer> map = new HashMap<>();
//添加元素
map.put("zhangsan", 98);
map.put("lisi", 100);
map.put("wangwu", 102);
map.put("zhangsan", 1000);
//根据key获取记录
System.out.println(map.get("lisi")); //100
//根据key删除记录
System.out.println(map.remove("lisi"));//100
//获取map中记录个数
System.out.println(map.size()); //2
//判断map集合中是否包含键为key的记录
System.out.println(map.containsKey("zhangsan"));//true
System.out.println(map.containsKey("xiaohua"));//false
System.out.println(map.isEmpty());//false
//清空map集合
map.clear();
System.out.println(map.isEmpty());//true
}
HashMap迭代器的使用:
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("a", "tulun1");
map.put("b", "tulun2");
map.put("c", "tulun3");
map.put("aa", "tulun4");
Iterator<Map.Entry<String, String>> itr = map.entrySet().iterator();
while(itr.hasNext()){
Map.Entry<String, String> entry = itr.next();
System.out.println(entry.getKey() + ":: "+entry.getValue());
}
}
迭代后的结果
自定义HashMap
/**
* 自定义HashMap,hash算法直接使用HashMap中的hash算法,解决哈希冲突
* 采用链地址法,即链表+数组实现,包括方法有put(K key, V value),get(K key),
remove(K key)等方法
*
* hashCode方法是将引用类型的内存地址转换为一个32位的整型返回,所以存在一定的
* 限制,导致两个不同对象有可能hashCode也会相等,这样的话比较了hash值还需要比较
引用,才能够保证两个对象完全相等。
*
* 哈希表扩容
*
* HashMap迭代器实现
*
* 1)哈希表中数据分布是不连续的,在迭代器初始化的过程中必须先跳转到第一个非空数据节点
* 2)当迭代器的下标到达当前桶的链表末尾时,迭代器下标跳转到下一个非空桶的第一个数据节点
*
*/
class MyHashMap<K,V>{
private Node<K, V>[] table;
private int size;
class Node<K,V>{
protected K key;
protected V value;
protected int hash;
protected Node<K, V> next;
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
public MyHashMap(int capacity){
table = new Node[capacity];
}
public Iterator<Node<K, V>> iterator(){
return new Itr();
}
class Itr implements Iterator<Node<K, V>>{
private int currentIndex;//当前桶的位置
private Node<K, V> nextNode;//返回的下一个数据节点
private Node<K, V> currentNode; //上一次next返回的数据节点
public Itr(){
if(MyHashMap.this.isEmpty()){
return;
}
for(int i=0; i < table.length; i++){
currentIndex = i;
Node<K, V> firstNode = table[i];
if(firstNode != null){
nextNode = firstNode;
return;
}
}
}
@Override
public boolean hasNext() {
return nextNode != null;
}
@Override
public Node<K, V> next() {
//返回下一个数据节点
currentNode = nextNode;
//nextNode指向自己的next
nextNode = nextNode.next;
if(nextNode == null){
//说明当前桶的链表已经遍历完毕
//寻找下一个非空的桶
for(int i=currentIndex+1; i<MyHashMap.this.table.length; i++){
//设置当前桶位置
currentIndex = i;
Node<K,V> firstNode = MyHashMap.this.table[currentIndex];
if(firstNode != null){
nextNode = firstNode;
break;
}
}
//nextNode保存的就是下一个非空的数据节点
}
return currentNode;
}
@Override
public void remove() {
if(currentNode == null){
return;
}
K currentKey = currentNode.key;
MyHashMap.this.remove(currentKey);
currentNode = null;
}
}
public void resize(){
//扩容桶table
Node<K, V>[] newTable = new Node[table.length * 2];
for(int i=0; i<table.length; i++){
//将oldTable中每一个位置映射到newTable中
rehash(i, newTable);
}
this.table = newTable;
}
public void rehash(int index, Node<K,V>[] newTable){
Node<K, V> currentNode = table[index];
if(currentNode == null){
return;
}
Node<K, V> lowListHead = null; //低位的头
Node<K, V> lowListTail = null; //低位的尾
Node<K, V> highListHead = null; //高位的头
Node<K, V> highListTail = null; //高位的尾
//currentNode表示oleTable下index位置中当前节点
while(currentNode != null){
//当前节点在newTable中的位置
int newIndex = newTable.length-1 & hash(currentNode.key);
if(newIndex == index){
//映射到原先下标处
if(lowListHead == null){
lowListHead = currentNode;
lowListTail = currentNode;
}else{
lowListTail.next = currentNode;
lowListTail = lowListTail.next;
}
}else{
//newIndex与index不等,映射到高位下标处
if(highListHead == null){
highListHead = currentNode;
highListTail = currentNode;
}else{
highListTail.next = currentNode;
highListTail = lowListTail.next;
}
}
currentNode = currentNode.next;
}
//将lowList head->tail之前的节点链接到index位置
if(lowListTail != null){
newTable[index] = lowListHead;
lowListHead.next = null;
}
//将highList head->tail之前的节点链接到index+table.length位置
if(highListTail != null){
newTable[index+this.table.length] = highListHead;
highListHead.next = null;
}
}
public int hash(Object key) {
int h;
return (h = key.hashCode()) ^ (h >>> 16);
}
public void put(K key, V value){
//确定所要添加元素的位置
int hash = hash(key);//散列码
int index = table.length-1 & hash;//确定的位置
//newNode -》table[index] == null || key在map不存在
//table[index]已经存在数据
//table[index]不存在数据
Node<K, V> firstNode = table[index];//得到该位置的第一个节点
if(firstNode == null){
table[index] = new Node(key, value, hash);
size++;
}else{
//第一种情况 key已经存在 考虑新值覆盖旧值
//第二种情况 key不存在 封装一个新的node尾插法插入链表
if(firstNode.key.equals(key)){
firstNode.value = value; //新值替换旧值
}else{
Node<K,V> tmp = firstNode;
while(tmp.next != null && !tmp.key.equals(key)){
tmp = tmp.next;
}
if(tmp.next == null){
if(tmp.key.equals(key)){
tmp.value = value; //新值替换旧值
}else{
tmp.next = new Node(key, value, hash);
size++;
}
}else{
tmp.value = value;
}
}
}
}
public boolean remove(K key){
int hash = hash(key);
int index = table.length-1 & hash;
Node<K, V> firstNode = table[index];
if(firstNode == null){
return false;
}else{
if(firstNode.next == null){
if(firstNode.key.equals(key) && firstNode.hash == hash){
//为什么判断key是否相等的同时还需要判断散列码
table[index] = null;
return true;
}
}
Node<K,V> tmpPrev = firstNode;
Node<K, V> tmp = firstNode.next;
while(tmp.next != null){
if(tmp.key.equals(key) && tmp.hash == hash){
//tmp之前节点的next指向tmp.next
tmpPrev.next = tmp.next;
size--;
}else{
tmpPrev = tmp;
tmp = tmp.next;
}
}
}
return false;
}
public Node<K,V> get(K key){
//获取对应key所在数组的index
int hash = hash(key);//散列码
int index = table.length - 1 & hash; //定位key记录所在的位置
Node<K, V> firstNode = table[index];
if (firstNode == null) {
return null;
}
if (hash == firstNode.hash && key == firstNode.key) {
return firstNode;
} else {
//遍历链表
Node<K, V> currentNode = firstNode.next;
while (currentNode != null) {
if (currentNode.hash == hash && currentNode.key == key) {
return currentNode;
}
currentNode = currentNode.next;
}
}
return null;
}
public boolean isEmpty(){
return size == 0;
}
}
HashMap源代码分析
继承的类与实现的接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
继承了AbstractMap类;
实现了Map<K,V>, Cloneable, Serializable三个接口。
类的属性
初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
初始化容量为1左移4位(即二进制00001左移4位变为10000=16)
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量为2^30
桶中结构转化为红黑树对用的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f
由链表转换为树的阈值
static final int TREEIFY_THRESHOLD = 8;
桶上的节点个数大于8时会转为红黑树
由树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
桶上的绩点个数小于6时会转为链表
存储元素的数组
transient Node<K,V>[] table;
大小总是2的幂次方
哈希表中节点的集合
transient Set<Map.Entry<K,V>> entrySet;
类的构造函数
HashMap有4种构造方法:
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
2、指定初始容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3、使用默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
4、从其他Map实例创建HashMap对象
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
常用的方法
put操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//tab为空
n = (tab = resize()).length;//重置大小
if ((p = tab[i = (n - 1) & hash]) == null)//首节点为null
tab[i] = newNode(hash, key, value, null);//插入节点
else {//首节点不为null
Node<K,V> e; K k;
if (p.hash == hash &&//这里p即为首节点
((k = p.key) == key || (key != null && key.equals(k))))//是首节点
e = p;//e指向首节点
else if (p instanceof TreeNode)//p不是首节点,如果p是TreedNode的实例,也就是按照树结构存放的话
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//按照树的方式put节点
else {//如果p不是首节点,且是按照链表存放
for (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) {//判断p.net是否为null,是则到了链表尾
p.next = newNode(hash, key, value, null);//插到尾部
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //节点数量大于阈值TREEIFY_THRESHOLD
treeifyBin(tab, hash);//将链表存储转化为树存储
break;//结束循环
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//如果key存在
break;//结束循环
p = e;//上边以及赋值e=p.next,这里就相当于p=p.next,移动指针到下一个节点
}
}
if (e != null) { // existing mapping for key //如果key存在
V oldValue = e.value;//得到旧value
if (!onlyIfAbsent || oldValue == null)//如果不是仅当键不存在才插入或者原来的值为null
e.value = value;//赋值value给e
afterNodeAccess(e);//添加之后的操作
return oldValue;//返回旧值
}
}
++modCount;//操作次数加1
if (++size > threshold)//判断阈值
resize();//更改大小
afterNodeInsertion(evict);
return null;
}
大致的流程图:
大致思路流程:
首先判断hashtable是否为null,如果为null,则先初始化容量;根据key计算处于桶中的索引位置,然后判断头节点是否为null,如果为null则插入节点;如果头节点不为空,判断key是否存在,如果是新值覆盖旧值,如果不存在,先判断是按照链表存储还是按照树的结构存储,如果是按照树的结构存储则就按照树的插入节点的方法插入,否则按照链表的方式插入。在按链表的方式插入时,移动引用,找到链尾,插入节点。在移动过程中会判断当前链表的容量与初始阈值的大小(判断链表长度是否大于8,如果大于8,则传化为红黑树;检查key是否存在,key不存在插入新节点,存在新值覆盖旧值),以此来决定要不要转化为树形结构存储。最后,插入成功返回旧的值,插入失败返回null。
参数:
hash : key的hash值
key : 键
value : 值
onlyIfAbsent : 仅当键不存在才添加
evict : 是否回收
判断key是否存在
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
resize();get();remove()方法的分析差不多一样这里就不分析了