Java 黑马程序员学习笔记(进阶篇15)
1. 双列集合
(1) 双列集合的概念
① 双列集合指的是 java.util.Map
体系,存储的是 键值对(key-value) 形式的数据。
② 特点:
- 键(key)唯一,不能重复(重复会覆盖原值)。
- 值(value)可以重复。
- 一个键对应一个值。
- 允许有 null 键(只能有一个),和多个 null 值。
2. Map 集合常用的 API
方法 | 作用 |
---|---|
V put(K key, V value) | 添加键值对(键重复则覆盖) |
V remove(Object key) | 根据键删除对应键值对 |
boolean containsKey(Object key) | 判断是否包含某个键 |
boolean containsValue(Object value) | 判断是否包含某个值 |
void clear() | 清空所有键值对 |
int size() | 获取键值对数量 |
① put
方法的作用
- 将指定的 key 与 value 关联起来
- 如果
Map
中已经存在该 key,则覆盖旧值,并返回旧的 value - 如果不存在该 key,则新增一个键值对,返回
null
② remove
方法的作用
- 根据 key 删除 Map 中的键值对
- 如果 key 存在,返回被删除的 value
- 如果 key 不存在,返回
null
3. Map 的遍历方式
① 遍历 keySet(最常见的 “键找值” 写法)
Map<String, String> map = new HashMap<>();
map.put("name", "Alice");
map.put("age", "20");// 1. 获取所有 key
Set<String> keySet = map.keySet();// 2. 遍历 key,通过 key 找 value
for (String key : keySet) {String value = map.get(key);System.out.println(key + " = " + value);
}
特点:
- 优点:代码直观,适合只需要 key 时
- 缺点:通过 key 再 get (value) 会多一次哈希查找(性能略低于 entrySet)
② 遍历 EntrySet
方法名 | 说明 |
---|---|
Set<Map.Entry<K,V>> entrySet() | 获取所有键值对对象的集合 |
public class MapDemo03 {public static void main(String[] args) {//Map集合的第二种遍历方式//1.创建Map集合的对象Map<String, String> map = new HashMap<>();//2.添加元素//键:人物的外号//值:人物的名字map.put("标枪选手", "马超");map.put("人物挂件", "明世隐");map.put("御龙骑士", "尹志平");//3.Map集合的第二种遍历方式//通过键值对对象进行遍历//3.1 通过一个方法获取所有的键值对对象,返回一个Set集合Set<Map.Entry<String, String>> entries = map.entrySet();//3.2 遍历entries这个集合,去得到里面的每一个键值对对象for (Map.Entry<String, String> entry : entries) {//entry ---> "御龙骑士","尹志平"//3.3 利用entry调用get方法获取键和值String key = entry.getKey();String value = entry.getValue();System.out.println(key + "=" + value);}}
}
③ Lambda 表达式
(1) 概述:
得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。
(2) 基本用例:
public class MapDemo03 {public static void main(String[] args) {//Map集合的第三种遍历方式//1.创建Map集合的对象Map<String,String> map = new HashMap<>();//2.添加元素//键:人物的名字//值:名人名言map.put("鲁迅","这句话是我说的");map.put("曹操","不可能绝对不可能");map.put("刘备","接着奏乐接着舞");map.put("柯镇恶","看我眼色行事");//3.利用lambda表达式进行遍历//底层://forEach其实就是利用第二种方式进行遍历,依次得到每一个键和值//再调用accept方法map.forEach(new BiConsumer<String, String>() {@Overridepublic void accept(String key, String value) {System.out.println(key + "=" + value);}});System.out.println("-----------------------------------");map.forEach((String key, String value)->{System.out.println(key + "=" + value);});System.out.println("-----------------------------------");map.forEach((key, value)-> System.out.println(key + "=" + value));}
}
4. HashMap
① HashMap 基本概念
HashMap 是 Java 集合框架中实现 Map
接口的类,专门用于存储键值对(key-value)。
② 核心特点
- 无序性:键值对的存储顺序与取出顺序可能不一致(因为基于哈希表存储,按哈希值分布)。
- null 支持:允许键或值为 null(注意:键只能有一个 null,因为 “键唯一”;值可以多个 null)。
- 键的唯一性:若插入相同键,后插入的
value
会覆盖先插入的value
。 - 线程不安全:多线程同时操作时,可能出现数据不一致问题。若需线程安全,可考虑
ConcurrentHashMap
或Collections.synchronizedMap()
。 - 高效性:基于哈希表实现,插入、查询(get/put)的时间复杂度为 O (1)(理想情况:哈希分布均匀,无大量冲突)。
③ 底层数据结构(JDK8 及以后)
HashMap 底层由 “数组 + 链表 + 红黑树” 组成(JDK7 及之前是 “数组 + 链表”),结构优化的核心是 “链表转红黑树”,用于解决哈希冲突后的性能问题:
- 数组:作为哈希表的 “桶(bucket)”,每个元素是一个 “节点(Node)”。
- 链表:当多个键的哈希值冲突(计算后落到同一个数组索引),会以链表形式存储这些冲突的节点。
- 红黑树:当链表的长度超过 8,且数组的长度 ≥ 64时,链表会自动转换为红黑树(红黑树是 “平衡二叉树”,查询效率从链表的 O (n) 优化到 O (logn)),大幅提升查询性能。
④ “键的唯一性” 如何保证?
HashMap 依赖 hashCode()
方法和 equals()
方法保证 “键唯一”:
- 当存储自定义对象作为键时,必须重写
hashCode()
和equals()
方法。否则会使用Object
类的默认方法(基于 “对象地址” 判断相等性),导致 “逻辑相同的对象” 被误认为 “不同键”。 - 若值是自定义对象,不需要重写这两个方法(因为 “值” 不参与 “键的唯一性” 判断)。
④ 综合练习
(1) 题目 1:学生信息与籍贯存储
需求:
- 创建一个
Student
类,包含姓名(name
)和年龄(age
)两个属性,并提供构造方法和toString()
方法。 - 在
main
方法中创建一个HashMap<Student, String>
,用来存储学生对象和对应的籍贯。 - 向
HashMap
中添加以下数据:Student("zhangsan", 23)
->"江苏"
Student("lisi", 24)
->"浙江"
Student("wangwu", 25)
->"福建"
- 通过
keySet()
方法获取所有学生对象(键),遍历并输出 “学生信息 = 籍贯”。
提示:
keySet()
方法会返回一个Set<K>
,其中K
是Map
的键类型,在这里是Student
。- 遍历
Set
时,通过get(key)
方法获取对应的值(籍贯)。
测试类:
package demo1;import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;public class test1 {public static void main(String[] args) {HashMap<Student, String> hm = new HashMap<>();Student s1 = new Student("zhangsan",23);Student s2 = new Student("lisi",24);Student s3 = new Student("wangwu",25);hm.put(s1,"江苏");hm.put(s2,"浙江");hm.put(s3,"福建");Set<Student> keys = hm.keySet(); //不太理解,这个Set是从哪里来的for (Student key : keys) {String value = hm.get(key);System.out.println(key + "=" + value);}System.out.println("-------------------");Set<Map.Entry<Student, String>> entries = hm.entrySet();for (Map.Entry<Student, String> entry : entries) {Student key = entry.getKey();String value = entry.getValue();System.out.println(key + "=" + value);}System.out.println("----------------------");hm.forEach((student, s)->System.out.println(student + "=" + s));}
}
JavaBean:
package demo1;import java.util.Objects;public class Student {private String name;private int age;public Student() {}public Student(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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, age);}public String toString() {return "Student{name = " + name + ", age = " + age + "}";}
}
关键逻辑 1:这个 Set 是从哪里来的?
Set<Student> keys = hm.keySet();
- 这里的
keys
就是hm
中所有Student
对象的集合(s1, s2, s3
)。 - 类型是
Set<Student>
,因为:- 元素是
Student
类型(Map 的 key 类型) - 元素唯一(Set 特性)
- 元素是
关键逻辑 2:遍历过程
for (Student key : keys) {String value = hm.get(key); // 通过 key 取 valueSystem.out.println(key + "=" + value);
}
- 先拿到所有 key (
keys
) - 循环每个 key
- 用
hm.get(key)
取出对应的值(籍贯)
(2) 题目 2:随机字母频率统计
需求:
- 定义一个字符串数组
{"A","B","C","D"}
。 - 使用
Random
从数组中随机取字母,共取 80 次,存到ArrayList
中。 - 使用
HashMap<String,Integer>
统计每个字母出现的次数。 - 遍历
HashMap
,找出出现次数最多的数字(最大值)。 - 再次遍历
HashMap
,输出所有出现次数等于最大值的字母。 - 打印格式如下:
{A=20, B=22, C=18, D=20}
22
B
提示:
containsKey(key)
方法可以判断Map
中是否包含某个键get(key)
方法可以获取键对应的值entrySet()
方法返回键值对的Set
集合,方便同时遍历键和值
package demo1;import java.util.*;public class test2 {public static void main(String[] args) {String[] arr = {"A","B","C","D"};ArrayList<String> list = new ArrayList<String>();Random r = new Random();for (int i = 0; i < 80; i++) {int index = r.nextInt(arr.length);list.add(arr[index]);}HashMap<String,Integer> hm = new HashMap<>();for (String name : list) {if (hm.containsKey(name)) { //不太理解int count = hm.get(name); //不太理解count++;hm.put(name, count);} else{hm.put(name,1);}}System.out.println(hm);int max = 0;Set<Map.Entry<String, Integer>> entries = hm.entrySet();for (Map.Entry<String, Integer> entry : entries) {int count = entry.getValue();if (count > max) {max = count;}}System.out.println(max);for (Map.Entry<String, Integer> entry : entries) {int count = entry.getValue();if (count == max) {System.out.println(entry.getKey());}}}
}
关键逻辑 1:if (hm.containsKey(name))
containsKey()
是 Map 接口 的方法- 作用:判断
hm
中是否已经存在某个 key(这里 key 是name
,也就是当前遍历到的字母)
- 返回值:
true
:已经存在这个字母false
:还没有这个字母
例子:
- 如果
name = "A"
,而hm
中已经有"A"
这个 key,就返回true
- 如果是第一次遇到
"A"
,就返回false
关键逻辑 2:int count = hm.get(name)
get(name)
是 Map 接口 的方法- 作用:根据 key 取出对应的 value
- 这里的
value
是一个Integer
(即这个字母已经出现的次数)