集合框架库Map接口 -- HashMap详细介绍

1. HashMap简介

HashMap是Map接口下的集合。
HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap有两个参数影响其性能:初始容量和加载因子,初始容量是哈希表在创建时的容量,默认为16个大小。加载因子默认为0.75,当哈希表中的节点个数超过加载因子*当前节点个数时,需要进行2倍扩容操作。

HashMap的特点

Map接口下的集合,存储双值,以key-value的形式存储数据
key是不重复的,且key可以为null,元素的存储位置由key决定
通过key去寻找key-value存储的位置,得到key对应的value值,适合做查询

  1. 继承的父类
    extends AbstractMap
  2. 实现类
    implements Map, Cloneable, Serializable
    Map:以key - value的形式存储数据,并且key是不重复的,元素的存储位置由key决定。也就是可以通过key去寻找key - value的位置,从而得打value的值,适合做查找工作。
    Cloneable:集合可以使用clone方法
    Serializable:序列化,表示HashMap可以进行可序列化操作
  3. 内部类
    static class Entry implements Map.Entry
    可以存储key - value具体数值
    private abstract class HashIterator implements Iterator
    只能从前往后进行遍历
HashMap的数据存储特点
  1. 元素顺序:
    HashMap中元素,不是插入顺序,其存储顺序是由key值决定的。
    允许存储key为null,或者value为null的值。key值不允许重复,如果添加重复的key,则旧的value会被覆盖。
  2. 元素存储位置:
    真正存储元素的位置在Map.Entry里面,因此迭代器在遍历时,不能直接获得Iterator进行遍历,要先获得Entry结点。

2. HashMap的基本使用

  1. 定义
//没有指定数据类型,存储的类型就为任意的
HashMap hashMap1 = new HashMap();
HashMap hashMap = new HashMap<>();
  1. 添加
hashMap.put("tom",34);
//允许存储null值
hashMap.put(null,null);
//添加元素时,key已存在,会修改对应的value值
hashMap.put(null,5);
  1. 删除
//删除key为null的元素
//public V remove(Object key)
hashMap.remove(null);
  1. 修改
//public void replace(Object key,V value)
hashMap.replace("tom",88);
  1. 查询
//public V get(Object key)  获取key对应的value
System.out.println(hashMap.get("tom"));
//判断key是否存在
System.out.println(hashMap.containsKey("tom"));

3. HashMap的遍历

  • 迭代器遍历

通过entrySet,获得HashMap的“键值对”的Set集合

//entrySet迭代器遍历
Iterator> iterator = hashMap.entrySet().iterator();
while(iterator.hasNext()){
    Map.Entry next = iterator.next();
    System.out.println("key:" + next.getKey()+" "+"value:"+next.getValue());
}

通过keySet,获得HashMap的“键”的Set集合

// keySet迭代器遍历
Iterator iterator1 = hashMap.keySet().iterator();
while(iterator1.hasNext()){
    String key = iterator1.next();
    System.out.println("key:" + key);
}

通过values,获得HashMap的“值”的集合

// values迭代器遍历
Iterator iterator2 = hashMap.values().iterator();
while(iterator2.hasNext()){
    Integer value = iterator2.next();
    System.out.println("value" + value);
}
  • foreach遍历

foreach遍历entrySet()

for(Map.Entry next : hashMap.entrySet()){
    System.out.println("key:" + next.getKey()+" "+"value:"+next.getValue());
}

foreach遍历keySet()

for(String key: hashMap.keySet()){
    System.out.println("key:" + key);
}

foreach遍历values()

for(Integer value:hashMap.values()){
    System.out.println("value" + value);
}

4. HashMap源码分析

1. HashMap的节点类型

HashMap是实现Map接口下的集合,以key-value的形式存储数据。
实现了Map.Entry,具体的数据存储在Entry中。
节点的参数:key、value、next

static class Entry{
    K key;
    V value;
    Entry next;
    int hash;

    public Entry(K key, V value, Entry next,int hash) {
        this.key = key;
        this.value = value;
        this.next = next;
        this.hash = hash;  //存储扰动处理后的hashcode即h,没有&(table.length)
    }
}
2. 哈希求解对应下标

如果key为null,对应的数组下标为0。

a. 求出hashcode值:int h = key.hashCode(); //求出来的h有可能为负

b. 扰动处理:降低hashCode的重复率,即降低index的重复率
( >>>) 解决index为负数的问题

