02Java的Map&HashMap底层原理(上)-存疑

Map接口定义了存储 “ 键(key)— 值(value) 映射对 ” 的方法,通过一个对象找到另一个对象。

下面看一个比较简单的程序:

package myhashmap;

import java.util.HashMap;
import java.util.Map;

/**
 * 测试HashMap的使用
 *
 * @author 发达的范
 * @date 2020/11/11 22:29
 */
public class TestHashMap {
    
    
    public static void main(String[] args) {
    
    
        Map<Integer, String> map = new HashMap<>();
        Map<Integer, String> map1 = new HashMap<>();
        map.put(1, "one");//向HashMap中添加put元素(键值对)
        map.put(2, "two");
        map.put(3, "three");
        System.out.println(map);//打印HashMap中所有的元素
        System.out.println(map.size());//打印HashMap的长度
        System.out.println(map.get(2));//获取键为2的值
        System.out.println(map.isEmpty());//判断HashMap是否为空
        System.out.println(map.containsKey(2));//判断HashMap中是否包含键2(索引为2)
        System.out.println(map.containsValue("one"));//判断HashMap中是否包含one值
        map.remove(2);//移除键2
        System.out.println(map);
        map1.put(2, "43523");//键不能重复,是否重复是根据equals方法来判断,如果重复,新的覆盖旧的
        map1.put(4, "发达的范");
        map.putAll(map1);//把map1中的所有元素添加到map中
        System.out.println(map);
    }
}

运行结果:在这里插入图片描述

Perfect!多么直观的显示方式。

看到,Map是一个接口,并且加了泛型,源码如下:

public interface Map<K,V> {
    
    }

其中,泛型<K,V>可以是任意数据类型,极大扩充了索引的范围。

HashMap是Map接口的实现类:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    }

下面使用一个自己的实现类作为对象:

package myhashmap;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 发达的范
 * @date 2020/11/12 21:21
 */
public class TestHashMap02 {
    
    
    public static void main(String[] args) {
    
    
        Employee employee1 = new Employee(122001, "发达的范", 1000);
        Employee employee2 = new Employee(122002, "奥特曼", 2000);
        Employee employee3 = new Employee(122003, "小怪兽", 500);

        Map<Integer, Employee> map = new HashMap<>();//把Integer作为“键”,把类Employee作为“值”,建立HashMap
        map.put(1, employee1);
        map.put(2, employee2);
        map.put(3, employee3);
        System.out.println(map.get(1).getName());
        
        Employee employee = map.get(3);
        System.out.println(employee.getName());
        System.out.println(map);
    }
}

class Employee {
    
    
    private int id;
    private String name;
    private int age;

    public Employee(int id, String name, int age) {
    
    
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getName() {
    
    
        return name;
    }

    @Override
    public String toString() {
    
    //重写toString()方法
        return id + "  " + name + "  " + age;
    }
}

运行结果:在这里插入图片描述

  • 如果“键”重复,就会把之前的覆盖掉。

这里关于HashMap的使用比较容易理解,下面着重说一下这里被@Override的toString()方法:

我们知道所有的类都是默认继承Object类,我们看一下Object类有哪些方法:

在这里插入图片描述

也就是说我们经常使用的System.out.println()方法是调用的Object类的toString()方法,默认按照源码定义的方式进行输出,下面看toString()方法的源码:

public String toString() {
    
    
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

这也就是直接对一个对象(没有指定它的属性)进行输出出现这样的现象的原因所在了,比如System.out.println(map),如图:

在这里插入图片描述

上一篇博客里面的疑问也就解开了,同样是因为重写了主类的祖宗类的toString()方法,所以才能按照我们想要的方式输出。


HashMap的底层实现

HashMap的底层实现采用的哈希表,一种很重要的数据结构!

哈希表的基本结构就是 “数组+链表”

数据结构中由数组和链表来实现对数据的存储:

  • 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。

  • 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。

为了深入理解HashMap,先看源码:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    }

可以看到,HashMap添加了两个泛型<K,V>,也就是一个“键值对”,下面再看:

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;

这是HashMap的核心结构,一个Node类型的数组,进入Node类

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    
    
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ......//此处省略若干行代码
}

可以看到,一个Node对象中包含了“键”对象的hash值,“键”对象K,“值”对象value,以及下一个节点next。


下面看HashMap是如何存储数据的。
在这里插入图片描述

如上图,存储过程是:

  1. 首先调用key对象的hashCode()方法获取“键”对象的哈希码(hashcode),哈希码是int型数据;

  2. 然后调用HashMap类的hash方法获取对应哈希码的hash值;

    hash值的取值范围是[0,数组长度-1].

  3. 把hash值当做索引,将键值对对象存入数组中,下次有相同的hash值,就存储在上一节点的后面形成单链表。

需要说明的是:转化后的hash值应该尽量分布均匀,提高效率,减少哈希冲突,所以就有了多种把hashcode转换成hash码的算法。下面我看一下JDK8是如何计算hash码的,源码如下:

static final int hash(Object key) {
    
    
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里用到了两个位操作符:

>>> : 无符号右移,忽略符号位,空位都以0补齐

^ :异或,相同为0,不同为1

多个Node对象通过单链表结构连接起来,如下图:

在这里插入图片描述

然后结合数组得到HashMap的存储结构示意图:
在这里插入图片描述

可以看到,使用数组和单向链表共同存储数据,这样做的好处是大大提高了查询和增删效率。

需要注意的是:

  • 存储链表头的数组不是固定大小,可以扩充;
  • 每个链表的头结点都是存储在数组中,查找时先查找头结点,然后通过头结点找后面的节点;

注意:

  1. 我觉得这样存储数据会有一个问题,已知不同哈希码对应的hash值可能相同,相同的hash值的节点使用单链表结构连接起来,那么如果一组数据中绝大多数的哈希码经计算后都对应同一个hash值,这些数据节点就会存储于数组中这个索引位置,最终导致其他数组索引位置没有或者只有很少的数据,这个问题如何解决?
  2. 如果数据量很大,数组长度很长,查询效率会不会降低?如何解决?
  3. 乍一看似乎这种方式(HashMap)并没有提高多少效率,但是结合数组和链表的优点稍作思考就会发现,假设一个HashMap中近似均匀存了一万条数据,数组长度是16,根据HashMap的特点可知,相同hash值的数据是存储在一个数组空间上的同一条链表上的,如果此时我需要查找其中的某一个数据Q,首先计算Q的哈希码(hashcode),然后计算他的hash值,根据hash值找到它所在的数组索引位置,然后从链表头结点开始搜寻,这样做的好处是,可以快速找到它所在链表的头结点,而不用搜寻剩下的所有的节点!

总结HashMap存储数据的过程:

在添加一个元素(key-value)时,首先使用hashcode()方法计算key的哈希码,然后使用hash()方法计算它的的hash值,以此确定插入数组中的位置(hash 值就是数组索引),可能存在同一hash值的元素已经被放在数组同一索引位置,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的hash值是相同的,所以说数组存放的是链表

JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

猜你喜欢

转载自blog.csdn.net/fada_is_in_the_way/article/details/109681682