一、双列集合的特点
关键点:
一次存一对数据:(键,值)
键:key 不可以重复
值:value 可以重复
键值对:Entry,键与值构成的整体,也叫键值对对象
特点:键与值一一对应
二、Map集合
1. 常用的API
方法名称 | 说明 |
---|---|
V put(K key, V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
代码实现:
- 创建Map集合的对象
Map<String, String> map = new HashMap();
- 添加元素 put
map.put("刘备", "诸葛亮");
map.put("曹操", "郭嘉");
map.put("曹丕", "司马懿");
map.put("曹植", "杨修");
System.out.println(map);
输出为:(注:输出无序)
{曹丕=司马懿, 孙权=周瑜, 刘备=诸葛亮, 曹操=郭嘉, 曹植=杨修}
put方法的细节:添加/覆盖
在添加数据时,如果键不存在,那么直接把键值对对象添加到map集合中。如果键存在,会把原有键值对对象覆盖,并返回覆盖的值。
// “孙权”键在map中不存在,此时是添加,所以没有返回值,输出为null
System.out.println(map.put("孙权", "周瑜"));
// 添加完后,“孙权”存在,此时是覆盖,返回值old为“周瑜”
String old = map.put("孙权", "陆逊");
- 删除元素 remove
删除该键值对,并将被删除的值返回。如果键不存在,则返回null。
String a = map.remove("曹植");
System.out.println(a);
System.out.println(map);
此时输出:
杨修
{曹丕=司马懿, 孙权=陆逊, 刘备=诸葛亮, 曹操=郭嘉}
- 判断是否包含指定的键/值 containsKey/containsValue
// 判断键
boolean cz1 = map.containsKey("曹植");
System.out.println("曹植在吗 "+cz1);
//判断值
if(!map.containsValue("杨修")){
System.out.println("杨修已经死了");
}else{
System.out.println("杨修还在");
}
输出
曹植在吗 false
杨修在吗
杨修已经死了
- isEmpty
System.out.println(map.isEmpty());
输出
false
- 集合长度 size
System.out.println(map.size());
- 清空
map.clear();
输出:
{}
2. Map集合的遍历方式
1)键找值
需要的两个函数:
map.keySet() 获取所有的键,放入一个单列集合
map.get(key) 获取键对应的值
Map<String, String> map = new HashMap<>();
map.put("刘备", "诸葛亮");
map.put("孙权", "周瑜");
map.put("曹操", "郭嘉");
map.put("曹丕", "司马懿");
// 获取所有的键,放入一个单列集合
Set<String> keys = map.keySet();
//增强for
for(String key : keys){
System.out.println(key+"="+map.get(key));
}
//迭代器
Iterator<String> it = keys.iterator();
while(it.hasNext()){
String key = it.next();
System.out.println(key+"="+map.get(key));
}
//lamada表达式
keys.forEach(new Consumer<String> (){
@Override
public void accept(String s){
System.out.println(s+map.get(s));
}
}
);
2)键值对
用到函数:
map.entrySet() 获取所有键值对对象
entry.getValue()
entry.getKey()
Map<String, String> map = new HashMap<>();
map.put("刘备", "诸葛亮");
map.put("孙权", "周瑜");
map.put("曹操", "郭嘉");
map.put("曹丕", "司马懿");
Set<Map.Entry<String, String>> entries = map.entrySet();
// 增强for
for(Map.Entry<String, String> entry :entries){
System.out.println(entry.getKey()+entry.getValue());
}
// 迭代器
Iterator<Map.Entry<String, String>> it = entries.iterator();
while(it.hasNext()){
Map.Entry<String, String> entry = it.next();
System.out.println(entry.getKey()+entry.getValue());
}
// lamada
entries.forEach(new Consumer<Map.Entry<String, String>>(){
@Override
public void accept(Map.Entry<String, String> entry){
System.out.println(entry.getKey()+entry.getValue());
}
}
);
3)lamada表达式
Map<String, String> map = new HashMap<>();
map.put("刘备", "诸葛亮");
map.put("孙权", "周瑜");
map.put("曹操", "郭嘉");
map.put("曹丕", "司马懿");
map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value){
System.out.println(key+value);
}
});
// 简写
map.forEach((String key, String value) -> System.out.println(key+value));
三、HashMap
-
HashMap是Map里的一个实现类
-
没有额外需要学习的特有方法,直接使用Map里的方法就可以了
-
特点都是由键决定的:无序,不重复,无索引
-
HashMap和HashSet底层原理是一模一样的,都是哈希表结构
HashMap的底层原理
- 创建一个长度为16,默认加载因子为0.75的数组。
- 用map.put()方法添加数据:创建一个Entry对象,里面记录要添加的键和值,利用键计算出键哈希值(与值无关),计算出在数组中应存入的索引。
- 若索引处为null,直接添加进去。
- 若索引处不为null,用equals方法比较键的属性值,如果键里的数据一样,会覆盖原有的entry对象,如果键里的数据不一样,会添加新的Entry对象。
JDK8以前,新元素添加到数组中,原先的元素挂在新元素下面形成链表。
JDK8以后,新元素直接挂在下面。且链表长度超过8且数组长度>=64时,链表自动转成红黑树
总结
- HashMap底层是哈希表结构的
- 依赖hashCode方法和equals方法保证键的唯一
- 如果键存储的是自定义对象,需要重写hashCode和equals方法;如果值存储自定义对象,不需要重写hashCode和equals方法
HashMap练习一
创建一个HashMap集合,键是学生对象(Student),值是籍贯(String)。存储三个键值对元素,并遍历。要求:同姓名,同年龄认为是同一学生。
//主函数
Map<Student, String> map = new HashMap<>();
Student s1 = new Student("zhangsan", 18);
Student s2 = new Student("lisi", 19);
Student s3 = new Student("wangwu", 20);
Student s4 = new Student("wangwu", 20);
map.put(s1, "A");
map.put(s2, "B");
map.put(s3, "C");
map.put(s4, "D");
//lamada表达式
map.forEach((stu, s)-> System.out.println(stu+s));
//键值对
Set<Map.Entry<Student, String>> entries = map.entrySet();
for(Map.Entry<Student, String> entry:entries){
System.out.println(entry.getKey()+entry.getValue());
}
//键找值
Set<Student> students = map.keySet();
Iterator<Student> it = students.iterator();
while(it.hasNext()){
Student key = it.next();
System.out.println(key+map.get(key));
}
// 学生类
package a04Map;
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;
}
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 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);
}
}
HashMAP练习二
统计投票人数
某个班级80名学生,现在需要组成秋游活动,班长提供了四个景点依次是(A,B,C,D),每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
思路:
- 用随机数生成80个学生的投票
- 用动态数组(静态数组貌似也行?)存放80个学生的投票
- 用Map存放统计票数
package a05Map;
import java.util.*;
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
/*
* 某个班级80名学生,现在需要组成秋游活动
* 班长提供了四个景点依次是(A、B、C、D)
* 每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多
* */
String[] arr = {"A", "B", "C", "D"};
//动态数组存放80个学生的票
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]);
}
HashMap<String, Integer> hm = new HashMap<>();
for(String name:list){
if(hm.containsKey(name)){
//先获取当前景点已经被投票的次数
Integer count = hm.get(name);
count++;
//把新的次数添加到集合当中
hm.put(name, count);
}else{
hm.put(name, 1);
}
}
System.out.println(hm);
//求最大值
Integer m = 0;
Set<String> key = hm.keySet();
Iterator<String> it = key.iterator();
while(it.hasNext()){
String k = it.next();
if(hm.get(k) >= m){
m = hm.get(k);
}
}
//判断哪个景点的次数跟最大值一样,如果一样,打印出来
Iterator<String> it2 = key.iterator();
while(it2.hasNext()) {
String next = it2.next();
if(hm.get(next)==m){
System.out.println(next+hm.get(next));
}
}
}
}
HashMap源码底层原理
- HashMap的每个元素都是一个Entry对象(图中Node实现了Map.Entry<K, V>,Node类是HashMap的内部类)。
- 内含4个对象:
- hash :哈希值(网上没找到定义,我的理解是:通过哈希值确定该对象在数组中的位置,以方便按照哈希值进行查找)
- key:键
- value:值
- next:哈希值相同的键处于同一个数组的位置,以链表的形式串起来,通过next记录链表中下一个元素的地址值。
-
上述Node类是元素为链表结构的类,下图TreeNode是元素为红黑树结构的类:
-
内含5个对象:
- parent:父节点的地址值
- left:左子节点
- right:右子节点
- prev:(没说,不重要)
- red:red==true表示当前节点是红色,反之当前节点是黑色。
-
TreeNode类继承自LinkedHashMap的Entry<K, V>,LinkedHashMap的Entry<K, V>继承自HashMap.Node<K, V> (图13.5)
-
HashMap类的一些成员变量:
- table:HashMap的那个数组,table记录了数组的地址值,数组的元素就是Node对象
- DEFAULT_INITIAL_CAPACITY:默认初始容量,1 << 4 = 16
- 最大容量:1 << 30 = 2 ^ 30 (了解)
- 加载因子:16×0.75=12,当数组里的元素超过12时,会扩容,每次扩容为原来的2倍
- HashMap的构造函数
- 空参构造:把加载因子0.75赋值给成员变量loadFactor,此时底层数组还没创建(因为成员变量table(上图)并没有赋值,没赋值时默认初始值为null)。也就是说空参构造不会建立数组table。数组在添加元素的时候创建(图13.9)
解释一下图13.9:这是创建元素put的底层源码。putVal的参数说明:hash(key)是一个方法(图3),根据key计算hash值。onlyfAbsent = false表示重复的数据不保留,也就是key值一样时会覆盖。
- 空参构造:把加载因子0.75赋值给成员变量loadFactor,此时底层数组还没创建(因为成员变量table(上图)并没有赋值,没赋值时默认初始值为null)。也就是说空参构造不会建立数组table。数组在添加元素的时候创建(图13.9)
- 最难的部分:putVal函数解析
1.看源码之前需要了解的一些内容
Node<K,V>[] table 哈希表结构中数组的名字
DEFAULT_INITIAL_CAPACITY: 数组默认长度16
DEFAULT_LOAD_FACTOR: 默认加载因子0.75
HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值
1.2 红黑树中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色
2.添加元素
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);
添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖
//参数一:键
//参数二:值
//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
// 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的数组![请添加图片描述](https://img-blog.csdnimg.cn/b73d0dcbef1441f9b5eb7e6e86982bae.png)
//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 555
//要添加的元素: 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;
}
四、LinkedHashMap
- 由键决定:有序、不重复、无索引
- 这里的有序指的是保证存储和取出的元素顺序一致
- 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序
代码实现:
package a06LinkedHashMap;
import java.util.LinkedHashMap;
public class Test {
public static void main(String[] args) {
/*
LinkedHashMap
由键决定:有序、不重复、无索引
这里的有序指的是保证存储和取出的元素顺序一致
原理:底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序
*/
//1.创建集合
LinkedHashMap<Integer, String> lhm1 = new LinkedHashMap<>();
//2.添加元素
lhm1.put(1, "zhangsan");
lhm1.put(1, "张三"); //继承HashMap的覆盖
lhm1.put(2, "lisi");
lhm1.put(3, "wangwu");
//打印集合
System.out.println(lhm1);
LinkedHashMap<Integer, String> lhm2 = new LinkedHashMap<>();
lhm2.put(2, "lisi");
lhm2.put(1, "zhangsan");
lhm2.put(3, "wangwu");
System.out.println(lhm2);
}
}
结果
{1=张三, 2=lisi, 3=wangwu}
{2=lisi, 1=zhangsan, 3=wangwu}
五、TreeMap
- TreeMap跟TreeSet底层原理一样,都是红黑树结构。
- 由键决定特性:不重复、无索引、可排序
- 可排序:对键进行排序。
- 注意:默认按照键从小到大进行排序,也可以自己规定键的排序规则。
代码书写两种排序规则:
- 实现Comparable接口,指定比较规则。
- 创建集合时传递Comparator比较器对象,指定比较规则。
- 当Comparable与Comparator同时存在时,以Comparator为准。
三个练习
- 键:整数表示id。
值:字符串表示商品名称。
要求:按照id的升序、降序排列。 - 键:学生对象。
值:籍贯。
要求:按照学生年龄的升序排列,年龄一样按照姓名的字母排列,同姓名年龄视为同一个人。
练习1
package a07TreeMap;
import java.util.Comparator;
import java.util.TreeMap;
public class Test {
public static void main(String[] args) {
TreeMap<Integer, String> tm = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//o1表示当前要添加的元素
//o2表示已经在红黑树中存在的元素
//返回值是负数,存左边,正数存右边,按中序遍历
return o2-o1; //表示降序排序
}
});
tm.put(2, "apple");
tm.put(3, "banana");
tm.put(1, "peach");
System.out.println(tm);
}
}
练习2
方法一:在学生类中重写Comparable
package a08TreeMap;
import java.util.Objects;
public class Student implements Comparable<Student>{
String name;
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 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 int compareTo(Student o) {
//this:表示当前要添加的元素
//o:表示红黑树中已经存在的元素
//返回值:
//负数:表示当前要添加的元素是小的,存左边
//正数:表示当前要添加的元素是大的,存右边
//0:表示当前要添加的元素已经存在,舍弃
int i = this.getAge() - o.getAge();
//默认没有重写的String类型的compareTo按照每个字母ASCII码表的数字排列,如果第一个字母一样,则比较第二个,以此类推。(与长度无关)
return i==0?this.getName().compareTo(o.getName()):i;
}
}
方法二:在主函数中重写Comparator
package a08TreeMap;
import java.util.Comparator;
import java.util.TreeMap;
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan", 25);
Student s2 = new Student("lisi", 25);
Student s3 = new Student("wangwu", 25);
TreeMap<Student, String> tm = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() != o2.getAge()?o1.getAge() - o2.getAge():(o1.getName().charAt(0) - o2.getName().charAt(0));
}
});
tm.put(s1, "北京");
tm.put(s2, "上海");
tm.put(s3, "武汉");
System.out.println(tm);
}
}
练习3
需求:字符串“aababcabcdabcde”,请统计字符串中每一个字符出现的次数,并按照以下格式输出:
输出结果:a(5) b(4) c(3) d(2) e(1)
package a09TreeMap;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.function.BiConsumer;
public class Test {
public static void main(String[] args) {
String s = "aababcabcdabcde";
TreeMap<Character, Integer> tm = new TreeMap<>();
for (int i = 0; i < s.length(); i++) {
if(tm.containsKey(s.charAt(i))){
//如果键存在,值加一
int value = tm.get(s.charAt(i));
value++;
tm.put(s.charAt(i), value);
}else{
//如果键不存在,添加进去
tm.put(s.charAt(i), 1);
}
}
//遍历集合,并按照指定格式进行拼接
//StringBuilder
StringBuilder sb = new StringBuilder();
tm.forEach((character,integer)-> sb.append(character).append("(").append(integer).append(")"));
System.out.println(sb);
//StringJoiner
StringJoiner sj = new StringJoiner("","","");
tm.forEach((character,integer)-> sj.add(character+"").add("(").add(integer+"").add(")"));
}
}
TreeMap总结
- TreeMap集合特点:
- 不重复,无索引,可排序
- 底层基于红黑树实现排序,增删改查性能较好
- TreeMap集合排序的两种方式
- 实现Comparable接口,指定比较规则
- 创建集合时传递Comparator比较器对象,指定比较规则
TreeMap源码底层原理
-
TreeMap中,每一个元素都是一个Entry对象
- key:键
- value:值
- parent:父节点的地址值
- left:左子节点
- right:右子节点
- color:节点颜色 (前面不是说默认节点是红色的吗?现在为什么是黑色的:在下面put方法源码里,每添加一个结点就会用fixAfterInsertion对结点做调整,而调整的第一件事就是把节点置为红色。这里默认为黑色是因为根节点一定是黑色的,但根节点不会走fixAfterInsertion调整方法,所以在这里默认为黑色把根节点置为黑色。)
-
TreeMap成员变量
-
TreeMap空参构造
-
TreeMap带参构造
-
TreeMap的put方法
1.TreeMap中每一个节点的内部属性
K key; //键
V value; //值
Entry<K,V> left; //左子节点
Entry<K,V> right; //右子节点
Entry<K,V> parent; //父节点
boolean color; //节点的颜色
2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
//比较器对象
private final Comparator<? super K> comparator;
//根节点
private transient Entry<K,V> root;
//集合的长度
private transient int size = 0;
3.空参构造
//空参构造就是没有传递比较器对象
public TreeMap() {
comparator = null;
}
4.带参构造
//带参构造就是传递了比较器对象。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
5.添加元素
public V put(K key, V value) {
return put(key, value, true);
}
参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
true:覆盖
false:不覆盖
private V put(K key, V value, boolean replaceOld) {
//获取根节点的地址值,赋值给局部变量t
Entry<K,V> t = root;
//判断根节点是否为null
//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
if (t == null) {
//方法的底层,会创建一个Entry对象,把他当做根节点
addEntryToEmptyMap(key, value);
//表示此时没有覆盖任何的元素
return null;
}
//表示两个元素的键比较之后的结果
//结果为负数:当前节点小,存左边;结果为正数,当前节点大,存右边
int cmp;
//表示当前要添加节点的父节点
Entry<K,V> parent;
//表示当前的比较规则
//如果我们是采取默认的自然排序,那么此时comparator记录的是null,cpr记录的也是null
//如果我们是采取比较去排序方式,那么此时comparator记录的是就是比较器
Comparator<? super K> cpr = comparator;
//表示判断当前是否有比较器对象
//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
//把键进行强转,强转成Comparable类型的
//要求:键必须要实现Comparable接口,如果没有实现这个接口
//此时在强转的时候,就会报错。
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把根节点当做当前节点的父节点
parent = t;
//调用compareTo方法,比较根节点和当前要添加节点的大小关系
cmp = k.compareTo(t.key);
if (cmp < 0)
//如果比较的结果为负数
//那么继续到根节点的左边去找
t = t.left;
else if (cmp > 0)
//如果比较的结果为正数
//那么继续到根节点的右边去找
t = t.right;
else {
//如果比较的结果为0,会覆盖
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
//就会把当前节点按照指定的规则进行添加
addEntry(key, value, parent, cmp < 0);
return null;
}
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (addToLeft)
parent.left = e;
else
parent.right = e;
//添加完毕之后,需要按照红黑树的规则进行调整
fixAfterInsertion(e);
size++;
modCount++;
}
private void fixAfterInsertion(Entry<K,V> x) {
//因为红黑树的节点默认就是红色的
x.color = RED;
//按照红黑规则进行调整
//parentOf:获取x的父节点
//parentOf(parentOf(x)):获取x的爷爷节点
//leftOf:获取左子节点
while (x != null && x != root && x.parent.color == RED) {
//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
//目的:为了获取当前节点的叔叔节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//表示当前节点的父节点是爷爷节点的左子节点
//那么下面就可以用rightOf获取到当前节点的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点为红色的处理方案
//把父节点设置为黑色
setColor(parentOf(x), BLACK);
//把叔叔节点设置为黑色
setColor(y, BLACK);
//把爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//把爷爷节点设置为当前节点
x = parentOf(parentOf(x));
} else {
//叔叔节点为黑色的处理方案
//表示判断当前节点是否为父节点的右子节点
if (x == rightOf(parentOf(x))) {
//表示当前节点是父节点的右子节点
x = parentOf(x);
//左旋
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//表示当前节点的父节点是爷爷节点的右子节点
//那么下面就可以用leftOf获取到当前节点的叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//把根节点设置为黑色
root.color = BLACK;
}
6.课堂思考问题:
6.1TreeMap添加元素的时候,键是否需要重写hashCode和equals方法?
此时是不需要重写的。
6.2HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成的。
既然有红黑树,HashMap的键是否需要实现Compareable接口或者传递比较器对象呢?
不需要的。
因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的
6.3TreeMap和HashMap谁的效率更高?
如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap的效率要更高
但是这种情况出现的几率非常的少。
一般而言,还是HashMap的效率要更高。
6.4你觉得在Map集合中,java会提供一个如果键重复了,不会覆盖的put方法呢?
此时putIfAbsent本身不重要。
传递一个思想:
代码中的逻辑都有两面性,如果我们只知道了其中的A面,而且代码中还发现了有变量可以控制两面性的发生。
那么该逻辑一定会有B面。
习惯:
boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
int类型的变量控制,一般至少有三面,因为int可以取多个值。
6.5三种双列集合,以后如何选择?
HashMap LinkedHashMap TreeMap
默认:HashMap(效率最高)
如果要保证存取有序:LinkedHashMap
如果要进行排序:TreeMap
红黑树节点添加规则:
可变参数
引例:假如需要定义一个方法求和,该方法可以灵活地完成如下需求:
计算2个数据的和
计算3个数据的和
计算4个数据的和
计算n个数据的和
这样代码太多太麻烦了
这样输入数据太麻烦了
- 可变参数:方法形参的个数是可以发生变化的
格式:数据类型…参数名称(比如:int…args)
底层:可变参数底层就是一个数组,只不过不需要我们自己创建了,Java会帮我们创建好
public static void main(String[] args) {
// getSum();
// getSum(1);
System.out.println(getSum(1, 2, 3, 4, 5));
}
// 底层:可变参数底层就是一个数组
// 只不过不需要我们自己创建了,Java会帮我们创建好
public static int getSum(int...args){
System.out.println(args); //输出结果:数组的地址值 [I@3b07d329
int sum = 0;
for(int i : args){
sum += i;
}
return sum;
}
- 可变参数的小细节:
- 方法形参中只能写一个可变参数(不然没法判断哪个参数给args1哪个参数给args2)
- 方法中如果除了可变参数以外还有其他形参,那么可变参数要写在最后
- 方法形参中只能写一个可变参数(不然没法判断哪个参数给args1哪个参数给args2)
public static void main(String[] args) {
getSum(1,2,3,4,5);
}
public static int getSum(int a, int...args){
return 0;
}
- 总结:
- 可变参数本质上就是一个数组
- 作用:在形参中接收多个数据
- 格式:数据类型…参数名称
集合工具类Collections
注意区别于Collection
- java.util.Collections
- 作用:集合的工具类(主要不是集合)
- Collections常用API:
方法名称 | 说明 |
---|---|
public static <T> boolean addAll(Collection<T> c, T…elements) | 给集合c批量添加元素elements |
public static void shuffle(List<?> list) | 打乱List集合元素顺序 |
public static <T> void sort(List<T> list) | 排序 |
public static <T> void sort(List<T> list, Comparator<T> c) | 根据指定规则排序 |
public static <T> void binarySearch(List<T> list, T key) | 以二分查找法查找列表list中的元素key |
public static <T> void copy(List<T> dest, List<T> src) | 拷贝src集合中的元素到dest中 |
public static <T> int fill(List<T> list, T obj) | 将list的所有元素替换为obj |
public static <T> void max/min(Collection<T> coll) | 获取最大/最小值 |
public static <T> void swap(List<?> list, int i, int j) | 交换集合list中指定位置元素 |
综合练习
- 班里有n个学生,实现随机点名器
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入点名次数:");
int times = sc.nextInt(); //点名次数
List<Student> list = new ArrayList<>();
Student s1 = new Student("Moran", 001, 0);
Student s2 = new Student("Xiayan", 002, 0);
Student s3 = new Student("Film", 003, 1);
Student s4 = new Student("Kilo", 004, 0);
Student s5 = new Student("Nikky", 005, 1);
Collections.addAll(list, s1,s2,s3,s4,s5);
//第一种方法:用Random类随机
Random rand = new Random();
for (int i = 0; i < times; i++) {
int r = rand.nextInt(list.size());
list.get(r).print();
}
System.out.println("-----------------------");
//第二种方法:用Collections.shuffle随机
for (int i = 0; i < times; i++) {
Collections.shuffle(list);
list.get(0).print();
}
}
- 班里有n个学生,要求70%的概率点到男生,30%的概率点到女生
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入点名次数:");
int times = sc.nextInt(); //点名次数
List<Student> list = new ArrayList<>();
Student s1 = new Student("Moran", 001, 0);
Student s2 = new Student("Xiayan", 002, 0);
Student s3 = new Student("Film", 003, 1);
Student s4 = new Student("Kilo", 004, 0);
Student s5 = new Student("Nikky", 005, 1);
Student s6 = new Student("Yeeanna", 006, 1);
Collections.addAll(list, s1,s2,s3,s4,s5,s6);
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getGender() - o2.getGender();
}
});
//统计男生有多少,女生有多少
int genderMan = 0;
int genderWoman = 0;
for (int i = 0; i < list.size(); i++) {
if(list.get(i).getGender() == 0){
genderMan++;
}else {
genderWoman++;
}
}
Random rand = new Random();
int a7 = 0;
int a3 = 0;
for (int i = 0; i < times; i++) {
int r = rand.nextInt(10);
int a;
if(r <= 6){
a = rand.nextInt(genderMan);
}else{
a = rand.nextInt(genderMan, list.size());
}
list.get(a).print();
}
}
- 班里有n个学生
要求:
被点到的学生不会再被点到
如果班级中所有学生都点完了,需要重新开启第二轮点名
//方法一 参考答案
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入点名轮次:");
int times = sc.nextInt(); //点名次数
List<Student> list = new ArrayList<>();
Student s1 = new Student("Moran", 001, 0);
Student s2 = new Student("Xiayan", 002, 0);
Student s3 = new Student("Film", 003, 1);
Student s4 = new Student("Kilo", 004, 0);
Student s5 = new Student("Nikky", 005, 1);
Student s6 = new Student("Yeeanna", 006, 1);
Collections.addAll(list, s1,s2,s3,s4,s5,s6);
List<Student> temp = new ArrayList<>();
Random rand = new Random();
int count = list.size();
for (int i = 0; i < times; i++) {
System.out.println("------------第" + (i + 1) + "轮点名开始");
for (int j = 0; j < count; j++) {
int r = rand.nextInt(list.size());
list.get(r).print();
temp.add(list.get(r));
list.remove(list.get(r));
}
list.addAll(temp);
temp.clear();
}
}
//方法二 我自己写的,用一个数组表示点名状态,这个没有练习到Collections,仅供参考
public static boolean allCalled(int[] arr){
for (int i = 0; i < arr.length; i++) {
if(arr[i] == 0){
return false;
}
}
return true;
}
public static void set_zero(int[] arr){
for (int i = 0; i < arr.length; i++) {
arr[i] = 0;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入点名次数:");
int times = sc.nextInt(); //点名次数
List<Student> list = new ArrayList<>();
Student s1 = new Student("Moran", 001, 0);
Student s2 = new Student("Xiayan", 002, 0);
Student s3 = new Student("Film", 003, 1);
Student s4 = new Student("Kilo", 004, 0);
Student s5 = new Student("Nikky", 005, 1);
Student s6 = new Student("Yeeanna", 006, 1);
Collections.addAll(list, s1,s2,s3,s4,s5,s6);
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getGender() - o2.getGender();
}
});
//统计男生有多少,女生有多少
int genderMan = 0;
int genderWoman = 0;
for (int i = 0; i < list.size(); i++) {
if(list.get(i).getGender() == 0){
genderMan++;
}else {
genderWoman++;
}
}
//创建一个记录是否被点名的数组
//0表示没被点到,1表示点到了
int[] arr = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
arr[i] = 0;
}
Random rand = new Random();
while (times != 0) {
int r = rand.nextInt(list.size());
if(allCalled(arr)){
System.out.println("------已经点完一轮-------");
set_zero(arr);
}
if(arr[r] == 0){
list.get(r).print();
times--;
arr[r] = 1;
}
}
}
- 定义一个Map集合,键表示省份province,值表示市city,但市会有多个。添加完毕后,遍历结果格式如下:
辽宁省 = 沈阳市,大连市,鞍山市,辽阳市
吉林省 = 长春市,吉林市,松原市,通化市
黑龙江省 = 哈尔滨市,大庆市,佳木斯市,齐齐哈尔市
public static void main(String[] args) {
Map<String, List<String>> map = new HashMap<>();
List<String> prov1 = new ArrayList<>();
List<String> prov2 = new ArrayList<>();
List<String> prov3 = new ArrayList<>();
Collections.addAll(prov1, "沈阳市", "大连市", "鞍山市", "辽阳市");
Collections.addAll(prov2, "吉林市", "长春市", "松原市", "通化市");
Collections.addAll(prov3, "哈尔滨市", "大庆市", "佳木斯市", "齐齐哈尔市");
map.put("辽宁省", prov1);
map.put("吉林省", prov2);
map.put("黑龙江省", prov3);
Set<Map.Entry<String, List<String>>> entries = map.entrySet();
for(Map.Entry<String, List<String>> entry : entries){
StringBuilder sb = new StringBuilder();
sb.append(entry.getKey());
sb.append(" = ");
List<String> list = entry.getValue();
StringJoiner sj = new StringJoiner(",", "", "");
for (int i = 0; i < list.size(); i++) {
sj.add(list.get(i));
}
sb.append(sj);
System.out.println(sb);
}
}
项目:斗地主
- 准备牌
- 洗牌
- 发牌
- 排序
Stream流
不可变集合
- 不可变集合:不可以被修改的集合
- 应用场景:
如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践。当集合对象被不可信的库调用时,不可变形式是安全的。
不想让别人修改集合中的内容
- 创建不可变集合的书写格式
List、Set、Map接口中都存在静态的of方法,可以获取一个不可变的集合。这种集合不能添加,不能删除,不能修改。
List<E> of(E… element) -->形参是不可变参数
不可变List集合:
//不可变集合,只能进行查询操作
List<String> list = List.of("曹操", "刘备", "孙权");
//可以遍历
for (String s : list) {
System.out.print(s + " ");
}
System.out.println();
//不可以删除
list.remove("曹操"); // 会报错
//不可以添加
list.add("司马懿"); // 会报错
//不可以修改
list.set(0, "曹丕"); // 会报错
不可变Set集合:
//不可变集合,只能进行查询操作
//不可变集合,只能进行查询操作
//细节:Set集合里的元素是唯一的,所以参数一定要保证唯一性,下面的set就不能有两个曹操
Set<String> set = Set.of("曹操", "刘备", "孙权");
// Set<String> set2 = Set.of("曹操", "曹操", "刘备", "孙权");
//可以遍历
//Set集合没有索引,不能用索引遍历
for (String s : set) {
System.out.print(s + " ");
}
System.out.println();
//不可以删除
set.remove("曹操"); // 会报错
//不可以添加
set.add("司马懿"); // 会报错
不可变Map集合:
// 注:键不能重复,会报错
// 最多只能存10个键值对 (因为不可以有两个可变参数)
Map<String, String> map = ("刘备", "诸葛亮", "孙权", "周瑜", "曹操", "郭嘉");
Set<String> strings = map.keySet();
for (String string : strings) {
System.out.println(string + " : " + map.get(string));
}
// 如果要传递多个键值对对象,数量大于10个:
// 1. 创建一个普通的Map集合
Map<String, String> map2 = new HashMap<>();
map2.put("aaa","111");
map2.put("bbb","222");
map2.put("ccc","333");
map2.put("ddd","444");
map2.put("eee","555");
map2.put("fff","666");
map2.put("ggg","777");
map2.put("hhh","888");
map2.put("iii","999");
map2.put("jjj","000");
map2.put("kkk","111");
// 2. 利用上面的数据获取一个不可变集合
// 获取到所有键值对对象
Set<Map.Entry<String, String>> entries = map2.entrySet();
// 把entries变成一个数组
Map.Entry[] arr = entries.toArray(new Map.Entry[0]);
/* 把这个语句拆解一下:
Map.Entry[] arr1 = new Map.Entry[0]; //创建长度为0的Map.Entry类型的数组
entries.toArray(arr1);
*/
// toArray方法会在底层比较数组长度和集合长度的大小
// 如果集合长度 > 数组长度,数据在数组中放不下,会根据实际数据的个数,重新创建数组
// 如果集合长度 <= 数组长度,数据在数组中放得下,不会创建新的数组吗,而是直接用,剩余的默认初始化为null
// 不可变的Map集合
Map map1 = Map.ofEntries(arr);
//此时不能添加修改删除
// map1.put("曹丕", "司马懿"); //会报错
//统一成一个整体:
Map map3 = Map.ofEntries(map2.entrySet().toArray(new Map.Entry[0]));
//JDK版本的进步:直接用Map.copyOf生成大于10的不可变Map集合
Map<String, String> map4 = Map.copyOf(map2);
总结:
- 不可变集合的特点:不可以修改添加删除
- 如何创建不可变集合:List.of、Set.of、Map.of
- 细节
- List 直接用
- Set:元素不可以重复
- Map:元素不可以重复,键值对数量最多是10个。超过10个要用ofEntries()/Map.copyOf()方法