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是如何存储数据的。
如上图,存储过程是:
-
首先调用key对象的hashCode()方法获取“键”对象的哈希码(hashcode),哈希码是int型数据;
-
然后调用HashMap类的hash方法获取对应哈希码的hash值;
hash值的取值范围是[0,数组长度-1].
-
把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的存储结构示意图:
可以看到,使用数组和单向链表共同存储数据,这样做的好处是大大提高了查询和增删效率。
需要注意的是:
- 存储链表头的数组不是固定大小,可以扩充;
- 每个链表的头结点都是存储在数组中,查找时先查找头结点,然后通过头结点找后面的节点;
注意:
- 我觉得这样存储数据会有一个问题,已知不同哈希码对应的hash值可能相同,相同的hash值的节点使用单链表结构连接起来,那么如果一组数据中绝大多数的哈希码经计算后都对应同一个hash值,这些数据节点就会存储于数组中这个索引位置,最终导致其他数组索引位置没有或者只有很少的数据,这个问题如何解决?
- 如果数据量很大,数组长度很长,查询效率会不会降低?如何解决?
- 乍一看似乎这种方式(HashMap)并没有提高多少效率,但是结合数组和链表的优点稍作思考就会发现,假设一个HashMap中近似均匀存了一万条数据,数组长度是16,根据HashMap的特点可知,相同hash值的数据是存储在一个数组空间上的同一条链表上的,如果此时我需要查找其中的某一个数据Q,首先计算Q的哈希码(hashcode),然后计算他的hash值,根据hash值找到它所在的数组索引位置,然后从链表头结点开始搜寻,这样做的好处是,可以快速找到它所在链表的头结点,而不用搜寻剩下的所有的节点!
总结HashMap存储数据的过程:
在添加一个元素(key-value)时,首先使用hashcode()方法计算key的哈希码,然后使用hash()方法计算它的的hash值,以此确定插入数组中的位置(hash 值就是数组索引),可能存在同一hash值的元素已经被放在数组同一索引位置,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的hash值是相同的,所以说数组存放的是链表。
JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。