文章目录
- 一、Map集合
- 1.1 Map集合概述和特点【理解】
- 1.2 Map集合的基本功能【应用】
- 1.3 Map集合的获取功能【应用】
- 1.4 Map集合的两种遍历方式
- 二、HashMap集合
- 2.1 HashMap集合概述和特点【理解】
- 2.2 HashMap的组成、构造函数
- 2.3 put、查找方法
- 2.4 HashMap集合应用案例【应用】
- 三、TreeMap集合
- 3.1 TreeMap集合概述和特点【理解】
- 3.2 TreeMap集合应用案例【应用】
- 四、可变参数
- 五、Collections类
- 5.1 Collections常用功能
- 5.2 Comparator比较器
- 六、综合练习
- 练习1:随机点名器
- 练习2:带概率的随机
- 练习3:随机不重复
- 练习4:集合的嵌套
还记得 Java集合框架体系、Collection、List、ArrayList、LinkedList、Set、TreeSet、HashSet 吗?如果忘记可以到这里重新温习: Java集合【超详细】
本文我们重点介绍 Map、HahsMap、TreeMap相关集合。Collection、List、Set相关部分可查看 Java集合【超详细】。
一、Map集合
1.1 Map集合概述和特点【理解】
-
Map集合概述
interface Map<K,V> K:键的类型;V:值的类型
-
Map集合的特点
- 双列集合,一个键对应一个值
- 键不可以重复,值可以重复
-
Map集合的基本使用
public class MapDemo01 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
//V put(K key, V value) 将指定的值与该映射中的指定键相关联
map.put("student1", "zhangsan");
map.put("student2", "lisi");
map.put("student3", "wangwu");
map.put("student4", "zhaoliu");
//输出集合对象
//{student2=lisi, student1=zhangsan, student4=zhaoliu, student3=wangwu}
System.out.println(map);
}
}
1.2 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() | 集合的长度,也就是集合中键值对的个数 |
- 示例代码
public class MapDemo02 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
//V put(K key, V value) 将指定的值与该映射中的指定键相关联
map.put("student1", "zhangsan");
map.put("student2", "lisi");
map.put("student3", "wangwu");
map.put("student4", "zhaoliu");
// System.out.println(map.remove("student1")); //zhangsan
// System.out.println(map.remove("student5")); //null
// //{student2=lisi, student4=zhaoliu, student3=wangwu}
// System.out.println(map);
// map.clear();
// System.out.println(map.size() + ", " + map); //0, {}
System.out.println(map.containsKey("student1")); //true
System.out.println(map.containsKey("student5")); //false
System.out.println(map.containsValue("zhangsan")); //true
System.out.println(map.containsValue("Jenny")); //false
System.out.println(map.isEmpty()); //false
System.out.println(map.size()); //4
System.out.println(map);
}
}
1.3 Map集合的获取功能【应用】
- 方法介绍
方法名 | 说明 |
---|---|
V get(Object key) | 根据键获取值 |
Set keySet() | 获取所有键的集合 |
Collection values() | 获取所有值的集合 |
Set<Map.Entry<K,V>> entrySet() | 获取所有键值对对象的集合 |
- 示例代码
public class MapDemo03 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("student1", "zhangsan");
map.put("student2", "lisi");
map.put("student3", "wangwu");
map.put("student4", "zhaoliu");
System.out.println(map.get("student1"));
System.out.println(map.get("student5"));
//Set<K> keySet():获取所有键的集合
Set<String> keySet = map.keySet();
for (String key : keySet) {
System.out.println(key);
}
System.out.println("~~~~~");
//Collection<V> values():获取所有值的集合
Collection<String> values = map.values();
for (String value : values) {
System.out.println(value);
}
System.out.println("~~~~~");
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
System.out.println(entry);
}
System.out.println(entries);
}
}
1.4 Map集合的两种遍历方式
Map集合有两种遍历方式
-
方式一
- 获取所有键的集合。用keySet()方法实现
- 遍历键的集合,获取到每一个键。用增强for实现
- 根据键去找值。用get(Object key)方法实现
-
方式二
- 获取所有键值对对象的集合
- Set<Map.Entry<K,V>> entrySet():获取所有键值对对象的集合
- 遍历键值对对象的集合,得到每一个键值对对象
- 用增强for实现,得到每一个Map.Entry
- 根据键值对对象获取键和值
- 用getKey()得到键
- 用getValue()得到值
- 获取所有键值对对象的集合
public class MapDemo04 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("student1", "zhangsan");
map.put("student2", "lisi");
map.put("student3", "wangwu");
map.put("student4", "zhaoliu");
//方式一:获取所有键的集合。用keySet()方法实现
Set<String> keySet = map.keySet();
for (String key : keySet) {
//根据键去找值。用get(Object key)方法实现
String value = map.get(key);
System.out.println(key + "," + value);
}
System.out.println("~~~~~");
//方式二:获取所有键值对对象的集合
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
//根据键值对对象获取键和值
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "," + value);
}
}
}
二、HashMap集合
2.1 HashMap集合概述和特点【理解】
- HashMap底层是哈希表结构的
- 依赖hashCode方法和equals方法保证键的唯一
- 如果键要存储的是自定义对象,需要重写hashCode和equals方法
HashMap存放数据的数据是什么呢?代码中存放数据的容器如下:
transient Node<K,V>[] table;
说明了该容器中是一个又一个node组成,而node有三种实现,所以hashMap中存放的node的形式既可以是Node也可以是TreeNode。
2.2 HashMap的组成、构造函数
- HashMap的组成
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
//是hashMap的最小容量16,容量就是数组的大小也就是变量,transient Node<K,V>[] table。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大数量,该数组最大值为2^31一次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子,如果构造的时候不传则为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//一个位置里存放的节点转化成树的阈值,也就是8,比如数组里有一个node,这个node链表的长度达到该值才会转化为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当一个反树化的阈值,当这个node长度减少到该值就会从树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
//具体存放数据的数组
transient Node<K,V>[] table;
//entrySet,一个存放k-v缓冲区
transient Set<Map.Entry<K,V>> entrySet;
//size是指hashMap中存放了多少个键值对
transient int size;
//对map的修改次数
transient int modCount;
//The next size value at which to resize (capacity * load factor).
int threshold;
//加载因子
final float loadFactor;
//...
}
- HashMap的构造函数
//只有容量,initialCapacity
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 容量不能为负数
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当容量大于2^31就取最大值1<<31;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//当前数组table的大小,一定是是2的幂次方
// tableSizeFor保证了数组一定是是2的幂次方,是大于initialCapacity最接近的值。
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor()方法保证了数组大小一定是是2的幂次方,是如何实现的呢?
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
该方法将一个二进制数第一位1后边的数字全部变成1,然后再加1,这样这个二进制数就一定是100…这样的形式。此处实现在ArrayDeque的实现中也用到了类似的方法来保证数组长度一定是2的幂次方。
2.3 put、查找方法
- put方法
开发人员使用的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap内部使用的put值的方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当hash到的位置,该位置为null的时候,存放一个新node放入
// 这儿p赋值成了table该位置的node值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//该位置第一个就是查找到的值,将p赋给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是红黑树,调用红黑树的putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//是链表,遍历,注意e = p.next这个一直将下一节点赋值给e,直到尾部,注意开头是++binCount
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表长度大于等于7,插入第8位,树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((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;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 查找方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//先判断表不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
//这一行是找到要查询的Key在table中的位置,table是存放HashMap中每一个Node的数组。
(first = tab[(n - 1) & hash]) != null) {
//Node可能是一个链表或者树,先判断根节点是否是要查询的key,就是根节点,方便后续遍历Node写法并且
//对于只有根节点的Node直接判断
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//有子节点
if ((e = first.next) != null) {
//红黑树查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//链表查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
//遍历链表,当链表后续为null则推出循环
while ((e = e.next) != null);
}
}
return null;
}
2.4 HashMap集合应用案例【应用】
-
案例需求
- 创建一个HashMap集合,键是学生对象(Student),值是居住地 (String)。存储多个元素,并遍历。
- 要求保证键的唯一性:如果学生对象的成员变量值相同,我们就认为是同一个对象
-
代码实现
学生类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@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);
}
}
测试类
public class HashMapDemo {
public static void main(String[] args) {
HashMap<Student, String> hm = new HashMap<Student, String>();
Student s1 = new Student("林青霞", 30);
Student s2 = new Student("张曼玉", 35);
Student s3 = new Student("王祖贤", 33);
Student s4 = new Student("王祖贤", 33);
//把学生添加到集合
hm.put(s1, "西安");
hm.put(s2, "武汉");
hm.put(s3, "郑州");
hm.put(s4, "北京");
Set<Student> keySet = hm.keySet();
for (Student key : keySet) {
String value = hm.get(key);
System.out.println(key.getName() + "," + key.getAge() + "," + value);
}
}
}
三、TreeMap集合
3.1 TreeMap集合概述和特点【理解】
- TreeMap底层是红黑树结构
- 依赖自然排序或者比较器排序,对键进行排序
- 如果键存储的是自定义对象,需要实现Comparable接口或者在创建TreeMap对象时候给出比较器排序规则
3.2 TreeMap集合应用案例【应用】
-
案例需求
- 创建一个TreeMap集合,键是学生对象(Student),值是籍贯(String),学生属性姓名和年龄,按照年龄进行排序并遍历
- 要求按照学生的年龄进行排序,如果年龄相同则按照姓名进行排序
-
代码实现
学生类
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
//按照年龄进行排序
int result = o.getAge() - this.getAge();
//次要条件,按照姓名排序。
result = result == 0 ? o.getName().compareTo(this.getName()) : result;
return result;
}
}
测试类
public class TreeMapDemo {
public static void main(String[] args) {
TreeMap<Student,String> tm = new TreeMap<>();
Student s1 = new Student("zhangsan",23);
Student s2 = new Student("lisi",22);
Student s3 = new Student("wangwu",22);
Student s4 = new Student("zhangsan",21);
tm.put(s1,"江苏");
tm.put(s2,"北京");
tm.put(s3,"天津");
tm.put(s4, "武汉");
// 遍历TreeMap集合,打印每个学生的信息
tm.forEach(
(Student key, String value) -> {
System.out.println(key + "---" + value);
}
);
}
}
四、可变参数
在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.
- 格式:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
-
**底层:**其实就是一个数组
-
**好处:**在传递数据的时候,省的我们自己创建数组并添加元素了,JDK底层帮我们自动创建数组并添加元素了
-
代码演示:
public class ChangeArgs {
public static void main(String[] args) {
int sum = getSum(8, 10, 5, 13, 2300);
System.out.println(sum);
}
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
- 注意:
1.一个方法只能有一个可变参数
2.如果方法中有多个参数,可变参数要放到最后。
- 应用场景: Collections
在Collections中也提供了添加一些元素方法:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。
public class CollectionsDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//原来写法
list.add(9);
list.add(14);
list.add(8);
list.add(320);
System.out.println(list); //[9, 14, 8, 320]
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 15, 14, 7, 310);
System.out.println(list);
}
}
五、Collections类
5.1 Collections常用功能
java.utils.Collections
是集合工具类,用来对集合进行操作。常用方法如下:
public static void shuffle(List<?> list)
:打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
代码演示:
public class CollectionsDemo1 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//原来写法
list.add(9);
list.add(8);
list.add(320);
list.add(14);
System.out.println(list); //[9, 8, 320, 14]
//将集合中元素按照默认的自然规则排序
Collections.sort(list);
System.out.println(list); //[8, 9, 14, 320]
//打乱集合顺序
Collections.shuffle(list);
System.out.println(list); //[320, 8, 14, 9]
}
}
我们的集合按照默认的自然顺序进行了排列,如果想要指定顺序那该怎么办呢?
5.2 Comparator比较器
如果希望根据指定规则对集合进行排序,可使用public static <T> void sort(List<T> list,Comparator<? super T> )
方法
Student类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类
public class StudentSort {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("Rose",18));
list.add(new Student("Jack",16));
list.add(new Student("LiHua",20));
list.add(new Student("Jenny",18));
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//以学生的年龄升序,如果年龄一样 就按名字升序
int result = o1.getAge() - o2.getAge();
result = result==0 ? o1.getName().compareTo(o2.getName()) : result;
return result;
}
});
for (Student stu : list) {
System.out.println(stu);
}
}
}
结果输出
Student{name=‘Jack’, age=16}
Student{name=‘Jenny’, age=18}
Student{name=‘Rose’, age=18}
Student{name=‘LiHua’, age=20}
六、综合练习
练习1:随机点名器
需求:班级里有N个学生,实现随机点名器
public class Test1 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list,"范闲","范建","范统","杜子腾","杜琦燕","宋合泛","侯笼藤","朱益群","朱穆朗玛峰","袁明媛");
//法一
Random r = new Random();
int index = r.nextInt(list.size());
String name = list.get(index);
System.out.println(name);
//法二 打乱顺序
Collections.shuffle(list);
String name1 = list.get(0);
System.out.println(name1);
}
}
练习2:带概率的随机
需求:
班级里有N个学生
要求在随机的时候,70%的概率随机到男生,30%的概率随机到女生
public class Test2 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Collections.addAll(list,1,1,1,1,1,1,1);
Collections.addAll(list,0,0,0);
//打乱集合中的数据
Collections.shuffle(list);
//从list集合中随机抽取0或者1
Random r = new Random();
int index = r.nextInt(list.size());
int number = list.get(index);
System.out.println(number);
//创建两个集合分别存储男生和女生的名字
ArrayList<String> boyList = new ArrayList<>();
ArrayList<String> girlList = new ArrayList<>();
Collections.addAll(boyList,"范闲","范建","范统","杜子腾","宋合泛","侯笼藤","朱益群","朱穆朗玛峰");
Collections.addAll(girlList,"杜琦燕","袁明媛","李猜","田蜜蜜");
//判断此时是从boyList里面抽取还是从girlList里面抽取
if(number == 1){
//boyList
int boyIndex = r.nextInt(boyList.size());
String name = boyList.get(boyIndex);
System.out.println(name);
}else{
//girlList
int girlIndex = r.nextInt(girlList.size());
String name = girlList.get(girlIndex);
System.out.println(name);
}
}
}
练习3:随机不重复
需求:
班级里有N个学生,被点到的学生不会再被点到。但是如果班级中所有的学生都点完了, 需要重新开启第二轮点名。
public class Test3 {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "范闲", "范建", "范统", "杜子腾", "杜琦燕", "宋合泛", "侯笼藤", "朱益群", "朱穆朗玛峰", "袁明媛");
//创建一个临时的集合,用来存已经被点到学生的名字
List<String> list2 = new ArrayList<>();
//外循环:表示轮数
for (int i = 0; i < 10; i++) {
System.out.println("=========第" + i + "轮点名开始了======================");
int count = list1.size();
Random r = new Random();
//内循环:每一轮中随机循环抽取的过程
for (int j = 0; j < count; j++) {
int index = r.nextInt(list1.size());
String name = list1.remove(index);
list2.add(name);
System.out.println(name);
}
//此时表示一轮点名结束
//list1 空了 list2 10个学生的名字
list1.addAll(list2);
list2.clear();
}
}
}
练习4:集合的嵌套
需求:
定义一个Map集合,键用表示省份名称province,值表示市city,但是市会有多个。
添加完毕后,遍历结果格式如下:
江苏省 = 南京市,扬州市,苏州市,无锡市,常州市
湖北省 = 武汉市,孝感市,十堰市,宜昌市,鄂州市
河北省 = 石家庄市,唐山市,邢台市,保定市,张家口市
public class Test4 {
public static void main(String[] args) {
Map<String, ArrayList<String>> hm = new HashMap<>();
//创建单列集合存储市
ArrayList<String> city1 = new ArrayList<>();
city1.add("南京市");
city1.add("扬州市");
city1.add("苏州市");
city1.add("无锡市");
city1.add("常州市");
ArrayList<String> city2 = new ArrayList<>();
city2.add("武汉市");
city2.add("孝感市");
city2.add("十堰市");
city2.add("宜昌市");
city2.add("鄂州市");
ArrayList<String> city3 = new ArrayList<>();
city3.add("石家庄市");
city3.add("唐山市");
city3.add("邢台市");
city3.add("保定市");
city3.add("张家口市");
//把省份和多个市添加到map集合
hm.put("江苏省",city1);
hm.put("湖北省",city2);
hm.put("河北省",city3);
Set<Map.Entry<String, ArrayList<String>>> entries = hm.entrySet();
for (Map.Entry<String, ArrayList<String>> entry : entries) {
//entry依次表示每一个键值对对象
String key = entry.getKey();
ArrayList<String> value = entry.getValue();
StringJoiner sj = new StringJoiner(", ","","");
for (String city : value) {
sj.add(city);
}
System.out.println(key + " = " + sj);
}
}
}
参考黑马程序员相关视频与笔记、【查漏补缺】Java 集合详解!