HashMap面试相关

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方法。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值