HashMap 基本介绍
HashMap
是 Java 中的一个集合类,实现了 Map
接口,用于存储键值对(key-value)数据。它基于哈希表的数据结构实现,可以实现高效的查找、插入和删除操作。
HashMap 细节讨论
- 无序性:
HashMap
中的元素是无序的,即遍历的顺序不一定是元素插入的顺序。 - 键唯一性:
HashMap
的键是唯一的,不允许重复的键。如果插入相同的键,后面的值会覆盖前面的值。 - 允许 null 键和 null 值:
HashMap
允许使用null
作为键,并且允许键对应的值为null
,但是只允许有一个null键,因为键是唯一的。 - 非线程安全:
HashMap
不是线程安全的,如果多个线程同时访问并修改同一个HashMap
,可能会导致数据不一致或其他错误。 - 初始容量和负载因子:
HashMap
允许设置初始容量和负载因子,初始容量是哈希表中桶的数量,负载因子是哈希表在达到多满时进行扩容操作(这里和HashSet一样,因为HashSet的底层是用HashMap实现的)。
使用注意事项
- 线程安全性: 如果需要在多线程环境中使用
HashMap
,需要进行适当的同步处理,或者考虑使用线程安全的集合类(如ConcurrentHashMap
)。 - 哈希冲突: 当不同的键映射到相同的哈希值时,发生哈希冲突。
HashMap
使用链表(JDK8 之前)或红黑树(JDK8 及以后)来处理哈希冲突。 - equals 和 hashCode 方法: 在使用自定义类作为
HashMap
的键时,需要正确实现该类的equals
和hashCode
方法,以确保键的唯一性和正确的哈希计算。
常用方法
以下是一些常用的 HashMap
方法:
put(key, value)
: 向HashMap
中插入键值对。get(key)
: 根据键获取对应的值。containsKey(key)
: 判断是否包含指定键。containsValue(value)
: 判断是否包含指定值。remove(key)
: 根据键删除对应的键值对。size()
: 返回HashMap
中键值对的数量。isEmpty()
: 判断HashMap
是否为空。keySet()
: 返回包含所有键的集合。values()
: 返回包含所有值的集合。entrySet()
: 返回包含所有键值对的集合。
底层源码和底层实现
HashMap
的底层实现主要是基于哈希表,其中每个桶(bucket)是一个链表或红黑树。当哈希冲突发生时,新的键值对会被插入到相应的桶中。
HashMap
的底层源码非常复杂,涉及哈希计算、扩容、桶的处理等多个方面。如果想深入了解 HashMap
的底层实现,可以查阅 Java 的源代码或相关的学习资料。
HashMap的遍历代码:
public class HashMap_ {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put(1,new Student("1","ret1",23,"计算机"));
hashMap.put(2,new Student("2","ret2",23,"计算机"));
hashMap.put(3,new Student("3","ret3",23,"计算机"));
System.out.println(hashMap);
for (Object entry : hashMap.entrySet()) {
Map.Entry entry1 = (Map.Entry) entry;
System.out.println(entry1.getKey()+" "+entry1.getValue());
}
Set set = hashMap.keySet();
for (Object key :set) {
System.out.println(key+" "+hashMap.get(key));
}
}
}
class Student{
private String sno;
private String name;
private int age;
private String major;
public Student(String sno, String name, int age, String major) {
this.sno = sno;
this.name = name;
this.age = age;
this.major = major;
}
public String getSno() {
return sno;
}
public void setSno(String sno) {
this.sno = sno;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
@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(sno, student.sno) && Objects.equals(name, student.name) && Objects.equals(major, student.major);
}
@Override
public int hashCode() {
return Objects.hash(sno, name, age, major);
}
@Override
public String toString() {
return "Student{" +
"sno='" + sno + '\'' +
", name='" + name + '\'' +
", age=" + age +
", major='" + major + '\'' +
'}';
}
}
HashMap的底层代码:
/*HashMap底层原理
1. 执行构造器 new HashMap()
初始化加载因子 loadFactor = 0.75
HashMap$Node[] table = null
2. 执行 put 调用 hash 方法,计算 key 的 hash 值(h = key.hashCode()) ^ (h >>> 16)
public V put (K key, V value){
//K = "java" value = 10
return putVal(hash(key), key, value, false, true);
}
3. 执行 putVal
final V putVal ( int hash, K key, V value,boolean onlyIfAbsent, boolean evict){
Node<K, V>[] tab;
Node<K, V> p;
int n, i;//辅助变量
//如果底层的 table 数组为 null, 或者 length =0 , 就扩容到 16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把加入的 k-v创建成一个 Node ,加入该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K, V> e;
K k;//辅助变量
// 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
// 并满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真)就认为不能加入新的 k-v
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处理
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的结点,后面是链表,就循环比较
for (int binCount = 0; ; ++binCount) {//死循环
if ((e = p.next) == null) {//如果整个链表,没有和他相同,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到 8 个,到 8 个,后
//就调用 treeifyBin 方法进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //如果在循环比较过程中,发现有相同,就 break,就只是替换 value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //替换,key 对应 value
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每增加一个 Node ,就 size++
if (++size > threshold[12 - 24 - 48])//如 size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5. 关于树化(转成红黑树)
//如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容. //否则才会真正的树化 -> 剪枝
final void treeifyBin (Node < K, V >[]tab,int hash){
int n, index;
Node<K, V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
*/