HashMap知识总结

1、hashMap继承及实现接口。

Map<K,V>,Cloneable , Serializable,Map.Entry,Iterator

2、特点分析(根据其继承类及实现的接口)
(1)Map<K,V>

  • collection(存储单值)和Map(存储 双值)是集合框架库的两个顶级接口。
  • 以key->value的形式存储数据,key是不重复的,元素位置由key决定。可以通过key去寻找key-value的位置,从而得到value的值,适合做查找工作。

(2)Cloneable

  • 可以使用clone方法

(3)Serializable

  • 可以被序列化

(4)Map.Entry

  • 可以存储key-value具体数值。

(5)Iterator

  • 迭代器,只能从前往后进行遍历。

3、插入顺序:hashmap中元素的顺序,不是插入顺序。因为key-value的存储位置是由key决定的。
4、使用场景:由于hashmap查询效率较高,并且可以通过key去查询对应的value。
5、源码分析
(1)底层数据结构

  • 数组+链表(V1.7)
  • 数组+链表或者数组+红黑树(V1.8)

(2)构造函数: 数组默认初始容量为0,当添加第一个元素时,容量变为16。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final Entry<?,?>[] EMPTY_TABLE = {};
MyHashMap(){
	table = EMPTY_TABLE;
	threshold = 0.75;
}

(3)hash算法:通过key值,调用hashcode()函数计算key的hashcode。
注意的点:

  • 一进入函数需要对key是否为空进行判断,若为空,置其hashcode为0。
  • 对通过hashcode()计算出的hash值进行扰动处理,目的是保证其为正数和减低hashcode的重复率,降低hash冲突。
	public int hash(K key){
		if(key==null){
			return 0;
		}
		//key.hashCode()是一个负数
		int h = key.hashCode();
		//扰动处理:hashcode会重复的,目的是降低hashcode重复率
		h ^=(h>>>20) ^ (h>>>12);// >>>无符号的右移
		h = h^(h>>>7)^(h>>>4);
		return h;
		//h % table.length 原来的,因为位运算符的运算效率高于算数运算符
		//前提条件是table.length是2的幂
	}

(4)indexOf()方法:计算下标。
参数:h(对key进行计算后的hashcode)
按位与操作:提高计算效率。

	public int indexOf(int h){
		return h & (table.length - 1);
	}

(5)resize()扩容方法:对hashMap进行扩容。

  • 初始化容量为16。
  • 扩容条件:当前元素个数 >= 容量*加载因子,此时需要扩容。
  • 扩容方式:2倍扩容,保证table.length是2的幂,以保证indexOf方法中按位与操作的准确性。
  • 扩容后由于数组长度发生改变,需要重新计算index值并且更新节点(采用单链表的头插法)。
	/*
	 * 扩容函数:resize()
	 * 2倍扩容始终能够保证table.length是2的幂
	 * 扩容后需要重新计算hash值,重新更新节点。
	 */
	public void resize(){
		int old_length = table.length;
		int new_length = 2 * old_length;
		Entry<K,V>[] old_table = table;
		table = new Entry[new_length];
		//将老数组的元素放到新数组中,需要重新计算每一个元素的下标
		for(Entry<K,V> e:old_table){
			while(e != null){
				Entry<K,V> next = e.next;
				int index = indexOf(hash(e.key));
				//头插法
				e.next = table[index];
				table[index] = e;//将头结点更新到e
				e=next;//e继续往下走
			}
		}
		
	}	

(6)put()方法

  • 函数名:put(K key,V value)
  • 函数作用:给HashMap中添加元素
  • 参数:key,value
  • 返回值类型:void

需要注意的点
对key为空的特殊处理,体现在两个地方。

  • hash()算法。(上文已说明)
  • if 语句判断key是否相等时,首先判断hash值是否相同,如果hash值相同,不能说明他俩就是同一个key,若不同,则一定不是同一个key。其次判断引用是否相同,引用相同说明他俩相同;引用不同的话判断equals里面的属性值是否相同。
if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {...}

以下为put()方法实现及思路。

