Map接口是和Collection并列的另一个接口,他的实现类有HashMap、TreeMap、HashTable、Properties等等实现类。
Map最大的特点是,他用来存储键(key)值(value)对,这个键值对是通过键(key)来标示的,所以key是不能重复的。这种成对存储的模式,在比如微信一个手机号只能对应一个账户这种的场景下的应用是很重要的。
一、HashMap的各种方法测试1
我们来测试一下HashMap的一些相关方法:
Map<Integer,String> map=new HashMap<>();//定义一个HashMap,泛型填入key和value的类型
Map<Integer,String> map2=new HashMap<>();
map.put(0, "A");//存放键值对
map.put(1, "B");
map.put(2, "C");
map2.put(3, "A");//存放键值对
map2.put(4, "B");
map2.put(5, "C");
System.out.println(map.toString());
System.out.println(map.isEmpty());//是否为空
System.out.println(map.containsKey(3));//是否包含key
System.out.println(map.containsValue("D"));//是否包含value
System.out.println(map.get(0));//通过key查找value
map.putAll(map2);//把map2全部复制到map1
System.out.println(map.containsKey(3));
System.out.println(map.toString());
System.out.println(map.get(3));
map.put(3, "E");//key要唯一,如果key重复就会覆盖原来key位置的value,重复的判断是调用的equals方法
System.out.println(map.get(3));
(在java已有的类型的情况下toString方法是包含在里面的,直接输出就可以)
二、HashMap的各种方法测试2
泛型参数里除了基本类型还可以传入我们自定义的类,我们用自己已定义一个类来测试对应的方法:
class Employee{
private int id;
private String name;
private double salary;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Employee(int id, String name, double salary) {
super();
this.id = id;
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}
}
然后再去测试对应的这些方法:
Employee employee=new Employee(160, "john", 500.00);
Employee employee2=new Employee(161, "john1", 500.00);
Employee employee3=new Employee(162, "john2", 500.00);
Map<Integer, Employee> map=new HashMap<Integer, Employee>();
map.put(1, employee);
map.put(2, employee2);
map.put(3, employee3);
System.out.println(map.toString());
Employee employee4=map.get(1);
System.out.println(employee4.getName());
Employee employee5=new Employee(164, "Archie", 30000);
map.put(1, employee5);
System.out.println(map.toString());
三、HashMap底层原理(1)—存储键值对的底层过程:
HashMap的底层实现使用了哈希表这种数据结构,而哈希表的基本结构是“数组+链表”
因为对于数组来说,占用空间是连续的,所以查询效率很高,但是增删操作效率低;而对于链表来说,占用空间不连续,查询很慢,但是增删效率很高。哈希表就把数组和链表的优点进行结合,用这两种数据结构来构成哈希表。
我们查看HashMap的源代码,核心部分的数据结构就是哈希表table:
/**
* 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;
那么我们再点开里面的数据结构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;//key对象
V value;//value对象
Node<K,V> next;//下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
从最前面的三个属性,就可以看得到这个是一个单链表的节点结构。
那么我们的这个HashMap的核心结构就是由一个装满了单链表的数组组成的,类似这样的图示:
那么进入正题,存储键值对在HashMap的结构里,究竟是怎么样的一个过程呢:
存储数据过程put(key.value)
put的源码是这样的:(里面的k和v是泛型哦)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们已经知道了HashMap的基本结构就是名为table的Node类型的数组,数组里是很多个Node组成的单链表。
我们的目的是将“key-value”键值对存放到这数组里,具体的步骤是这样的:
1.调用key对象.hashcode()方法(上面的源码可以看到)
这个方法对整个key对象产生哈希值并返回。
2.根据hashcode计算出hash值(在[0,数组长度-1]之间)
产生了hashcode这个编码之后,要最终对应到我们的数组的固定位置,这里要算出hash值,然后对应到数组的索引位置,这个过程肯定要进行散列的计算,方法有很多,肯定希望的是产生的hash值能尽量均匀的分布在[0,数组长度-1]的区间内,并且要减少哈希冲突,因为极端情况下,可能要么都存在了这个链表数组的第一个位置,就变成了链表,要么就成了单纯的数组,没有了链表的增删优势。
3.生成Node对象
计算出hash值之后,这个Node节点的next为空,这样Node对象有的四个属性都已经存在了。
4.将生成的Node对象放到table数组里
如果本来对应的数组索引位置没有存放Entry对象,就直接把Entry对象存储进数组里,如果已经有,就追加到这个数组的位置的链表的后面,也就是本来存放的链表里的next为空的Node指向这个新生成 的Node,形成链表。
上面的过程就是抽象层面的HashMap的put方法的过程,其中具体实现的顺序并不是这样,源代码也只是思路是这样。
具体的put方法代码和流程详解在另外一篇笔记里(以JDK8为例),put方法里面的流程非常巧妙,还涉及到了一些红黑树的知识,存储也并不是简单的链表。有博客做了详细的分析。
四、HashMap底层原理(2)—查找键值对的底层过程:
取数据的过程需要通过key对象获得“键值对”对象然后返回value对象,因为已经知道了取数据的过程, 所以get(key)的过程就相对简单了:
1.获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置;
2.在链表上挨个比较key对象,调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点为止;
3.返回equals()为true的节点对象的value对象
明白了存取数据的过程,我们再来看一下hashcode()和equals()方法的关系:Java中是这样规定的,如果两个对象的内容相同,也就是说他们的equals()结果为true,那他们的hashcode一定要相同;但是如果他们不equals,那么hashcode也是有可能相等的;反过来如果他们的hashcode相等,也不一定equals;但是最后,如果hashcode不相等,那么一定不equals。
五、HashMap的扩容问题
HashMap的位桶数组,初始大小为16,实际使用时,大小是可变的,如果位桶中的元素达到(0.75*数组length),就重新调整数组大小变成原来的两倍大小。
扩容是非常耗时的,因为本质上扩容是定义一个更大的数组,并将原来的数组内容全部拷贝到新的数组中。