哈希表
又被称为散列表,是根据关键码 key 直接访问内存存储位置的数据结构,即通过关于 key的函数,映射到一个地址来访问数据,这样加快查找速度
解决哈希冲突:
链地址法:数组+链表 HashMap 的数据结构
key ->f(key) ->address(index)判断是否有元素
1)构造链表
2)直接插入元素 O(1)->O(N)
开放地址法:HashMap 本身处理海量数据,当位于同一个位置中的元素越来越多,hash 值相等的元素越来越多,使用 key查找效率低
链表长度超过阈值8时 会将链表结构转为红黑树结构
1、HashMap 的使用
1)HashMap的创建
Map<String,Integer>map=new HashMap<>();
2)部分使用
map.put("lian",21);//添加
map.put("xue",22);
System.out.println(map.get("xue"));
System.out.println(map.remove("xue"));//删除
System.out.println(map.size());//大小
System.out.println(map.isEmpty());判空
System.out.println(map.containsKey("sun"));//搜索key
System.out.println(map.containsValue(21));//搜索value
3)遍历/获取所有点的集合
//获取所有点的集合
Set<Map.Entry<String,Integer>>entries=map.entrySet();
//HashMap当中所有的元素作为一个entry 节点存在,所有的系欸但封装为一个set
//获取set集合的迭代器对象
Iterator<Map.Entry<String,Integer>>iterator= entries.iterator();
while(iterator.hasNext()){
//判断是否含有下一个可迭代的元素
//获取下一个可迭代的元素
Map.Entry<String,Integer>next= iterator.next();
//分别获取键和值
System.out.println(next.getKey()+":"+next.getValue());
}
2、HashMap的底层结构
HashMap底层就是一个数组,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组
class Node<K, V> {
protected K key;
protected V value;
private Node<K, V> next;
private int hash;
public Node(int hash, K key, V value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
2)增加键值对
增加时要考虑是否key存在
不存在 直接将key-value键值对封装成为一个Node 直接放到index位置
存在 key重复 考虑新值去覆盖旧值
存在 key不重复 尾插法 将key-value键值对封装成为一个Node 插入新节点
public void put(K key, V value) {
//key->Hash值->index
int hash = hash(key);//散列码
int index = table.length - 1 & hash;
//当前index位置不存在节点
Node<K, V> firstNode = table[index];
if (firstNode == null) {
//table[index]位置不存在节点 直接插入
table[index] = new Node(hash, key, value);
size++;
return;
}
//key不允许有重复的
//查找当前链表中key是否已经存在
//当前位置存在节点 判断key是否重复
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) {
//表示最后一个节点之前的所有节点都不包含key
if (tmp.key.equals(key)) {
//最后一个节点的key与当前所要插入的key是否相等,考虑新值覆盖旧值
tmp.value = value;
} else {
//如果不存在,new Node,尾插法插入链表当中
tmp.next = new Node(hash, key, value);
size++;
}
} else {
//如果存在,考虑新值覆盖旧值
tmp.value = value;
}
}
}
3)获取key所对应的value
public V get(K key) {
//获取key所对应的value
//key->index
int hash = hash(key);
int index = table.length - 1 & hash;
//在index位置的所有节点中找与当前key相等的key
Node<K, V> firstNode = table[index];
//当前位置点是否存在节点 不存在
if (firstNode == null) {
return null;
}
//判断第一个节点
if (firstNode.key.equals(key)) {
return firstNode.value;
} else {
//遍历当前位置点的链表进行判断
Node<K, V> tmp = firstNode.next;
while (tmp != null && !tmp.key.equals(key)) {
tmp = tmp.next;
}
if (tmp == null) {
return null;
} else {
return tmp.value;
}
}
}
4)删除
public boolean remove(K key) {
//key->index
//当前位置中寻找当前key所对应的节点
int hash = hash(key);
int index = table.length - 1 & hash;
Node<K, V> firstNode = table[index];
if (firstNode == null) {
//表示table桶中的该位置不存在节点
return false;
}
//删除的是第一个节点
if (firstNode.key.equals(key)) {
table[index] = firstNode.next;
size--;
return true;
}
//相当于在链表中删除中间某一个节点
while (firstNode.next != null) {
if (firstNode.next.key.equals(key)) {
//firstNode.next是所要删除的节点
//firstNode是它的前一个节点
//firstNode.next.next是它的后一个节点
firstNode.next = firstNode.next.next;
size--;
return true;
} else {
firstNode = firstNode.next;
}
}
return false;
}
5)扩容
public void resize() {
//HashMap的扩容
//table进行扩容 2倍的方式 扩容数组
Node<K, V>[] newTable = new Node[table.length * 2];
//index -> table.length-1 & hash
//重哈希
for (int i = 0; i < table.length; i++) {
rehash(i, newTable);
}
this.table = newTable;
}
3、HashMap源码分析
1)类的继承关系
* public class HashMap<K,V> extends AbstractMap<K,V>
* implements Map<K,V>, Cloneable, Serializable
* HashMap允许空值和空键
* HashMap是非线程安全
* HashMap元素是无序 LinkedHashMap TreeMap
* (HashTable不允许为空 线程安全)
* 2)类的属性
* static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 16 默认初始容量 用来给table初始化
* static final int MAXIMUM_CAPACITY = 1 << 30;
* static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容机制
* static final int TREEIFY_THRESHOLD = 8; //链表转为红黑树的节点个数
* static final int UNTREEIFY_THRESHOLD = 6;//红黑树转为链表的节点个数
* static final int MIN_TREEIFY_CAPACITY = 64;
* static class Node<K,V> implements Map.Entry<K,V>
* transient Node<K,V>[] table; //哈希表中的桶
* transient Set<Map.Entry<K,V>> entrySet; //迭代器遍历的时候
* transient int size;
* int threshold;
* final float loadFactor;
* 3)类中重要的方法 (构造函数 put remove resize)
* 构造函数中并未给桶进行初始化
*
* put
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
* //resize() 初始化(和扩容)
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* //当前位置不存在节点,创建一个新节点直接放到该位置
* else{
* //当前位置存在节点 判断key是否重复
* if (p.hash == hash &&
* ((k = p.key) == key || (key != null && key.equals(k))))
* e = p;
* //判断第一个节点的key是否与所要插入的key相等
* //hashCode 表示将对象的地址转为一个32位的整型返回 不同对象的hashCode有可能相等
* //比较hash相比于使用equals更加高效
* else if (p instanceof TreeNode)
* //判断当前节点是否是红黑树节点
* //是的话,则按照红黑树插入逻辑实现
* e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* else {
* for (int binCount = 0; ; ++binCount) {
* if ((e = p.next) == null) {
* p.next = newNode(hash, key, value, null);
* if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
* treeifyBin(tab, hash);
* break;
* }
* if (e.hash == hash &&
* ((k = e.key) == key || (key != null && key.equals(k))))
* break;
* //判断e是否是key重复的节点
* p = e;
* }
* }
* }
4、HashMap常见面试题分析
1)JDK1.7与JDK1.8HashMap有什么区别和联系
a. 底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
b. jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;1.8直接调用resize()扩容;
c. 插入键值对的put方法,1.8中会将节点插入到链表尾部,而1.7中是头插;
d. 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;
e. 1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
f. 扩容策略不同,1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
2)用过HashMap没?说说HashMap的结构(底层数据结构 + put方法描述)
HashMap的底层是由数组+链表+红黑树
自定义put方法
* 1)key-> hash(key) 散列码 -> hash & table.length-1 index
* 2)table[index] == null 是否存在节点
* 3)不存在 直接将key-value键值对封装成为一个Node 直接放到index位置
* 4)存在 key不允许重复
* 5)存在 key重复 考虑新值去覆盖旧值
* 6)存在 key不重复 尾插法 将key-value键值对封装成为一个Node 插入新节点
3)说说HashMap的扩容过程
HashMap在内部数组无法装载更多对象时需要对数组进行二倍扩容。
对原数组中的数据进行重新rehash。
4)HashMap中可以使用自定义类型作为其key和value吗?
需要重写hashCode()和equals()方法才能实现自定义键在HashMap中的查找。
5)HashMap中table.length为什么需要是2的幂次方
位运算更高效,为了能让hashMap存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀。
6)HashMap与HashTable的区别和联系
都实现了Map接口,保存了Key-Value(键值对)
两者的数据结构类似。HashMap和HashTable都是由数组元素为链表头节点的数组组成。
HashMap继承AbstractMap类,而HashTable继承Dictionary类。
HashMap是线程不安全的,是非Synchronize(同步)的,而HashTable是线程安全的,是Synchronize的。
HashTable中key和value都不允许为null,HashMap中空值可以作为Key,也可以有一个/多个Key的值为空值。
HashMap的hash数组默认长度大小为16,扩容方式为2的指数,HashTable的hash数组默认长度大小为11,扩容方式为两倍加一。
7)HashMap、LinkedHashMap、TreeMap之间的区别和联系?
HashMap和LinkedHashMap最多只允许一条key为Null,允许多条value为Null
HashMap继承AbstractMap,LinkedHashMap继承HashMap
HashMap基于Key-Value的散列表,LinkedHashMap是基于双向链表散列
HashMap在遍历时是无序的,LinkedHashMap遍历时按照插入的顺序或者访问的顺序进行遍历
HashMap(JDK1.8下)和TreeMap都是基于数组加链表实现的
HashMap和TreeMap都是非线程安全的
HashMap继承于抽象类AbstractMap,并且实现Map接口,TreeMap实现SortedMap接口
HashMap遍历时,取得的数据完全是随机的,TreeMap默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的
HashMap添加、删除操作时间复杂度都是O(1),TreeMap添加、删除操作时间复杂度都是O(log(n))
HashMap最多只允许一条key为Null,允许多条value为Null,TreeMap只允许value为Null,key不能为Null
HashMap里面存入的键值对bai在取出的时候是随机的,也是我们最常用的一个Map.它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。
TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现. (应用场景:购物车等需要顺序的)
8)HashMap与WeakHashMap的区别和联系
HashMap和WeakHashMap都继承AbstractMap
HashMap和WeakHashMap都实现了Map接口
HashMap和WeakHashMap都保存了key-value键值对
HashMap和WeakHashMap一般用于单线程中,是非线程安全的
HashMap和WeakHashMap最多只允许一条key为Null,允许多条value为Null
HashMap和WeakHashMap添加、删除操作时间复杂度都是O(1)。
HashMap和WeakHashMap都是非Synchronize(同步)的
HashMap的键是强键,weakHashMap中的键是弱键(当弱键被垃圾回收GC回收时,其对应的键值也会从WeakHashMap中删除)
9)WeakHashMap中涉及到的强弱软虚四种引用
强引用
这是最常见的引用关系,变量o对 new object()这个对象(下称对象xx)的进行引用,o持有对象的强引用,宁愿内存溢出也不清除强引用的内存
obj置为null的情况下,如果想继续对对象xx进行引用处理,只能再次new一个出来,在这种场景下,jdk1.2后出了一个java.lang.ref包,加强了对对象的生命周期的控制,同时也可以作为java的内置缓存,使得在引用不可达的情况下 仍可以被使用
弱引用
一旦执行GC就会清除
软引用
丢到SoftReference中就是软引用,内存快溢出了就把软引用的干掉
虚引用
当多种引用关系并存的时候以那个为准呢?
强引用>软引用>弱引用>虚引用 当然是以强的为主
比如前面代码的若是obj不置为null,那即便obj有软/弱引用,那对象也是强引用
10)HashMap是线程安全的吗?
不是线程安全。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。