HashMap面试相关整理
问题1:HashMap的基本原理和Hash冲突。
-
结构:键值对 Key,value 结构。数组(方便查找 O(1))+链表(方便增删 O(n))(1.8 链表元素大于8转红黑树 查询效率log(n) )
-
最大值 static final int MAXIMUM_CAPACITY = 1 << 30;
-
父类: 继承自AbstractMap<K,V> 实现接口Map<K,V>
-
HashMap线程不安全,ConcurrentHashMap(Segment分段锁)线程安全、效率高、HashTable(synchronized)线程安全、效率低。
-
允许NULL Key和Value
-
初始容量:static final int DEFAULT_INITIAL_CAPACITY = 16;
- 必须是2的指数次幂,如果初始值是13 那么会转换成 >13的2的最小指数次幂16;
- 必须是2的指数次幂:方便后期HASH计算 扩容等。
-
负载因子: static final float DEFAULT_LOAD_FACTOR = 0.75f;空间和查询效率的平衡。
- 如果是1,最大限度提高空间利用率 只用八个内存(实际情况不太可能 都分布在数组上)。查询效率慢。
- 0.5,查询效率最高(HASH冲突少,链表/树 元素比较少)占容量。
-
红黑树:链表元素达到8,会转换成红黑树。
- 泊松分布:概率学。随着链表元素的增加,出现HASH冲突的几率指数级下降。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
- 封装一个实体类
package com.javaee.collections.map;
/**
*
* @Title: MapElement
* @Description: TODO(检测Hash冲突)
* @author X-Dragon
* @version V1.0
*
*/
public class MapElement {
private int id;
private String name;
public MapElement() {
super();
}
public MapElement(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// @Override
// public int hashCode() {
// final int prime = 31;
// int result = 1;
// result = prime * result + id;
// result = prime * result + ((name == null) ? 0 : name.hashCode());
// return result;
// }
/**
*
* @Title: hashCode
* @Description: TODO(返回值都是1,用于测试Hash冲突)
* @return
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MapElement other = (MapElement) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "MapElement [id=" + id + ", name=" + name + "]";
}
}
- 测试类
package com.javaee.collections.map;
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
HashMap<MapElement, String> map = new HashMap<MapElement, String>();
MapElement mapElement1 = new MapElement(1, "xielong1");//A 放入
MapElement mapElement2 = new MapElement(2, "xielong2");//B 放入
MapElement mapElement3 = new MapElement(2, "xielong2");//C 不放入
MapElement mapElement4 = new MapElement(2, "xielong4");//D 放入
if(mapElement1.hashCode()==mapElement2.hashCode()){
System.out.println("hashCode相等:");
}
map.put(mapElement1, mapElement1.getName());
map.put(mapElement2, mapElement2.getName());
map.put(mapElement3, mapElement3.getName());
map.put(mapElement4, mapElement4.getName());
map.put(null, null);//放入NULL
// map.put(null);//语法报错
System.out.println(map.size());//包含NULL4个 不算NULL 三个
for(Map.Entry<MapElement, String> entry:map.entrySet()){//遍历元素
System.out.println("Key:"+entry.getKey()+"Value:"+entry.getValue());
}
}
}
-
结论:
-
put方法
- 判断是否可以放入MAP集合。首先判断key的hashCode()是否冲突,不冲突直接放入。如果hashCode()一致,然后判断equals()方法是否相等。如果equals()不相等,就根据Key的hashCode()找到对应的位置放入KEY冲突的数据。1.8是尾插法,1.7是头插法。
- 当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
- equals()和hashCode()方法都要一起重写。
-
get方法
-
根据Key找到对应的hashCode()方法找到bucket位置,然后获取值对象。
-
如果两个键的hashcode相同,当我们调用get()方法找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
-
许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
-
问题2:4HashMap怎么进行动态扩容
- 扩容的方式是新建一个newTab,是oldTab的2倍。遍历oldTab,将oldTab赋值进对应位置的newTab。与ArrayList中的扩容逻辑基本一致,只不过ArrayList是当前容量+(当前容量>>1)。
- JDK1.8使用了红黑树(自平衡二叉查找树)TreeMap相对复杂。
- 链表长度大于8链表会转换成红黑树。
- 如何避免扩容:比如要存放32个元素,可以设置初始化容量为32,加载因子是1.0。
红黑树:
- 是一个接近于自平衡的二叉树。
- 查询方便:O(log2N)
- 频繁插入,效率不高。插入元素 需要左旋、右旋再平衡,重新着色。
区分AVL树(平衡二叉树)
- 实际用到很少。
- 不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况。
https://blog.csdn.net/u010899985/article/details/80981053?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522161063959316780255240903%252522%25252C%252522scm%252522%25253A%25252220140713.130102334.pc%25255Fall.%252522%25257D&request_id=161063959316780255240903&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-3-80981053.pc_search_result_cache&utm_term=%E7%BA%A2%E9%BB%91%E6%A0%91
问题3:HashMap、HashTable、HashSet区别
- A:HashSet是set的一个实现类,hashMap是Map的一个实现类,同时hashMap是hashTable的替代品。
- HashMap线程不安全,允许空KEY和空VALUE,不允许放入一整个NULL,CurrentHashMap线程安全、效率高、HashTable线程安全、效率低、不允许放入任何空值。
- HashMap和Hashtable的区别
- 1 继承和实现方式不同
- HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
https://www.cnblogs.com/skywang12345/p/3311126.html - 2 HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
- 3 Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。 - 就HashMap与HashTable主要从三方面来说。
一.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现
二.同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的
三.值:只有HashMap可以让你将空值作为一个表的条目的key或value
问题4. 为啥HashMap是线程不安全的:
HASHMAP为啥线程不安全
1、put的时候导致的多线程数据不一致。
- 比如有两个线程A和B。首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)
问题5:concurrentHashMap
- 线程安全的HashMap:
- 线程安全原理:分段锁技术,segment。concurrentHashMap由segment[]数组组成。
- 并发级别(concurrencyLevel几个锁):16个元素,8个锁。代表segment[8] ,数组长度为8,一个数组两个元素。
- segment:本质是一个有锁的小hashMap。
- put方法:调用put()方法,根据Key值判断属于哪个segment,大概率不会遇到加锁的情况。如果遇到加锁,等到锁释放在调用PUT方法。