c. & (table.length) 或者 % table.length:
两者相等的前提是:table.length必须是2的幂,所有默认table每次都是2被增长
使用& (table.length) ,因为位运算的效率高于算数运算

//扰动处理后的hash
private int hash(K key){
    //如果key为空,其对应的下标为0
    if(key == null){
        return 0;
    }
    //index有可能为负数
    int h = key.hashCode();
    //扰动处理:降低hashCode的重复率,即降低index的重复率
    // >>> 解决index为负数的问题
    h ^= (h >>> 20) ^ (h >>> 12);
    h = h ^ (h >>> 7) ^ (h >>> 4);
    //位运算的效率高于算数运算,前提是table.length是2的幂,两者才相等
    return h;
}

//取余table长度后index值下标
private int indexOf(int h){
    return h & (table.length-1);
}
3. 扩容操作

为什么要扩容?
扩容不是因为没有数据存储空间,为了提高查询效率
因为当数据量越大,哈希冲突越高
扩容之后要重新计算每一个节点对应的index,哈希冲突概率降低。

如何判需要进行扩容?
加载因子:默认的加载因子 = 0.75
当前键值对的个数 >= 容量 * 加载因子

为什么要保证2倍扩容?
因为要保证table.length是2的幂,因为经过扰动处理的哈希值,需要进行与运算。

//扩容方法
private void resize(){
    int old_lenght = table.length;
    int new_lenght = old_lenght * 2;
    //定义一个数组存放以前的table
    Entry[] old_table = table;
    table = new Entry[new_lenght];
    //对元素进行重新哈希
    for(Entry e : old_table){
        while(e != null){
            Entry next = e.next;
            int h = hash(e.key);
            int index = indexOf(h);
            e.next = table[index];
            table[index] = e;
            e = next;
        }
    }
}
4. 添加操作

注意1 :
HashMap允许存储key为null,或者value为null的元素,因此需要注意处理key为null的情况,避免出现空点访问操作。
对于key为空进行特殊处理
注意2 :
比较链表判断key是否重复:若重复新value替换老的value,如果不重复,使用头插法将新的节点插入到链表中。

添加操作步骤:
a. 判断如果第一次添加元素,将数组扩容至16个大小;
b. 通过哈希计算key对应的数组下标
c. 通过index定位到该下标对应的链表,遍历链表,如果key存在则用新的value覆盖旧的value。
先判断hashcode,再判断引用地址是否相同,最后判断equals
先比较hashcode,如果hashcode不同,那么key一定不相同,如果hashcode相同这两个key有可能是相同的key,然后再去比对引用地址或者equals相同。
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key))))
d. 不存在key需要插入新节点,首先判断是否需要扩容;
e. 头插法插入新节点,size++。

//添加
    public void put(K key,V value){
        //第一次添加数据,进行扩容
        if(table == EMPTY_TABLE){
            table = new Entry[DEFAULT_CAPCITY];
        }

        //计算key对应的数组下标
        int h = hash(key);
        int index = indexOf(h);

        //找是否存在相等key,存在则替换value
        for(Entry e = table[index];e != null;e = e.next){
//            if(hash(e.key) == index && (e.key == key || key.equals(e.key))){
            if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
                e.value = value;
                modCount++;
                return;
            }
        }
        //判断是否需要扩容
        if(size >= table.length * threshold){
            resize();
        }
        //不存在key,头插插入新节点key
        table[index] = new Entry(key,value,table[index],h);
        modCount++;
        size++;
    }
5. 删除操作

删除步骤:
a. 计算key对应的数组下标,找到对应的链表
b. 遍历链表找出key相等的节点前驱 (key为空特殊判断)
c. 需要考虑删除的是头结点,删除其他节点,节点的next域指向下一个节点

//删除操作
    public void remove(K key){
        if(size <= 0){
            return;
        }
        //计算key对应的数组下标
        int h = hash(key);  
        int index = indexOf(h);
        //链表为null,不存在元素
        if(table[index] == null){
            return;
        }

//        遍历找到key相同的节点前驱
        Entry pre = table[index];
        Entry e = table[index];
        while(e != null){
            Entry next = e.next;
            if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
                if(e == pre){ //删除的是头结点
                    table[index] = next;
                }else{ //删除后面的节点
                    pre.next = next;
                }
                e.key = null;
                e.value = null;
                e.next = null;
                size--;
                modCount++;
                return;
            }
            pre = e;
            e = next;
        }
}
6.查询:get方法

