HashMap 集合
- 1. 概述
- 2. 方法
- 3. 遍历方式
- 4. 代码示例1
- 5. 代码示例2
- 6. 注意事项
- 7. 源码分析
其他集合类
父类 Map
实现类 LinkedHashMap
集合类的遍历方式
具体信息请查看 API 帮助文档
1. 概述
HashMap 是 Java 中的一种集合类,它实现了 Map 接口。HashMap 使用键值对存储数据,每个键值对被称为一个 Entry(条目)。HashMap 使用哈希表来存储数据,因此能够在 O(1) 时间复杂度下进行插入、删除和查找操作。
HashMap 的特点包括:
-
键不重复:HashMap 中的键是唯一的,如果插入时有重复键,则后面的值会覆盖之前的值。
-
无序性:HashMap 中的元素没有固定的顺序,即不保证元素的顺序与插入的顺序一致。
-
允许空键和空值:HashMap 允许键和值都为 null。
-
非线程安全:HashMap 不是线程安全的,如果有多个线程同时访问一个 HashMap 对象并对其进行修改,可能会导致数据不一致或抛出异常。如果需要在多线程环境中使用,可以考虑使用 ConcurrentHashMap。
2. 方法
HashMap
集合是Map
集合的子类,因此Map
集合的方法HashMap
集合都能使用。
Map集合
方法名 | 说明 |
---|---|
V put(K key,V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
3. 遍历方式
与共有的 集合遍历方式 一样
4. 代码示例1
- 代码示例
存储学生对象并遍历
需求:创建一个HashMap集合,键是学生对象(Student),值是籍贯(String)。存储三个键值对元素,并遍历集合
要求:同姓名,同年龄认为是同一个学生
package text.text02;
import java.util.*;
import java.util.function.BiConsumer;
/*
存储学生对象并遍历
需求:创建一个HashMap集合,键是学生对象(Student),值是籍贯(String)。存储三个键值对元素,并遍历集合
要求:同姓名,同年龄认为是同一个学生
*/
public class text49 {
public static void main(String[] args) {
//创建学生对象
Student7 student1 = new Student7("张三", 23);
Student7 student2 = new Student7("李四", 24);
Student7 student3 = new Student7("王五", 25);
Student7 student4 = new Student7("王五", 25);
//创建集合对象
HashMap<Student7, String> hm = new HashMap<>(); //键是自定义对象,必须要重写equals和hashCode方法
//添加集合元素
hm.put(student1, "山西");
hm.put(student2, "河南");
hm.put(student3, "湖北");
hm.put(student4, "湖北");
//遍历集合 、
//1.通过键找值的方式遍历
System.out.println("1.通过键找值的方式遍历:");
Set<Student7> set = hm.keySet();
Iterator<Student7> it = set.iterator();
while (it.hasNext()) {
Student7 key = it.next();
String value = hm.get(key);
System.out.println(key + " = " + value);
}
//2.通过键值对的方式遍历
System.out.println("2.通过键值对的方式遍历:");
Set<Map.Entry<Student7, String>> entries = hm.entrySet();
for (Map.Entry<Student7, String> map : entries) {
Student7 key = map.getKey();
String value = map.getValue();
System.out.println(key + " = " + value);
}
//3.通过结合Lambda表达式的方式遍历
System.out.println("3.通过结合Lambda表达式的方式遍历:");
hm.forEach(new BiConsumer<Student7, String>() {
@Override
public void accept(Student7 key, String value) {
System.out.println(key + " = " + value);
}
});
}
}
class Student7 {
private String name;
private int age;
public Student7() {
}
public Student7(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
*
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
*
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
*
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
*
* @param age
*/
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student7 student7 = (Student7) o;
return age == student7.age && Objects.equals(name, student7.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return "Student7{name = " + name + ", age = " + age + "}";
}
}
- 输出结果
-
1.通过键找值的方式遍历
-
2.通过键值对的方式遍历
-
3.通过结合Lambda表达式的方式遍历
-
5. 代码示例2
- 代码示例
Map集合案例–统计投票人数
需求:某个班级80名学生,现在需要组成秋游活动,班长提供了四个景点,依次是(A,B,C,D),每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
package text.text02;
import java.util.*;
/*
Map集合案例--统计投票人数
需求:某个班级80名学生,现在需要组成秋游活动,班长提供了四个景点,依次是(A,B,C,D),每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
*/
public class text50 {
public static void main(String[] args) {
//计数器方法
System.out.println("====================计数器方法==================");
method1();
//通用方法
System.out.println("====================通用方法====================");
method2();
}
//计数器方法
public static void method1() {
//定义变量记录A,B,C,D四个景点的投票人数
int A = 0;
int B = 0;
int C = 0;
int D = 0;
//创建集合
HashMap<String, Integer> hm = new HashMap<>();
//利用随机数模拟随机选择的景点,并利用循环添加进集合
Random r = new Random();
for (int i = 0; i < 80; i++) {
int num = r.nextInt(4);
if (num == 0) A++;
if (num == 1) B++;
if (num == 2) C++;
if (num == 3) D++;
}
//将景点以及每个景点的投票人数添加进去
hm.put("A", A);
hm.put("B", B);
hm.put("C", C);
hm.put("D", D);
System.out.println("四个景点的投票情况:" + hm);
//判断哪个景点投票人数最多
//求出投票最多的票数
int sum = 0;//定义一个变量记录最多的景点的票数
Set<String> set = hm.keySet();
for (String key : set) {
Integer value = hm.get(key);
if (value > sum) {
sum = value;
}
}
System.out.println("景点投票最多的票数:" + sum);
//遍历map集合,根据投票最多的票数确定景点(可能存在多个景点投票一样的情况)
for (String key : set) {
Integer value = hm.get(key);
if (value == sum) {
System.out.println("投票最多的景点:" + key);
}
}
}
//通用方法
public static void method2() {
//定义个数组,存储四个景点
String[] arr = {"A", "B", "C", "D"};
//定义个集合用来存储每个学生投票的景点
ArrayList<String> list = new ArrayList<>();
Random r = new Random();
//利用循环将学生的投票景点添加进集合
for (int i = 0; i < 80; i++) {
int index = r.nextInt(arr.length);
list.add(arr[index]);
}
System.out.println("ArrayList集合里的数据:" + list);
//创建双列集合用于存储景点名称和票数
HashMap<String, Integer> hm = new HashMap<>();
//遍历单列集合,并将其中的景点和对应的票数添加进双列集合
for (String name : list) {
//单列集合中存储的景点名称在双列集合中存在
if (hm.containsKey(name)) {
//获取该景点的投票数
Integer value = hm.get(name);
//将投票数+1
value++;
//再将新的投票数覆盖原来的投票数
hm.put(name, value);
}
//单列集合中存储的景点名称在双列集合中不存在
else {
hm.put(name, 1);
}
}
System.out.println("HashMap集合里面的数据:" + hm);
//判断哪个景点投票人数最多
//求出投票最多的票数
int sum = 0;//定义一个变量记录最多的景点的票数
Set<String> set = hm.keySet();
for (String key : set) {
Integer value = hm.get(key);
if (value > sum) {
sum = value;
}
}
System.out.println("景点投票最多的票数:" + sum);
//遍历map集合,根据投票最多的票数确定景点(可能存在多个景点投票一样的情况)
for (String key : set) {
Integer value = hm.get(key);
if (value == sum) {
System.out.println("投票最多的景点:" + key);
}
}
}
}
- 输出结果
-
计数器方法
-
通用方法
-
6. 注意事项
-
键的唯一性:HashMap 中的键是唯一的,如果插入时有重复键,则后面的值会覆盖之前的值。因此,在使用 HashMap 时要确保键的唯一性,避免出现重复键的情况。
-
键的稳定性:HashMap 的元素没有固定的顺序,也就是说,存入元素的顺序与取出元素时的顺序可能不一致。如果需要按照特定的顺序遍历或操作元素,可以考虑使用 LinkedHashMap。它是 HashMap 的一个子类,保留了元素插入的顺序。
-
hashCode()和equals()方法:HashMap 使用键的 hashCode() 和 equals() 方法来确定键的位置。因此,键的类型必须正确实现这两个方法,以确保正确的插入、查找和删除操作。如果自定义的类作为 HashMap 的键,必须重写 hashCode() 和 equals() 方法,以保证键的唯一性和正确性。
-
并发安全性:HashMap 是非线程安全的,如果有多个线程同时访问一个 HashMap 并对其进行修改,可能会导致数据不一致或抛出异常。如果需要在多线程环境中使用 HashMap,可以考虑使用 ConcurrentHashMap,它是线程安全的。
-
初始容量和负载因子:HashMap 默认的初始容量为 16,并且每次扩容是当前容量的两倍。若事先无法估计容量大小,则可以通过构造函数指定初始容量和负载因子。负载因子表示 HashMap 在容量自动增加之前可以达到的平均负载程度,默认为 0.75。较低的负载因子会减少碰撞,但会增加空间消耗;较高的负载因子会增加碰撞,但会减少空间消耗。根据实际情况选择合适的初始容量和负载因子来平衡空间和时间的开销。
7. 源码分析
-
看源码之前需要了解的一些内容
Node<K,V>[] table 哈希表结构中数组的名字 DEFAULT_INITIAL_CAPACITY: 数组默认长度16 DEFAULT_LOAD_FACTOR: 默认加载因子0.75
HashMap里面每一个对象包含以下内容:
- 链表中的键值对对象
- 包含:
int hash; //键的哈希值 final K key; //键 V value; //值 Node<K,V> next; //下一个节点的地址值
- 红黑树中的键值对对象
- 包含:
int hash; //键的哈希值 final K key; //键 V value; //值 TreeNode<K,V> parent; //父节点的地址值 TreeNode<K,V> left; //左子节点的地址值 TreeNode<K,V> right; //右子节点的地址值 boolean red; //节点的颜色
-
添加元素
HashMap<String,Integer> hm = new HashMap<>(); hm.put("aaa" , 111); hm.put("bbb" , 222); hm.put("ccc" , 333); hm.put("ddd" , 444); hm.put("eee" , 555);
添加元素的时候至少考虑三种情况:
-
数组位置为null
-
数组位置不为null,键不重复,挂在下面形成链表或者红黑树
-
数组位置不为null,键重复,元素覆盖
-
- put()方法
//参数一:键
//参数二:值
//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- hash()方法
//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- putVal()方法
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留(默认是false)
// true,表示老元素的值保留,不会覆盖
// false,表示老元素的值不保留,会进行覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义一个局部变量,用来记录哈希表中数组的地址值。
//虽然开头定义了一个全局变量table,但是全局变量存储在堆内存中,而局部变量和方法都存储在栈内存中,省去了再在堆内存调用全局变量的时间,提高了效率
Node<K,V>[] tab;
//临时的第三方变量,用来记录键值对对象的地址值
Node<K,V> p;
//表示当前数组的长度
int n;
//表示索引
int i;
//把哈希表中数组的地址值,赋值给局部变量tab
tab = table;
if (tab == null || (n = tab.length) == 0){
//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
tab = resize();
//表示把当前数组的长度赋值给n
n = tab.length;
}
//拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象在数组中应存入的位置
i = (n - 1) & hash;//index
//获取数组中对应元素的数据
p = tab[i];
if (p == null){
//底层会创建一个键值对对象,直接放到数组当中
tab[i] = newNode(hash, key, value, null);
}else {
Node<K,V> e;
K k;
//等号的左边:数组中键值对的哈希值
//等号的右边:当前要添加键值对的哈希值
//如果键不一样,此时返回false
//如果键一样,返回true
boolean b1 = p.hash == hash;
if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
}
//判断数组中获取出来的键值对是不是红黑树中的节点
else if (p instanceof TreeNode){
//如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
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
//treeifyBin方法的底层还会继续判断
//判断数组的长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//e: 0x0044 ddd 444
//要添加的元素: 0x0055 ddd 555
//如果哈希值一样,就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
//如果e为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
//e:0x0044 ddd 444
//要添加的元素: 0x0055 ddd 555
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
//等号的右边:当前要添加的值
//等号的左边:0x0044的值
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
//threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 16 * 0.75 = 12
if (++size > threshold){
resize();
}
//表示当前没有覆盖任何元素,返回null
return null;
}