🔥作者主页:小林同学的学习笔录
🔥mysql专栏:小林同学的专栏
目录
有问题可以评论讨论哦!!!
1.Collection集合
1.1 数组和集合的区别
相同点:
- 都是容器,可以存储多个数据
不同点:
-
数组的长度是不可变的,集合的长度是可变的
-
数组可以存基本数据类型和引用数据类型
集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类
1.2 集合类体系结构
1.3 Collection 集合概述和使用
Collection集合概述:
-
是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素
-
JDK 不提供此接口的任何直接实现.它提供更具体的子接口(如Set和List)实现
创建Collection集合的对象:
-
多态的方式
-
具体的实现类ArrayList
Collection集合常用方法:
1.4 Collection集合的遍历
1.4.1 迭代器遍历
迭代器介绍:
-
迭代器,集合的专用遍历方式
-
Iterator<E> iterator(): 返回此集合中元素的迭代器,通过集合对象的iterator()方法得到
Iterator中的常用方法:
- boolean hasNext(): 判断当前位置是否有元素可以被取出
-
E next(): 获取当前位置的元素,将迭代器对象移向下一个索引位置
-
void remove(): 删除迭代器对象当前指向的元素
1.4.2 增强for
- 介绍
-
它是JDK5之后出现的,其内部原理是一个Iterator迭代器
-
实现Iterable接口的类才可以使用迭代器和增强for
-
简化数组和Collection集合的遍历
-
2.List集合
2.1 List集合的概述和特点
List集合的概述:
-
有序集合,这里的有序指的是存取顺序
-
用户可以精确控制列表中每个元素的插入位置,用户可以通过整数索引访问元素,并搜索列表中的元素
-
与Set集合不同,列表通常允许重复的元素
List集合的特点:
-
存取有序
-
可以重复
-
有索引
2.2 List集合的特有方法
方法介绍:
代码演示:
2.3 List集合的五种遍历方式
2.3.1 遍历方式
-
迭代器
-
列表迭代器
-
增强for
-
Lambda表达式
-
普通for循环
代码演示:
//创建集合并添加元素
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//1.迭代器
/*Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}*/
//2.增强for
//下面的变量s,其实就是一个第三方的变量而已。
//在循环的过程中,依次表示集合中的每一个元素
/* for (String s : list) {
System.out.println(s);
}*/
//3.Lambda表达式
//forEach方法的底层其实就是一个循环遍历,依次得到集合中的每一个元素
//并把每一个元素传递给下面的accept方法
//accept方法的形参s,依次表示集合中的每一个元素
//list.forEach(s->System.out.println(s) );
//4.普通for循环
//size方法跟get方法还有循环结合的方式,利用索引获取到集合中的每一个元素
/*for (int i = 0; i < list.size(); i++) {
//i:依次表示集合中的每一个索引
String s = list.get(i);
System.out.println(s);
}*/
// 5.列表迭代器
//获取一个列表迭代器的对象,里面的指针默认也是指向0索引的
//额外添加了一个方法:在遍历的过程中,可以添加元素
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
String str = it.next();
if("bbb".equals(str)){
//qqq
it.add("qqq");
}
}
System.out.println(list);
2.3.2 遍历选取
2.4 细节点注意
List系列集合中的两个删除的方法:
- 1.直接删除元素
- 2.通过索引进行删除
代码演示:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 删除元素值为"B"的元素
list.remove("B");
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 删除索引为1的元素,即删除元素"B"
list.remove(1);
3.数据结构
3.1 数据结构之栈和队列
栈:
先进后出
队列:
先进先出
3.2 数据结构之数组和链表
数组:
查询快、增删慢
单向链表:
查询慢、增删快
链表中的结点是独立的对象,在内存是不连续的,每个结点包含数据值和下一个结点的地址值。单向链表查询慢的原因是无论查询哪个数据都要从头开始找。优化的话可以用双向链表,因为双向链表可以判断想要查的数据离开头近还是结尾近。
双向链表:
4.源码分析
4.1 ArrayList的源码
底层是数组结构
核心步骤:
-
创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。
数组名字:elementDate,定义变量size。
size这个变量有两层含义: ①:元素的个数,也就是集合的长度 ②:下一个元素的存入位置
-
添加元素,添加完毕后,size++
扩容时机一:
-
当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。
扩容时机二:
-
一次性添加多个数据,扩容1.5倍不够,怎么办呀?
如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。
举个例子: 在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,怎么办?
此时新数组的长度,就以实际情况为准,就是110
添加一个元素时的扩容:
添加多个元素时的扩容:
4.2 LinkedList源码
底层是双向链表结构
核心步骤:
-
刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null
-
添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值
-
添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值
添加第一个元素:
添加第二个元素:
添加第三个元素:
4.3 迭代器源码
迭代器遍历相关的三个方法:
-
Iterator<E> iterator() :获取一个迭代器对象
-
boolean hasNext() :判断当前指向的位置是否有元素
-
E next() :获取当前指向的元素并移动指针
hashNext()方法:
next()方法:
5.List集合的实现类
5.1 List集合子类的特点
-
ArrayList集合
底层是数组结构实现,查询快、增删慢
-
LinkedList集合
底层是链表结构实现,查询慢、增删快
5.2 LinkedList集合的特有方法
6.数据结构
6.1 树
6.1.1 二叉树
二叉树的特点:
-
二叉树中,任意一个节点的度要小于等于2
-
节点: 在树结构中,每一个元素称之为节点
-
度: 每一个节点的子节点数量称之为度、
-
树高:树的总层数
-
根节点:最顶层的节点
-
左子树(Left Subtree):左子树是指该节点的左侧所有子节点的集合。
-
右子树(Right Subtree):右子树是指该节点的右侧所有子节点的集合。
-
每一个节点的结构:
6.1.2 二叉查找树
二分查找树在需要频繁进行查找操作,并且希望保持数据有序的情况下是非常适用的。而普通的二叉树则更适用于不需要有序性要求的情况,或者是更关注插入和删除操作的性能。
特点:
- 每一个节点上最多有两个子节点
- 任意节点左子树上的值都小于当前节点
- 任意节点右子树上的值都大于当前节点
规则:
- 添加节点,小的存左边,大的存右边,一样的不存
弊端:可能出现退化为链表的情况,类似于二分查找树,如果插入的顺序不合适,二叉查找树也可能会退化为链表。这种情况下,树的高度接近于节点数量,导致查找、插入和删除操作的时间复杂度变为 O(n),而不再是理想的 O(log n)。这些弊端意味着在某些特定情况下,二叉查找树可能不太适合,需要考虑其他数据结构或者对其进行优化,比如使用平衡二叉树(如AVL树、红黑树)
6.1.3 二叉树的四种遍历方式
前序遍历:从根节点开始,然后按照当前节点,左子节点,右子节点的顺序遍历
中序遍历:从最左边的子节点开始,然后按照左子结点,当前节点,右子节点的顺序遍历
中序遍历常用于按顺序输出树中的元素
后序遍历:从最左边的子节点开始,然后按照左子节点,右子节点,当前节点的顺序遍历
层序遍历:从根节点开始一层一层的遍历
6.1.4 平衡二叉树
平衡二叉树是一种特殊的二叉查找树,旨在防止树的高度过于高而导致效率低的问题,保持树的高度平衡。
规则:
- 任意节点左右子树高度差不超过1
- 添加节点,小的存左边,大的存右边,一样的不存
不是平衡二叉树:
平衡二叉树:
平衡二叉树的旋转:
旋转触发时机
-
当添加一个节点之后,该树不再是一颗平衡二叉树,就需要相应的旋转机制把二叉树重新变成平衡二叉树
支点
- 从添加的节点开始,不断往父节点找不平衡的节点
左旋
- 先确定支点
- 就是将根节点的右侧往左拉,把支点降级为左子节点,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点
右旋
- 先确定支点
- 就是将根节点的左侧往右拉,把支点降级为右子节点,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点
平衡二叉树旋转的四种情况:
左左
-
左左: 当根节点左子树的左子树有节点插入,导致二叉树不平衡
-
如何旋转: 直接对整体进行右旋即可,只需要旋转一次
左右
-
左右: 当根节点左子树的右子树有节点插入,导致二叉树不平衡
-
如何旋转: 先在左子树对应的节点位置进行左旋,在对整体进行右旋。一般需要旋转多次,先旋转局部再全局旋转
先看问题:
一次右旋转完还是不平衡,因此需要局部左旋,再全局右旋
先局部左旋:
再全局右旋:
右右
-
右右: 当根节点右子树的右子树有节点插入,导致二叉树不平衡
-
如何旋转: 直接对整体进行左旋即可,只需要旋转一次
右左
-
右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡
-
如何旋转: 先在右子树对应的节点位置进行右旋,在对整体进行左旋
先看问题:
一次左旋转完还是不平衡,因此需要局部右旋,再全局左旋
局部右旋:
全局左旋:
弊端:平衡二叉树可能会导致过多的旋转操作,从而降低性能。
6.1.5 红黑树
红黑树是一种特殊的二叉查找树,之前也被称为“平衡二叉B树”,高度不平衡,但是有特有的红黑规则。
红黑树的节点:
红黑规则:
- 每一个节点或是红色的,或者是黑色的
- 根节点必须是黑色
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
- 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况)
- 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
简单路径:意思是只能从头往后面走,不能从后面往前走
红黑树添加节点的默认颜色:
-
添加节点时,默认为红色,效率高
红黑树添加节点后如何保持红黑规则:
实例:
根据以上面添加节点的规则结果:
7.Set集合
7.1 Set集合的概述和特点
概述:Set集合是一种常见的数据结构,用于存储不重复的元素。每个元素在集合中只能出现一次,如果尝试将重复的元素添加到集合中,则会被忽略。add()方法中,重复了会返回false。
特点:
无序:存取顺序不一致
不重复:可以去除重复
无索引:没有带索引的方法,所以不能用普通for循环遍历,也不能通过索引获取元素
7.2 Set集合的实现类
HashSet:无序,不重复,无索引
LinkedHashSet:有序,不重复,无索引
TreeSet:可排序,不重复,无索引
7.3 HashSet
原理:底层数据结构是哈希表,哈希表是一种对于增删改查数据性能都较好的结构
哈希表组成:
JDK8之前:数组 + 链表
JDK8之后:数组 + 链表 + 红黑树
哈希值:
-
哈希值简介
是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值,哈希值通过hashCode()方法计算出来的
-
如何获取哈希值
Object类中的public int hashCode():返回对象的哈希码值
-
哈希值的计算
-
int index = (数组长度 - 1) & 哈希值;
-
-
哈希值的特点
-
如果没有重写hashCode方法,不同的对象计算出的哈希值是不同的
-
如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
-
在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。这种称为"哈希碰撞"。
-
JDK版本不同,底层添加数据的方式也有差异:
注意:
1.当链表的长度大于8并且数组长度大于等于64,该链表会升级为红黑树
2.如果HashSet集合泛型是自定义对象,那么一定要重写hashCode()和equals()方法,不然不会去重
3.String和Integer这两个包装类底层已经重写了hashCode()和equals()方法,因此不需要再重写
1.HashSet为什么存和取的顺序不一样?
当你向 HashSet 中添加元素时,元素被哈希化并存储在内部的哈希表中,这个过程并不考虑元素的顺序。
2.HashSet为什么没有索引?
HashSet 是基于哈希表实现的,它通过哈希值来存储和定位元素,而不是使用索引。
3.HashSet是利用什么机制保证数据的去重的?
HashSet 通过哈希表的特性来保证数据的去重。当你向 HashSet 中添加元素时,HashSet 首先计算元素的哈希码,并检查哈希表中是否已经存在相同哈希码的元素。如果存在相同哈希码的元素,则会调用 equals() 方法来比较这些元素是否相等,如果相等则不添加,否则添加新元素。这样就保证了 HashSet 中不会存在重复的元素。
HashSet底层就是HashMap,因此源码看下面的HashMap就可以
7.4 LinkedHashSet
原理:底层数据结构是哈希表,只是每个元素额外的多了个双向链表机制记录存储的顺序,因此
LinkedHashSet存和取可以做到有序。
虽然LinkedHashSet可以做到既有序又可以去重,但是因为它还维护了一个双向链表也导致效率比较低。
在以后如果要数据去重,我们使用哪个?
默认使用HashSet
如果既要有序又要去重,再考虑LinkedHashSet
7.5 TreeSet
原理:TreeSet集合底层是基于红黑树的数据结构进行排序的,增删改查性能都较好
可排序:按照元素的默认规则(从小到大)排序
TreeSet集合的默认排序规则:
- 对于数字类型:Integer,Double默认按照从小到大的顺序进行排序
- 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序
排序的构造方法:
-
TreeSet():根据其元素的自然排序进行排序
-
TreeSet(Comparator comparator) :根据指定的比较器进行排序
7.5.1 自然排序/默认排序Comparable
-
案例需求
-
存储学生对象并遍历,创建TreeSet集合使用无参构造方法
-
要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
-
-
实现步骤
- 1.使用空参构造创建TreeSet集合
- 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
- 2.自定义的Student类实现Comparable接口
- 自然排序,就是让元素所属的类实现Comparable<E>接口,重写compareTo(T o)方法
- 3.重写接口中的compareTo方法
- 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
- compareTo方法返回值
-
1.如果返回值为正数,表示当前存入的元素是较大值,存右边
-
2.如果返回值为负数,表示当前存入的元素是较小值,存左边
-
3.如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
-
compareTo()方法的参数解析:
根据年龄进行升序操作
每插入一个数据都会调用compareTo方法,来确定数据在在红黑树的位置并且按照红黑树规则进行调整,调整完成。再重新调用compareTo方法,继续上面的操作。
注意:插入第一个元素因为红黑树没有元素,直接插入到红黑树即可。所有的比较都是从根节点开始比较,实现小的在左边,大的在右边,相等的去除。
代码演示:
想看到this,和o可以自己打印一下
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 = this.age - o.age;
//次要判断条件: 年龄相同时,按照姓名的字母顺序排序
result = result == 0 ? this.name.compareTo(o.getName()) : result;
return result;
}
}
public class MyTreeSet2 {
public static void main(String[] args) {
//创建集合对象
TreeSet<Student> ts = new TreeSet<>();
//创建学生对象
Student s1 = new Student("zhangsan",28);
Student s2 = new Student("lisi",27);
Student s3 = new Student("wangwu",29);
Student s4 = new Student("zhaoliu",28);
Student s5 = new Student("qianqi",30);
//把学生添加到集合
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
//遍历集合
for (Student student : ts) {
System.out.println(student);
}
}
}
7.5.2 比较器排序Comparator
- 实现步骤
-
1.用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
-
2.比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法
-
3.重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
-
- compareTo方法返回值
-
1.如果返回值为正数,表示当前存入的元素是较大值,存右边
-
2.如果返回值为负数,表示当前存入的元素是较小值,存左边
-
3.如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
-
public class TreeSet01 {
public static void main(String[] args) {
/**
* 需求:
* 存入四个字符串,"c","ab","df","qwer"
* 按照长度排序,如果长度一样长就按照首字母排序
*/
// TreeSet<String> treeSet = new TreeSet<>(new Comparator<String>() {
// @Override
// public int compare(String o1, String o2) {
// int i = o1.length() - o2.length();
// //长度相等就进行默认排序
// i = i == 0 ? o1.compareTo(o2) : i;
// return i;
// }
// });
//用lambda表达式简化
TreeSet<String> treeSet = new TreeSet<>((String o1, String o2) -> {
int i = o1.length() - o2.length();
//长度相等就进行默认排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
});
treeSet.add("c");
treeSet.add("ab");
treeSet.add("df");
treeSet.add("qwer");
System.out.println(treeSet);
}
}
输出结果:
[c, ab, df, qwer]
7.5.3 两种比较方式总结
-
两种比较方式小结
-
自然排序: 自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序
-
比较器排序: 创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序
-
在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序
-
-
两种方式中关于返回值的规则
-
如果返回值为负数,表示当前存入的元素是较小值,存左边
-
如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
-
如果返回值为正数,表示当前存入的元素是较大值,存右边
-
8.选型总结
- 1.如果想要的集合中的元素可以重复
- 用ArrayList集合,基于数组的
- 2.如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
- 用LinkedList集合,基于链表的
- 3.如果想对集合中的元素去重
- 用HashSet集合,基于哈希表的
- 4.如果想对集合中的元素去重,而且保证存取顺序
- 用LinkedHashSet集合,基于哈希表和双链表,效率低于HashSet
- 5.如果想对集合中的元素进行排序
- 用TreeSet集合,基于红黑树。也可以用List集合实现排序
总结效率:
- HashSet 在大多数操作上的效率最高,但是它是无序的。
- LinkedHashSet 在需要保持插入顺序时非常有用,并且具有 HashSet 的快速查找特性。
- TreeSet 在需要有序集合,并且需要按顺序遍历元素时很有帮助,但是要牺牲一些性能。
9.Map
双列集合的特点:
-
1.双列集合一次需要存一对数据,分别是键和值
-
键不可以重复,值可以重复
-
键和值是一 一对应的,每个键只能找到对应的值
-
键 + 值这个整体,我们称为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”
Map常见的API:
Map是双列集合的顶级接口,它的功能是全部双列集合都可以继承使用的
put()方法的细节
可以是添加,也可以是覆盖
在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合中,并且返回null
在添加数据的时候,如果键已经存在,那么会把原来的键值对对象覆盖,并且会把被覆盖的值进行返回
remove()方法细节
根据键删除键值对对象,并且返回被删除的键值对对象的值
遍历Map集合:
1.键找值:调用keySet()方法,返回所有键,然后在循环调用get()获取值
2.键值对:调用entrySet()方法,返回所有键值对对象,然后在循环获取
3.lambda表达式进行遍历(forEach(key,values) -> { 具体操作 } );、
9.1 HashMap
特点:
-
HashMap的特点都是有键决定的:无序、不重复、无索引
-
HashMap底层是哈希表结构
-
依赖hashCode方法和equals方法保证键的唯一
-
如果键要存储的是自定义对象,需要重写hashCode和equals方法
哈希表组成
JDK8之前:数组 + 链表
JDK8之后:数组 + 链表 + 红黑树
9.2 LinkedHashMap
LinkedHashMap的特点和LinkedHashSet都是有键决定的:有序、不重复、无索引
原理:底层数据结构是哈希表,只是每个键值额外的多了个双向链表机制记录存储的顺序,因此
LinkedHashSet存和取可以做到有序。
9.3 TreeMap
特点:
- TreeMap跟TreeSet底层原理一样,都是红黑树结构
- 由键决定:可排序,不重复,无索引
- 可排序:对键进行排序
两种排序规则:
实现Comparable接口,指定比较规则
创建集合是传递Comparators比较器对象,指定比较规则
10.源码分析
10.1 HashMap源码
可以复制下面代码到NodePad++进行查看,效果更直观
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.添加元素//执行这句话后,其实就是进行加载因子的赋值,但是并没有创建数组,因此此时table为null
HashMap<String,Integer> hm = new HashMap<>();//运行这一句才会创建一个默认数组长度为16的数组
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) {
//定义一个局部变量,用来记录哈希表中数组的地址值。
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){// 2.1数组位置为null
//底层会创建一个键值对对象,直接放到数组当中
tab[i] = newNode(hash, key, value, null);
}else {//2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
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方法比较内部的属性值是否相同// 2.3数组位置不为null,键重复,元素覆盖
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) {// 2.3数组位置不为null,键重复,元素覆盖
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
//等号的右边:当前要添加的值
//等号的左边:0x0044的值//这里需要注意的是覆盖其实只是把值覆盖,并不会覆盖节点
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
//threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 , 16 * 0.75 = 12,//也就是超过12就会进行扩容
if (++size > threshold){
resize();
}
//表示当前没有覆盖任何元素,返回null
return null;
}
10.2 TreeMap源码
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;
}
10.3 选型总结
- HashMap 在大多数操作上的效率最高,但是它是无序的。
- LinkedHashMap 在需要保持插入顺序或访问顺序时非常有用。
- TreeMap 在需要按顺序遍历键值对时很有帮助,但是要牺牲一些性能。
你觉得在Map集合中,java会提供一个如果键重复了,不会覆盖的put方法呢?
此时putIfAbsent本身不重要,可以在键重复时,进行不覆盖处理。
主要是传递一个思想:
代码中的逻辑都有两面性,如果我们只知道了其中的A面,而且代码中还发现了有变量可以控制两面性的发生。
那么该逻辑一定会有B面。
习惯:
boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
int类型的变量控制,一般至少有三面,因为int可以取多个值。
11.Collections工具类
Collections是集合的一个工具类,并不是集合
常见的API:
12.可变参数
实质上是一个数组
- 语法:
- 在方法声明时,将可变参数类型放在参数列表的最后,用三个点(
...
)表示。 - 可以选择传多个值,也可以选择不传任何值
- 例如:
public void exampleMethod(String... args) { ... }
- 在方法声明时,将可变参数类型放在参数列表的最后,用三个点(
-
注意事项:
- 可变参数必须是方法的最后一个参数,一个方法最多只能有一个可变参数。
- 如果方法有多个参数,可变参数前面的参数必须明确定义类型。
- 如果方法有固定数量的参数,优先匹配固定参数的方法。