HashMap常考点!

面试官:了解集合吗?
你:了解一点点
面试官:那你讲一下hashmap吧
你:hashmap就是数组+链表的一种结构,使用键值对,键不能重复,值可以重复
面试官:还有呢?
你:。。。
面试官:讲一下resize的过程?
你:。。。
面试官:讲一下hashmap是不是线程安全的?
你:hashmap是线程不安全的
面试官:为什么是线程不安全的?
你:。。。
面试官:好的,这次面试就到这里吧,我们下次联系(内心os:这小逼崽子什么都不会,联系你才怪了)

1、什么是HashMap?

我们知道,Java集合中最重要的三种集合:
Set 不允许重复
List 允许重复,有序
Map 键值对
我们不仅要了解三种的特性,还要了解他们具体的一些实现类
Set
	HashSet
	TreeSet
List
	ArrayList
	LinkedList
Map
	HashMap
	ConcurrentHashMap
	HashTable
	SynchronizedMap

那么什么是HashMap呢?

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    //……
	    static final int hash(Object key) {
	        int h;
	        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	    }
    }
从类定义中,我们可以看到,首先它是继承自AbstractMap,实现了Map,Cloneable,Serializable
所以,他是一种键值对的数据结构,继承自AbstractMap,实现了Map接口

2、讲一下HashMap 的底层原理

因为HashMap有两个版本,分为jdk1.7以前的和jdk1.8以后的。我们这里分开来讲
------------------------jdk1.7-------------------------
jdk1.7以前,HashMap底层使用的是数据+链表的数据结构,并且使用的是头插法
在初始化的时候呢,默认的初始化容量是16,默认加载因子是0.75

什么是加载因子?
加载因子就是当容量使用情况到达:16(当前容量)*0.75(加载因子)的时候,就会
进行一次resize()操作,也就是扩容

什么是resize()扩容?具体是怎么操作的?
	举个栗子:当前我的容量为16,默认的加载因子为0.75,当我HashMap里的容量
使用为12的时候,我还想往HashMap里面去添加数据,此时HashMap就会触发扩容。
	resize()的过程:
		1、首先是创建一个新的数组,长度为16(当前容量)*2=32
		2、然后使用头插法,将HashMap里面的元素进行rehash计算,再把元素映射
		  到新的数组中去

为什么默认的初始化容量是16?
	因为16的二进制为1111,而进行hash计算的时候,我们是获得key的hashCode()
再和数组长度进行与操作,来进行hash运算的。所以当为2次幂的时候,能够实现最大均
匀,至于为什么是16不是8或者32的话,可能是为了均匀分布吧
index = HashCode(Key) & (Length- 1)

------------------------jdk1.8-------------------------
jdk1.8基本上和jdk1.7一样,做的改变就是:
1、底层数据结构
	jdk1.7采用的是数组+链表
	jdk1.8采用的是数组+链表+红黑树
2、resize()
	jdk1.7,采用头插法
	jdk1.8,采用尾插法

什么时候从链表转为红黑树?
	因为红黑树的平均查找长度是log(n),链表的平均查找长度是n/2,长度为8的时候,
平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。
链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时
间并不会太短。

3、HashMap里面有什么参数?

1、存放数据的结构数组
2、结构体Node
3、默认初始化长度default——capacity
4、最大的长度2^30
5、默认加载因子0.75
6、jdk1.8之后,加入了链表与红黑树的转化,
   于是又一个转换为红黑树的阈值8和转换为链表的阈值6
7、modcount

4、HashMap怎么处理hash冲突的?

	上面讲了Hash Map是一个数组加链表来实现的,(jdk1.8之后加入了红黑树优化),
我们首先会对key的值进行hash运算,得到一个hash值存储到数组中,当然了,可能又很
多的key值映射到同一个位置上,所以,所有映射到同一位置上的值,会以链表的映射存储
,jdk1.7使用的是头插法,jdk1.8之后使用的为尾插法

5、HashMap存取原理?

put

	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

get

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

6、HashMap为什么是线程不安全的?

还记得我们之前讲的,jdk1.7以前,使用的是头插法;jdk1.8以后使用的是尾插法
那么线程不安全就发生在这个resize()的过程

假设我们现在有一个HashMap,当前容量为10,当HashMap里面已经有7个元素的时候,
我想在插入一个,肯定就触发扩容机制resize(),扩容了

假设HashMap里面有一个有这么两个元素("lignque",1)和("queling",1),
假设我们恰巧进行hash映射的时候,映射到同一个位置,
使用链表存储的指向为("lignque",1)->("queling",2)

此时有两个线程去操作这个HashMap,线程1和线程2
(此时里面已经有7个元素了,再插入一个就会发生扩容resize())
都想往里面插入一个元素
--------------------------------------------------
             线程1        |          线程2
--------------------------------------------------
 插入一个元素,发现需要扩容   |         
--------------------------------------------------
                          | 插入一个元素,发现需要扩容
--------------------------------------------------
 把数组扩容到原来的两倍(20) | 
--------------------------------------------------
将原来的元素重新计算hash值    | 
映射到新的数组里面,变成了     | 
("queling",2)->("lingque",1)| 
--------------------------------------------------
                          | 把数组扩容到原来的两倍(20)
--------------------------------------------------
                          | rehash,复制过去,首先定位到
                          | ("lignque",1),他的下一个为
                          | ("queling",2),但是线程1已经进行了
                          | resize()操作,此时("queling",2)->("lingque",1)
                          | 此时两个元素就相互为next,形成死循环
--------------------------------------------------

7、有什么安全的替代?

  • HashTable
  • ConCurrentHashMap
  • SynchronizedMap

敖丙:我把ConcurrentHashMap & HashTable的知识点都整理了一下,讲的很详细,推荐一波

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值