Map、哈希表数据结构、HashMap、Hashtable
首先需要明确的是,Map与Collection是不同的接口,二者是没有关系的。
Map是(key,value)键值对元素形式的集合的超级父接口,HashMap和Hashtable是Map的实现类。
1.Map
存储元素的形式为(key,value),key和value存储的都是引用数据类型,key起主导作用。
key是无序、不可重复的,value是没有限制的。
Map集合的常用方法如下:
- V put(K key, V value) :添加元素;
- V get(Object key) :通过key获取指定元素;
- V remove(Object key):通过key移除某个元素;
- boolean containsKey(Object key):是否存在某个key;
- boolean containsValue(Object Value):是否存在某个Value;
- void clear():清空元素;
- boolean isEmpty():判断集合是否为空;
- int size():返回元素个数;
- Set
keySet():获取集合中所有的键key; - Collection
values():获取集合中所有的值value; - Set<Map.Entry<K,V>> entrySet():将Map集合转换为Set集合。
- Set集合中存储的元素为key=value形式;
- Entry是Map的一个静态内部类,简单理解就看作像String一样的数据类型。
例:
package com.dh.map;
import java.util.*;
public class Map01 {
public static void main(String[] args) {
//Map是接口,HashMap是其实现类
Map<Integer,String> map = new HashMap<>();
//使用put添加元素
map.put(1,"zhangsan");
map.put(1,"zhangsansan");//key可以重复,但是此时相当于替换元素为zhangsansan
map.put(2,"lisi");
map.put(3,"wangwu");
//通过key获取元素
String s = map.get(1);//输出为zhangsansan
System.out.println(s);
//判断是否存在指定的key和value
System.out.println(map.containsKey(1));
System.out.println(map.containsValue("zhangsan"));
//输出元素个数
System.out.println(map.size());
//判断集合是否为空
System.out.println(map.isEmpty());
//获取所有的key
//遍历集合的一种方式
Set<Integer> keySet = map.keySet();
Iterator<Integer> iterator = keySet.iterator();
System.out.println("---------通过key遍历:");
while (iterator.hasNext()){
Integer k = iterator.next();
System.out.println(k+"="+map.get(k));
}
//获取所有的value
System.out.println("---------获取所有的value:");
Collection<String> values = map.values();
for (String value : values) {
System.out.println(value);
}
//将Map转变为Set
//遍历集合的第二种方式
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
System.out.println("---------将Map集合转变为Set集合遍历:");
for (Map.Entry<Integer, String> entry : entrySet) {
System.out.println(entry);
}
//清空元素
map.clear();
}
}
结果:
zhangsansan
true
false
3
false
---------通过key遍历:
1=zhangsansan
2=lisi
3=wangwu
---------获取所有的value:
zhangsansan
lisi
wangwu
---------将Map集合转变为Set集合遍历:
1=zhangsansan
2=lisi
3=wangwu
2.哈希表数据结构和HashMap
HashMap的底层是哈希表。HashMap是线程不安全的。
查看HashMap的源码,可以看到HashMap底层元素的组织形式:
注:以下只是摘取部分代码:
public class HashMap<K,V>{
Node<K,V> table; //一维数组
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key调用hashCode()的结果
final K key; //存储在HashMap中key部分和HashSet集合上
V value; //存储在HashMap中value部分
Node<K,V> next; //指向单向链表中的下一个节点
}
}
有源代码可以得出以下结论:
- 哈希表是一维数组和单向链表的结合;
- 一维数组中的每一个元素为一个单向链表。
哈希表结构图如下:
一般来说,同一个单向链表上的节点的hash是一致的,但是还有特殊的哈希碰撞的情况出现,即hash不一样,但是通过哈希算法得出的数组下标是一样的。
put(K,V)的原理:
将K和V封装在一个Node<K,V>节点对象中;
K调用其hashCode()得到hash;
再将该hash通过哈希算法得出一个数组下标;
数组下标位置上如果没有单向链表:
则直接放在该数组下标位置上;
若该数组下标有单向链表:
将K与该单向链表中的元素一个一个调用equals()进行比较:
若其中一个为true,则用V替换掉该单向链表节点中的V;
若全部为false,则将元素添加到单向链表的末尾。
get(K)原理:
先调用K的hashCode()得到hash;
将hash通过哈希算法转换成数组下标(快速定位);
如果该数组下标位置上没有单向链表:
则返回null;
如果该数组下标位置上有单向链表:
将K与与该单向链表中的元素一个一个调用equals()进行比较:
若其中一个为true,则返回该单向链表节点上的V;
若全部为false,则返回null。
以上就可以看出哈希表的优势了:
哈希表集数组和链表只所长,在检索元素的时候,可以快速定位到数组的某一个下标,再遍历单向链表;在随机增删元素时,也是先快速定位到数组的某一个下标,然后再进行单向链表上的增删元素。
(虽然并没有达到一维数组检索的最高效率和单向链表随机增删的效率,但是哈希表集合了它们共同的优点,效率也是很高的)
但是我们知道,每一个对象的hashCode()的结果都是不一样的,所以我们需要重写hashCode(),并且我们在使用equals()比较的也都是值,而不是引用,所以也需要重写equals()。
所以得出一个结论:存放在HashMap集合key部分的元素和存放在HashSet集合上的元素,必须重写hashCode()和equals()。
否则就无法达到无序和不可重复的目的。
(重写hashCode()后,因为元素添加到哪一个链表的位置上是随机的,所以输出顺序是不一样的,所以无序;因为调用的是重写的equals(),所以比较的是内容,所以不可重复)
但是重写hashCode()有技巧:
如果hashCode()每次返回一个相同的值,则所有的元素都放在了一个单向链表上,数组则毫无意义;
如果hashCode()每次返回不同的值,则所有的单向链表上只能存放一个元素,即变成了一维数组,单向链表就毫无意义。
IDEA可以帮我们自动重写hashCode()和equals()。(快捷键:alt+Ins,选择重写hashCode()和equals())
基本包装类和String类都已经重写了这两个方法,但是我们存放我们自己书写的对象的时候就需要自己重写。
例:Student类重写hashCode()、equals()、toString()
package com.dh.map;
import java.util.Objects;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
package com.dh.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMap01 {
public static void main(String[] args) {
HashMap<Student, String> hashMap = new HashMap<>();
//实例化两个属性值相同的对象
Student student1 = new Student("zhangsan", 1);
Student student2 = new Student("zhangsan", 1);
//添加元素
hashMap.put(student1,"student1");
hashMap.put(student2,"student2");
//遍历
Set<Map.Entry<Student, String>> entrySet = hashMap.entrySet();
for (Map.Entry<Student, String> entry : entrySet) {
System.out.println(entry);
}
}
}
结果:只输出了后面添加的那个元素
Student{name='zhangsan', age=1}=student2
HashMap其余点:
-
HashMap的初始化容量为16,加载因子为0.75(容量达到75%时就会进行扩容),增长到容量的两倍;
-
自定义HashMap的初始化容量,官方建议为2的倍数,这时的存取效率都比较高;
-
在JDK8之后,如果单向链表上的节点数>8,会将单向链表转换为红黑树;如果红黑树上的元素<6,又会还原为单向链表;
-
HashMap可以存储null键null值(但是null键只能一个,因为key不可重复)。
package com.dh.map; import java.util.HashMap; public class HashMap02 { public static void main(String[] args) { HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(null,null); //HashMap重写了toString(),可直接输出 System.out.println(hashMap); } }
结果:
{null=null}
3.Hashtable
Hashtable的底层也是哈希表。是线程安全的。
由于Hashtable效率比较低,现在使用比较少。
Hashtable是不能存储null键null值的。
package com.dh.map;
import java.util.Hashtable;
public class Hashtable01 {
public static void main(String[] args) {
Hashtable<Object, Object> hashtable = new Hashtable<>();
hashtable.put(null,null);
System.out.println(hashtable);
}
}
结果:
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Hashtable.put(Hashtable.java:475)
at com.dh.map.Hashtable01.main(Hashtable01.java:8)
不管是null键bull值,还是单独的null键和单独的bull值,都会报空指针异常。
Hashtable的初始化容量是11,加载因子也是0.75,扩容增长到2倍再+1。
水平有限,若有错误请指出~