HashMap

哈希表
又被称为散列表,是根据关键码 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是线程安全的吗?

不是线程安全。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值