HashMap原理、hashmap常见面试题

HashMap

Map遍历

map有多种遍历方法,这里是最简单的一种,通过遍历key,来得到key和对应的value

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
//遍历map中的key
for (Integer key : map.keySet()) {
    System.out.println("Key = " + key+);
    
    //因为hashmap的时间复杂度接近o(1),所以根据key获取value并不消耗性能;
    System.out.println("value = " + map.get(key));
}

另一种

遍历Map.Entry

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());

}

equals()和hashcode()方法

Object的hashcode()方法返回对象的hashcode,hashcode由对象的地址转换为整数得来的;
每一个对象的对象头里都存储了一个hashcode;

equals()方法比较的是地址

查找散列集合,先比较key的hashcode,若hashcode相同,则使用使用equals()方法比较value;

java官方对hashCode()方法的说明:
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。


测试

只重写equals方法,不重写hashCode方法
public class HashCodeTest {
	public static void main(String[] args) {
		Collection set = new HashSet();
		Point p1 = new Point(1, 1);
		Point p2 = new Point(1, 1);
 
		System.out.println(p1.equals(p2)); //因为重写了equels方法,所以输出true
		
		set.add(p1);   //p1成功存入集合中
		set.add(p2);   //p1、p2是2个不同的对象,因次其hashcode也不同,故p1、p2会被认为是2个不同的对象,p2被成功存入集合;
		set.add(p1);   //之前p1已经存入集合,当再存入p1时,发现存在hashcode相同的元素,因次使用equals方法比较value也相同,因次认为p1已经存在于集合,故舍弃现在的p1;
 
 /*当使用散列集合时,我们想要: 2个对象中的x,y只要相等,就认为相等,在这里只重写equals方法;
 ,因为默认调用的父类Object的hashCode方法,故只要不是同一个对象,得到的hashCode必不相同;
 没有重写hashCode方法,p1,p2这2个对象依旧会被认作为2个不同数据*/
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			System.out.println(object);
		}
	}
}


@Data
@AllArgsConstructor 
class Point {
	private int x;
	private int y;
 
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Point other = (Point) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}
 
	@Override
	public String toString() {
		return "x:" + x + ",y:" + y;
	}
 
}
true
x:1,y:1
x:1,y:1

测试内存泄漏

注意,重写hashCode方法,使用不当容易造成内存泄漏;

public class HashCodeTest {
	public static void main(String[] args) {
		Collection set = new HashSet();
		Point p1 = new Point(1, 1);
		Point p2 = new Point(1, 2);
 
		set.add(p1);
		set.add(p2);
		
  /*注意,这里重写了hashCode方法,返回的hashCode与x,y的值有关 
  先将p1,p2放入了set中,然后又改变了p2的x,y值,那么此时对应的p2的hashcode也会改变;
 
  当执行set.remode(p2)时,由于此时p2的hashcode已经改变,故时删除不了p2,但是p2依旧在内存中;*/
		p2.setX(10);
		p2.setY(10);
		
		set.remove(p2);
 
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			System.out.println(object);
		}
	}
}
 
 
@Data
@AllArgsConstructor
class Point {
	private int x;
	private int y;
 
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + x;
		result = prime * result + y;
		return result;
	}
 
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Point other = (Point) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}
 
	@Override
	public String toString() {
		return "x:" + x + ",y:" + y;
	}
}
x:1,y:1
x:10,y:10

如何正确的重写equals() 和 hashCode()

https://blog.youkuaiyun.com/zzg1229059735/article/details/51498310


JDK1.7

JDK1.7 HashMap结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IRQNlAW-1621011692748)(https://note.youdao.com/yws/res/69205/B64E0AD831044FA786862F0700EA53DE)]

HashMap地层是 数组 + 链表 实现的;
数组的每个索引被称作桶

HashMap底层维护一个数组,数组中的每一项都是一个Entry

transient Entry<K,V>[] table;

向 HashMap 中所放置的对象实际上是存储在该数组table当中;

而Map中的key,value则以Entry的形式存放在数组中

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        /** 指向下一个元素的引用 */
        Entry<K,V> next;
        int hash;
        ...
 } 

