java基础:HashMap

HashMap介绍

HashMap是基于hash表实现的,HashMap的每个元素都包含了key-value,其内部是通过数组+链表+红黑树来实现的,当容量不足的时候会根据2的整数幂来实现自动扩容。
需要注意的是HashMap是非线程安全的,只使用与单线程环境下,多线程环境线可以采用ConcurrentHashMap

Hash冲突

  • 当哈希函数对两个不同的数据项产生了相同的hash值时,这就称为哈希冲突。
  • 实现HashMap的一个重要考量,就是尽可能地避免哈希冲突。HashMap在JDK 1.8中的做法是,用链表和红黑树存储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。

解决办法

  • 开放定址法
  • 链地址法
  • 再哈希法
  • 公共溢出区域法

数组+链表的优势

  • 数组特点:查询比较块,插入删除比较慢
  • 链表优点:插入删除比较块,查询比较慢
  • hashMap引入了这两种数据结构,在一定成都上利用到了他们相互的优点,首先桶的个数是确定的(16,不涉及到插入删除,定位快,时间复杂度O(1)),链表(设计到数据的频繁插入删除)
  • 链表还可以用来处理hash冲突问题,需要注意的是hash值并不是hashCode,而是将hashcode高低十六位异或过的

自动扩容

扩容条件

  • 如果bucket满了(load factor*current capacity)就会扩容。
  • 之所以负载因子不是1,而是0.75是为了最大限度的,原因是为了最大限度的避免hash冲突

扩容方式

hashmap为了存取高效率,要尽量减少hash碰撞,所以她要尽可能的分配均匀,最好每条链表上面长度相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length。但是这种运算速度不如位运算速度快,源码中做了优化hash&(length-1)。

  • 那为什么是2的n次方呢?
    因为2的n次方实际就是1后面n个0,2的n次方-1,实际就是n个1。
    例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。
    而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了。
    所以,保证容积是2的n次方,是为了保证在做(length-1)的时候,每一位都能&1 ,也就是和1111……1111111进行与运算。
  • 源码中采用的是先高16位异或低16位再取模运算,这种做法是为了降低hash冲突的几率。

HashMap中的key

  • 是否可以为null?
    当然可以,key为null时,hash值为0,也就是放在数组的第一个位置。但是只能允许一个key为null
  • key的设计需要注意那些事项?
    1、首先HashMap的key-value支持泛型,而泛型要求必须是对象
    2、其次需要保证,对象可以存进去
    3、再次需要保证,对象可以取出来
    如何才能保证能存进去且能取出来呢?这就要求对象一旦存进去后,就不能发生任何改变。因此要求的是一个不可变对象。咱们经常看到HashMap中key用基础数据类型或者String做key,其实基本数据类型在hashmap中会进行自动装箱操作。
  • 如何设计一个不可变类
    1、类添加final修饰符,保证类不被继承。
    如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
    2、保证所有成员变量必须私有,并且加上final修饰
    通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。
    3、不提供改变成员变量的方法,包括setter
    避免通过其他接口改变成员变量的值,破坏不可变特性。
    4、通过构造器初始化所有成员,进行深拷贝(deep copy)
    如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。
  • 重写hashCode和equals需要注意
    1、两个对象相等,hashcode一定相等
    2、两个对象不等,hashcode不一定不等
    3、hashcode相等,两个对象不一定相等
    4、hashcode不等,两个对象一定不等

HashMap中的方法

hashmap中的静态变量
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量(扩容超过最大容量后,会默认设置为最大容量)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子(容量超过MAXIMUM_CAPACITY*DEFAULT_LOAD_FACTOR,hashmap就会进行扩容)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
static final int TREEIFY_THRESHOLD = 8;
// 当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 1、当哈希表中的容量大于这个值时,表中的桶才能进行树形化
* 2、否则桶内元素太多时会扩容,而不是树形化
* 3、为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
*/
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap()
// 所有其他字段默认
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
HashMap(int initialCapacity, float loadFactor)
  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);
        // hashMap初始化
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
//需要注意的是hashmap的容量是2的整数幂,所以咱们输入的初始化容量并不是hashmap的真实容量,hashmap会取>initialCapacity的最小的2的整数幂
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
put(K key, V value)
   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {          
        Node[] tab; Node p; int n, i;
        // 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 计算index,并对null做处理
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            // 节点key存在,直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 判断该链为红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode)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);
                        // 链表长度大于8转换为红黑树进行处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // key已经存在直接覆盖value
                    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;
        // 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
get(Object key)
public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node getNode(int hash, Object key) {
        Node[] tab; Node 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) {
                // 判断是否是二叉树,如果是则getTreeNode获取节点值
                if (first instanceof TreeNode)
                    return ((TreeNode)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;
    }
treeifyBin(Node[] tab, int hash) (树形化方法)
/**
* (1)根据哈希表中元素个数确定是扩容还是树形化
* (2)如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
* (3)然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
*/
// 将桶内所有链表节点替换成红内书节点,使用内部类Node存储节点数据
final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        // 如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 如果哈希表中的元素个数超过了 树形化阈值,进行树形化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode hd = null, tl = null;
            // 从第一个节点开始,每个节点数据都进行树形化
            do {
                TreeNode p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
           //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

参考博客:https://www.jianshu.com/p/33b25964798c

你可能感兴趣的:(java基础:HashMap)