通过key获得对应的value

//    找到对应的key,然后将value值返回
    public V get(K key){
        int h = hash(key);
        int index = indexOf(h);
        Entry e = table[index];
        while(e != null){
            if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
                return e.value;
            }
            e = e.next;
        }
        return null;
    }
7.修改:replace(key,newValue);

通过key修改对应的value值

//找到key对应的结点,替换成新的value
public void replace(K key,V value){
    int h = hash(key);
    int index = indexOf(h);
    if(table[index] == null){
        return;
    }
    Entry e = table[index];
    while(e != null){
        if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
            e.value = value;
            return;
        }
        e = e.next;
    }
}
8.迭代器的实现
//获取迭代器对象
public Iterator> iterator(){
    return new HashIterator();
}

//迭代器实现
private class HashIterator implements Iterator>{

    private Entry next;//当前节点
    private int expectedModCount; //快速失败
    private int index; //记录当前数组的下标

    public HashIterator() {
        this.expectedModCount = modCount;
        //找第一个节点
        if(size > 0){
            Entry[] t = table;
            while(index < t.length && (next = t[index++]) == null){
                ;
            }
        }
    }

    @Override
    public boolean hasNext() {
        return next != null;
    }

    @Override
    public Entry next() {
        checkForComodification();
        Entry e = next;
        next = e.next;
        //找下一个节点
        if(next == null){
            Entry[] t = table;
            while(index < t.length && (next = table[index++]) == null){
                ;
            }
        }
        return e;
    }

    @Override
    public void remove() {
        checkForComodification();
        MyHashMap.this.remove(next.key);
    }

    final void checkForComodification(){
        if(expectedModCount != modCount){
            throw new ConcurrentModificationException();
        }
    }
}

//测试迭代器
Iterator> hashIterator = hashMap.iterator();
        while(hashIterator.hasNext()){
//            hashIterator.remove();
            Entry p = hashIterator.next();
            System.out.println("key:" + p.getKey() + " "+ "value:" + p.getValue());
        }

5. HashMap的使用场景

HashMap是Map接口下的集合,可以存放key - value键值对
当我们存储的元素需要有唯一标识,且对应一定的元素,可以使用HashMap,因为key为唯一的,且对应的value可以是一个元素,也可以是个对象。

6. HashMap的不足

  1. 浪费空间
    HashMap的初始容量为16,加载因子为0.75,当元素存储到12,需要进行扩容。
    哈希冲突最大时:所以元素占用一个下标
    哈希冲突最低时:每一个元素都占一个下标
    数组容量为16时,数组的利用率为1 - 12
    所以HashMap采用空间换时间的方式,因为空间利用率越高,存储元素越多,哈希冲突就高,查询效率变低。
    HashMap的优势:
    查询效率时间复杂度近似于O(1)
    HashMap将查询效率放第一位,空间换时间
    HashMap可以自定义初始容量和加载因子,因此可以根据具体的使用场景,定义初始容量和加载因子

  2. 依赖哈希算法
    HashMap比较依赖哈希算法。
    哈希算法设计的好,查询效率高,
    哈希算法设计的不好,查询效率低。

  3. HashMap是无序的,插入节点是没有顺序的
    使用LinkedHashMap可以得到插入有序
    LinkedHashMap实现插入有序的原理:
    (1)LinkedHashMap在HashMap的基础上维护了一个双向链表
    LinkedHashMap = HashMap + 双向链表
    (2)LinkedHashMap的节点构成
    Entry before, after,int hash, K key, V value, Entry next
    相对于HashMap的节点多了after和before,Entry before, after;
    before:指向该节点之前插入的节点;
    after:指向该节点之后插入的节点
    继承了HashMap,添加等使用的都是HashMap,重写部分方法,维护before,after。
    (3)定义头指针和尾指针,维护双链表,采用头插和尾插添加元素。

  4. 插入无序,无法按照key值得大小进行排序
    使用TreeMap可以维护key - value结构的大小顺序。
    TreeMap:key-value的存储结构是根据key的大小比较排序得来的,而是不是通过key计算对应的哈希值。插入时,通过比较key插入到红黑树中,小的走左子树,大的走右子树。插入和删除都使用红黑树的规则。
    底层结构:红黑树,将key-value作为一个节点存储在红黑树的节点中。
    TreeMap需要使用比较器进行比较,给类提供比较原则。

你可能感兴趣的:(JavaSE)