数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分key(键)、value(值),next(指向下一个Entry)

通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象,则以链表形式存储。

初始化

构造器源码

    /** 初始容量,默认16 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
    /** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
 //即指定初始化容量,又指定负载因子   
    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);
        // 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
    
//只指定初始化容量,负载因子默认为0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
//无参构造器,初始数组(table)容量默认为16,负载因子为0.75
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);
        putAllForCreate(m);
    }
无参构造器HashMap<String,String> m = new HashMap<>();

则默认初始化数组容量为16,负载因子0.75 ;

初始化时 指定容量HashMap<String,String> m = new HashMap<>(int initialCapacity);

若 指定的容量不是2的次幂,则返回一个大于且最接近它的2次幂的值;故 hashmap初始化和永远都是2次幂;
例如HashMap<String,String> m = new HashMap<>(31)则初始化为32

put操作

put源码

public V put(K key, V value) {  
    // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
    // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
    if (key == null)  
        return putForNullKey(value);  
    // 若“key不为null”,则计算该key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在对应table中的索引
    int i = indexFor(hash, table.length);  
    // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        // 哈希值相同并且对象相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 修改次数+1
    modCount++;
    // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
    addEntry(hash, key, value, i);  
    return null;  
}  

put操作大致流程为:
然后对key判空,如果key==null,则遍历数组table[0],如果链表中存在key==null,则替换掉它的value并返回旧value,如果链表中不存在key==null的entry,则直接添加进去;

如果key!=null,则通过key的hashcode算出table(数组)的下标,然后先判断table对应位置是否为null(为null 表示此处的链表为null 即 还没有任何数据),如果null则遍历查看是否存在相同的key,如果存在则替换value,如果不存在则插入到链表里;

get操作

如果key==null,则遍历table[0]链表,返key==null的entry的value,
如果key!=null,则根据key的hashcode计算出table数组(桶)的下标,然后遍历链表并返回key相等的entry的value。

扩容机制

当HashMap中的元素超过了 加载因子 与当前 容量(数组大小) 的乘积(默认16*0.75=12)时,通过调用resize方法重新创建一个原来HashMap大小的两倍的newTable数组,重新计算hash,然后再重新根据hash分配位置。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 根据新传入的newCapacity创建新Entry数组
        Entry[] newTable = new Entry[newCapacity];
        // 用来将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 再将newTable赋值给table
        table = newTable;
        // 重新计算临界值,扩容公式在这儿(newCapacity * loadFactor)
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
扩容机制的性能

原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。

所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。

线程安全

HasmMap线程不安全

hashmap里面的方法没有进行同步
扩容 导致死循环

JDK 1.7 hashmap使用的头插法,高并发下 会死循环;

https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng


JDK1.8

JDK1.8中HashMap底层实现为 数组+链表+红黑树 ;桶中的结构可能是链表,也可能是红黑树

JDK1.7中,如果hash冲突的概率高,就会使一个桶中的链表过长,遍历效率低;

JDK1.8中,当数组大于64 链表大于8 时 才会转化为红黑树,当红黑树小于6时 就会退化成链表。

JDK 1.8HashMap底层实现:数组+链表+红黑树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDK1Tlym-1621011692750)(https://note.youdao.com/yws/res/79978/08498035AE6640B6866FCB386F3E98AE)]

初始化

无参构造器HashMap<String,String> m = new HashMap<>();

则默认初始化数组容量为16,负载因子0.75 ;

初始化时 指定数组容量HashMap<String,String> m = new HashMap<>(int initialCapacity);

若 指定的数组容量不是2的次幂,则返回一个大于且最接近它的2次幂的值;故 hashmap初始化和永远都是2次幂;
例如HashMap<String,String> m = new HashMap<>(31)则初始化为32,比如传入6,实际分配8;

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    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;
//tableSizeFor(initialCapacity) 如果构造函数设置的初始容量不是2的次幂,那么使用以下方法返回一个大于且最靠近它的2的次幂的值
        this.threshold = tableSizeFor(initialCapacity);
    }

hash表扩容后,如果发现某一个红黑树的长度小于6,则会重新退化为链表


Collections.synchronizedMap(Map map)

HashMap线程不安全,除了使用ConcurrentHashMap 以为,还可以使用Collections.synchronizedMap(Map map)返回一个线程安全的map;

    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        return new SynchronizedMap<>(m);
    }
    private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
 
        private final Map<K,V> m;     // Backing Map
        final Object      mutex;  
        
        ...
        
        public int size() {
            synchronized (mutex) {return m.size();}
        }
        ...
    }
        
        

可以看到SynchronizedMap维护了一个Map 和一个 Object mutex表示锁 , SynchronizedMap中的方法基本上都是对mutex上锁;


常见面试题

HashMap时间复杂度为什么是O(1)

定位到数组的时间复杂度O(1)
链表(长度<8)时间复杂度O(n)
红黑树(长度>8)时间复杂度O(logn)

可以看出,影响hashmap性能的主要因素是 链表长度或红黑树节点数 ,理想情况下 数组中的链表或红黑树节点数 为1,此时时间复杂度也是O(1);
而且,正常情况下,hashmap链表、红黑树节点不会很多,因为当hasdmap中元素的数量 达到 数组(桶)大小 * 0.75(负载因子)时,hashmap就会扩容,hash冲突概率小,也不太可能出现一个桶中 有很多个元素;
因次,HashMap时间复杂度接近O(1)


HashMap扩容(大小)是2的幂次方

https://blog.youkuaiyun.com/miranaibuai/article/details/106674285

https://www.cnblogs.com/jinjian91/p/11917413.html

有2个好处:

  1. 按位运算,效率高(高16位 异或 运算);
  2. 为了数据的均匀分布,减少哈希碰撞

HashMap计算散列位置,如果不按位运算通常的做法是取模,即 先a=n/c,再 x=n-a*c (就结果而言,这里就是取余),这里包含了除法、乘法、减法、运算 效率低;

如果按位运算,就必须让hashmap的容量为2的倍数 才能使数据均匀分布
hashmap确定落在数组的位置的时候,计算方法是(n - 1) & hash ,奇数n-1为偶数,偶数2进制的结尾都是0,经过&运算末尾都是0,会增加hash冲突。

与运算符(&)
运算规则:

0&0=0;0&1=0;1&0=0;1&1=1

即:两个同时为1,结果为1,否则为0
(n - 1) & hash

例如 hashmap长度为15 --> 1111
hashcode   数组中位置
0   1110 & 0 = 0
1	1110 & 1	0
2	1110 & 10	2
3	1110 & 11	2
4	1110 & 100	4
5	1110 & 101	4
……	……	……
16	1110 & 10000	0
17	1110 & 10001	0
18	1110 & 10010	2

可以看到如果不是偶数位,按位与 的结果都是分布在偶数位;


hashmap如何解决hash冲突

hashmap用的是 链地址法;


HashMap中hash函数是如何实现

对key的hashCode做hash操作,和 高16位做异或运算

还有哪些hash函数的实现方式?  
还有平方取中法,除留余数法,伪随机数法

为什么链表转化为红黑树的门槛是8

https://blog.youkuaiyun.com/adorechen/article/details/107709726

查看java8 HashMap注释大意是:
在hashcode随机分布时,链表长度和红黑树的出现概率复合泊松分布。在链表长度为8时,红黑树出现概率为百万分子6


为什么要进行链表转红黑树的优化

如果单纯的使用链表,发生hash碰撞时,链表会不断增长,当对HashMap的相应位置进行查询的时候,时间复杂度位O(n),如果使用红黑树就是O(logn);


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值