java基础部分-第四周学习总结-HashMap

1.HashMap集合简介

HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。

补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。

这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考 treeifyBin方法。

当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。

小结:

特点:

1.存取无序的

2.键和值位置都可以是null,但是键位置只能是一个null

3.键位置是唯一的,底层的数据结构控制键的

4.jdk1.8前数据结构是:链表 + 数组 jdk1.8之后是 : 链表 + 数组 + 红黑树

5.阈值(边界值) > 8 并且数组长度大于64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

2.HashMap集合底层的数据结构

2.1数据结构概念

数据结构:就是存储数据的一种方式。ArrayList LinkedList

在JDK1.8 之前 HashMap 由 数组+链表 数据结构组成的。

在JDK1.8 之后 HashMap 由 数组+链表 +红黑树数据结构组成的。

2.2HashMap底层的数据结构存储数据的过程

存储过程如下所示:

使用的代码:

public class Demo01 {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("刘德华", 53);
        map.put("柳岩", 35);
        map.put("张学友", 55);
        map.put("郭富城", 52);
        map.put("黎明", 51);
        map.put("林青霞", 55);
        map.put("刘德华", 50);
    }
}

说明:

1.面试题:HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

对于key的hashCode做hash操作,无符号右移16位然后做异或运算。
还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高的。至于底层是如何计算的我们下面看源码时给大家讲解。

 2.面试题:当两个对象的hashCode相等时会怎么样?

会产生哈希碰撞,若key值内容相同则替换旧的value.不然连接到链表后面,链表长度超过阈值8就转换为红黑树存储。

 3.面试题:何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?

只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。

4.面试题:如果两个键的hashcode相同,如何存储键值对?

hashcode相同,通过equals比较内容是否相同。
相同:则新的value覆盖之前的value
不相同:则将新的键值对添加到哈希表中

5.在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

6.通过上述描述,当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只是为了查找效率更高。

  JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

7.总结

说明:

1.size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。

2.threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 )。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 。

3.HashMap代码实现(数组加链表)

import java.util.Arrays;

/**
 *  jdk1.8以前HashMap由数组+链表
 *  数组是主体,链表是主要为了解决哈希冲突(调用hashCode方法计算出的hash值一样,导致计算出的数组索引相同)
 *
 *  jdk1.8以后使用数组+链表+红黑树(
 *  当链表长度大于阈值或者红黑树的边界值,默认为 8
 *  并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储)
 *
 *
 */
public class MyHashMap<K,V> {

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认数组长度16

    static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认加载因子0.75,

    static final int MAXIMUM_CAPACITY = 1 << 30;//2^30

    private int size;//表示 HashMap中K-V的实时数量 ,不等于数组的长度

    //threshold( 临界值)  =capacity(容量) * loadFactor( 加载因子 )
    //当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍
    private int threshold;

    private float loadFactor;//加载因子,表示HashMap的疏密程度

    private Node<K,V>[] table;//存储元素的数组

    public MyHashMap() {
        this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
    }

    public MyHashMap(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);
        }
        table = (Node<K,V>[])new Node[initialCapacity];//初始化数组大小
        this.loadFactor = loadFactor;
        this.threshold = (int) (loadFactor*initialCapacity);//初始化阀值
    }

    public MyHashMap(int initialCapacity){
        this(initialCapacity,DEFAULT_LOAD_FACTOR);
    }

    /**
     * put方法
     *实现步骤
     * 1.先通过hash值计算出key映射到哪个桶
     * 2.如果桶上没有碰撞冲突,则直接插入
     * 3.如果出现碰撞冲突了,则需要处理冲突:
     *     如果桶中存在重复的键,则为该键替换新值value
     *     否则使用头插法,接到链表头部
     * 4.如果size大于阈值threshold,则进行扩容;
     * @param key
     * @param value
     * @return
     */
    public V put(K key,V value){
        return putVal(hash(key), key, value);
    }

    /**
     *
     * @param hash
     * @param key
     * @param value
     * @return
     */
    private V putVal(int hash, K key, V value) {
        Node<K,V>[] tab = table;
        int index = hash & (tab.length-1); //取余,计算索引
        Node<K,V> p = tab[index];//索引节点

        if (++size > threshold) {
            tab = Arrays.copyOf(tab,table.length*2);
        }

        if (p == null) {
            //创建一个新的节点存入到数组中
            tab[index] = new Node<>(key,value,null);
        }else {
            //此位置有节点
            Node<K, V> temp = p;
            while (temp!=null){
                //两个元素哈希值相等,并且key的值也相等
                if (hash(temp.key) == hash && ((temp.getKey() == key) || key.equals(temp.getKey()))) {
                        p.setValue(value);
                        size--;
                        return value;
                }
                temp = temp.next;
            }
            Node<K, V> newNode = new Node<>(key, value, p);//头插
            tab[index] = newNode;
        }
        return value;
    }

    /**
     * 先找到元素的位置,遍历链表找到元素之后删除
     * @param key
     * @return
     */
    public V remove(K key){
        Node<K,V> e = removeNode(hash(key),key);
        return e == null ? null : e.value;
    }

    private Node<K,V> removeNode(int hash, K key) {
        Node<K,V>[] tab = table;
        int index = hash & (tab.length-1); //取余,计算索引
        Node<K,V> p = tab[index];
        //如果当前key映射到的数组不为空
        if (p != null && tab.length > 0 && tab != null) {
            Node<K,V> prev  = null;//目标节点的前一个节点,即前驱
            while (p != null){
                    if (hash(p.key) == hash && ((p.getKey() == key) || key.equals(p.getKey()))) {
                        if (prev == null) {//链表只有一个元素
                            tab[index] = p.next;
                        }else {
                            prev.next = p.next;
                        }
                        size--;
                        return p;
                    }
                    prev = p;
                    p = p.next;
                }
            }
        return null;
        }

    public V get(K key) {
        Node<K,V> e = getNode(hash(key), key);
        return e == null ? null : e.value;
    }

    private Node<K,V> getNode(int hash, K key) {
        Node<K,V>[] tab = table;
        int index = hash & (tab.length-1); //取余,计算索引
        Node<K,V> p = tab[index];

        if (p != null && tab.length > 0 && tab != null) {
            while (p != null){
                if (hash(p.key) == hash && ((p.getKey() == key) || key.equals(p.getKey()))) {
                    Node<K, V> node = p;

                    return node;
                }
                p = p.next;
            }
        }
        return null;
    }


    private int hash(K key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//异或运算,减少哈希冲突
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private class Node<K,V>{
        private K key;
        private V value;
        private Node<K,V> next;

        public K getKey() {
            return this.key;
        }

        public V getValue() {
            return this.value;
        }

        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public Node(K key, V value, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
}

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值