/*
      * 添加函数:(思路如下)
      * 1、如果是第一次添加,将数组容量设置为默认容量
      * 2、根据当前数组大小,计算哈希后的index值
      * 3、如果要添加的key值与已有key值有重复,则替换value值
      * 4、如果不重复,先判断是否需要扩容。(size>table.length*0.75时需要扩容)
      * 5、头插法插入链表。
      */
	public void put(K key,V value){	
		if (table == EMPTY_TABLE) {  // 是否时第一次添加元素
            table = new Entry[DEFAULT_CAPCITY];
        }
        int h = hash(key);
        int index = indexOf(h);//计算key值对应的下标
        //e.key.equals(key)  比较速率较慢
        for (Entry<K, V> e = table[index]; e != null; e = e.next) {//遍历下标下的链表
            if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {//如果key值相同,则值替换
                e.value = value;
                return;  //如果key有重复  值替换掉之后方法就退出
            }
        }
        //代码走到着说明key 没有重复  头插法
        if (size >= table.length * threshold) {
            resize();  //如何扩容:2倍扩容  始终能够保证table.length是2的幂
            index = indexOf(hash(key));
        }
        //添加数据,之前出错时因为没有传入h的值,导致每个插入的节点没有hash值。
        table[index] = new Entry<K,V>(key, value, table[index],h);//不相同则头插
        size++;
	}

(6)remove()删除方法

  • 函数名:remove(K key)
  • 函数作用:通过指定key删除元素
  • 参数:key
  • 返回值类型:布尔类型

思路:

  • 计算key的hash值和对应的index下标。
  • 循环找出与传入key相同的节点。
  • 找到后根据所删节点是否为头结点分情况处理。
  • 移动pre(指向当前节点前驱)和e(指向当前节点)两个指针的位置。(pre和e初始位置都指向table[index])。
public boolean remove(K key){
		if (size <= 0) {
            return false;
        }
        int h = hash(key);
        int index = indexOf(h);
        Entry<K, V> pre = table[index];
        Entry<K, V> e = table[index];
        while (e != null) {
            Entry<K, V> next = e.next;
            //第一个循环进来如果判断相等就成立 删除的的就是头节点  pre == e
            if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {
                size--;
                //如果删除的节点是头节点
                if (pre == e) {  //判断删除的节点就是头节点
                    table[index] = next;
                } else {
                    pre.next = next;
                }
                e.next = null;
                e.key = null;
                e.value = null;  //只有指向空之后才能进行垃圾回收
                return true;
            }
            pre = e;
            e = next;   //pre 变成了e的前驱
        }
        return false;

	}

6、解决哈希冲突的方法:链地址法。
7、常见问题
(1)为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
答:保证index一定时小于数组长度。防止数组下标访问越界。
(2)为什么HashMap需要扩容?
答:不是因为没有足够存储空间。扩容之后要重新计算每一个节点对应的index,哈希冲突概率降低
(3)如果自己指定HashMap的初始容量和加载因子,那么容量的大小,和加载因子的大小对HashMap有什么影响?

  • 初始容量:如果我们指定不是2的幂,源码中有函数能够保证将最终数组容量改变到2的幂。向上取整找最近的2的幂。
  • 初始容量越小,越容易发生哈希冲突。
  • 加载因子 :越大 。对扩容时机有影响。
  • 加载因子越大扩容的时机越晚,哈希冲突的几率越大,空间利用率越大。
  • 加载因子越小,越大扩容的时机越早,哈希冲突的几率越小,空间利用率越小。

(4)为什么用按位与代替取余?
答:因为位运算的计算效率高于算数计算。
(5)为什么hashmap的容量要保持2的幂?
答:为了用 & (length-1) 代替 % length 。
(6)HashMap中如果添加重复key会怎样?
答:新添加进来的key对应的newvalue代替之前老的value。
8、应用场景
(1)hashMaori查询效率较高,可以通过key值去快速查找value值。
(2)计数。
9、HashTable与HashMap异同
相同点:

  • 两者都会根据key重新计算所有元素的存储位置,算法相似,时间空间效率相同。

不同点:

  • hashtable默认容量为11,hashmap为16。
  • hashtable是在构造函数中初始化并且new对象;hashmap是在put方法中第一次添加数据时。
  • hashtable中计算hash方法,hash(),hashSeed^k.hashCode(),没有扰动处理,index重复率会高。
  • 计算index下标的方法:(hash & 0x7FFFFFFF)%table.length;保证hash值不为负数,保证index下标不会大于table.length。
  • 扩容方式:hashTable是2倍加1,保证结果都是奇数,因为取余之后的结果相比于偶数更分散。tableMap是2倍扩容。
  • 数据遍历方式:hashTable可以使用Enumerate和Iterator进行遍历;使用iterator进行遍历时支持快速失败机制。
  • 是否是线程安全:hashTable是线程安全的。但并不是绝对的线程安全。由于hashTable效率低已被淘汰,索引多线程环境下会选择ConcurrentHashMap。
  • 是否接受值为null的Key 或Value:hashtable中值和value都不能为空。hashmap中key和value都可以为空。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值