java基础-集合
9-1-Collection
01-集合与数组对比
- 数组的长度是不可变的,集合的长度是可变的。
- 数组可以存基本数据类型和引用数据类型。集合只能存引用数据类型,如果集合要存基本数据类型,需要存对应的包装类。
02-集合体系结构
03-Collection常见成员方法
Collection概述和使用:
- 是单列集合的顶层接口,他表示一组对象,这些对象也称为
Collection
的元素 - JDK不提供此接口的任何实现。提供更具体的子接口(如
List
、Set
)的实现。
创建Collection集合的对象
- 多态的方式
- 具体的实现类
ArrayList
常用成员方法:
除了removeif方法,其他方法都比较常规。
public class CollectionDemo01 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("aaaa");
list.removeIf(s->s.length()==3);
System.out.println("list = " + list);
}
}
参数是一个函数式接口,可以使用lamda表达式,会遍历整个集合,进行满足条件的就会进行删除。
解释:传入Predicate
实现类,该方法会获取集合的迭代器,遍历集合元素,并调用实现类的test
方法,逐个进行判断,如果实现类的test
方法结果为true
,则删除元素。
04-迭代器的基本使用
Collection接口中有获取迭代器对象的抽象方法:是由实现类提供具体实现。
作用:返回集合中的迭代器对象,该迭代器对象默认指向当前集合的0索引。
迭代器Iterator
中的方法:
使用迭代器遍历集合:
public class CollectionDemo03 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
// 1 获取迭代器对象。迭代器对象一旦创建出来,默认指向0索引
Iterator<String> iterator = list.iterator();
// 2 利用迭代器中的方法来遍历集合
// 迭代器当前指向位置是否有元素可以取出
boolean b = iterator.hasNext();
System.out.println(b);
// next()方法做了两件事情,1取出当前位置元素 2将迭代器往后移动一个索引的位置
System.out.println("iterator.next() = " + iterator.next());
System.out.println("iterator.next() = " + iterator.next());
System.out.println("iterator.next() = " + iterator.next());
System.out.println("iterator.next() = " + iterator.next());
System.out.println("iterator.next() = " + iterator.next());
}
}
会报错:
为什么会报错?因为只有4个元素,却调用了5次next()方法,第五次调用时,迭代器指向的位置已经超出了集合索引范围。所以报错了
为了防止报错,在遍历时需要调用hasNext()
方法,判断迭代器当前指向位置是否有元素可以取出。
public class CollectionDemo03 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
// 1 获取迭代器对象。迭代器对象一旦创建出来,默认指向0索引
Iterator<String> iterator = list.iterator();
// 2 利用迭代器中的方法来遍历集合
// 迭代器当前指向位置是否有元素可以取出
boolean b = iterator.hasNext();
System.out.println(b);
// next()方法做了两件事情,1取出当前位置元素 2将迭代器往后移动一个索引的位置
while (iterator.hasNext()){
System.out.println("iterator.next() = " + iterator.next());
}
}
}
05-迭代器原理
当迭代器指向最后一个元素时,hasNext()
方法返回是false。原理个锤子。
06-迭代器的删除方法
先写个删除方法:删除集合中字符串为“b”的元素
public class CollectionDemo04 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
for (int i=0;i<list.size();i++){
if ("b".equals(list.get(i))){
list.remove(i);
}
}
System.out.println("list = " + list);
}
}
可以看到,有个元素明明是"b"却没有被删除,当我们换一下元素位置为:abcbd时(重复元素不连续)又能删除了:
为什么?
java中的List集合是动态的,当删除一个元素后,长度会自动减1,如图,删除索引1后,长度减1,索引又移动到2了,导致漏了一个元素没有被遍历到。
如何解决?
当成功删除后,索引往前移一个位置。
如果使用迭代器,就不会存在这个问题,迭代器的删除方法,是指向谁就删除谁:
public class CollectionDemo05 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if ("b".equals(next)){
// 指向谁就删除谁
iterator.remove();
}
}
System.out.println("list = " + list);
}
}
gpt的解释:
迭代器能维护集合状态:
07-增强for
作用:简化数组和Collection集合的遍历
- jdk5以后出现的,内部是一个
Iterator
迭代器 - 实现
Iterable
接口的类才可以使用迭代器和增强for
单列集合顶层Collection
实现了Iterable
接口,而双列集合Map
没有实现该接口。
可以得出结论:单列集合都可以使用增强for,而双列集合不行。
格式:
for(元素数据类型 变量名:数组或Collection集合){
// 在此处使用变量即可,该变量就是元素
}
举例:
public class CollectionDemo06 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
// 1.数据类型一定是集合或数组中元素的类型
// 2.s仅仅是一个变量名而已,在循环的过程中,依次表示集合或数组中的每一个元素
// 3.list就是要遍历的集合或者数组
for (String s : list) {
System.out.println("s = " + s);
}
}
}
08-增强for注意点
public class CollectionDemo07 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("d");
for (String s : list) {
s="q";
System.out.println("s = " + s);
}
System.out.println("list = " + list);
}
}
运行结果:
在增强for中修改变量的值,不会影响集合中的元素
目前为止学习了三种遍历方式:
- 如果需要操作索引,使用普通for
- 如果再遍历过程中需要删除元素,使用迭代器
- 仅仅只想遍历,使用增强for
9-2-List
10-List的特点
单列集合分为List
和Set
,都实现了Collection
接口。List又分为ArrayLis
t和LinkedList
。
List
集合特点:
- 有序:存储和取出的元素顺序一致
- 有索引:可以通过索引操作元素
- 可重复:存储的元素可以重复
11-List特有方法
方法名 | 描述 |
---|---|
void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
12-栈和队列
栈
因为只有一端开口,后进先出。
队列
两端口,先进先出。
13-数组和链表
数组
查询快,增删慢。
为什么?
- 查询数据通过地址值和索引定位,查询任意数据耗时相同,查询速度快。
- 删除数据时,要将原始数据删除,同时后面每个元素前移,删除效率低。
- 添加数据时,添加位置后的每个数据右移,再添加元素,添加效率极低。
链表
图上节点中记录的是下一个元素的地址值,而不是值,图没画好。
上一个节点中记录下一个节点的地址值,形成链表。这是单链表。
对比数组,链表是一种增删快的模型。
添加删除时改变指针即可。
查询时,因为链表的元素所占内存是不连续的,所以只能从head节点开始通过指针一个个往下找,查询慢。
链表又分单向链表、双向链表、循环链表等等。
14-ArrayList源码浅读
当我们调用空参构造创建ArrayList
时,会创建一个长度为0的数组。再次向这个数组添加一个元素时,会重新创建一个长度为10的数组,所以我们默认认为ArrayList
底层数组默认长度为10,这个数组名称为elementData
。
同时还会有一个变量size
记录下一次插入时的索引值,同时也表示着当前ArrayList的元素的个数。
当元素个数超出底层数组长度时,会再次创建一个数组,长度是原来的1.5倍,(在JDK 7及以下版本中是扩大为原来的2倍),并且会把原来数组的所有元素拷贝到新的数组中,size
的值不变,然后再次插入。
查询数据:直接通过数组地址+索引获取。
看代码:
- 空参构造,底层创建一个长度为0的数组。关于
elementData
修饰符解释:点击跳转
- 向空ArrayList中添加一个元素时:
将元素、底层数组、size(当前集合元素个数)传递给如下add方法:
判断,如果当前集合元素个数等于底层数组长度,则进行扩容(调用grow方法),否则进行数组赋值,size自增。
可以看到,将size+1后传递给了带参的扩容方法,使用了Arrays
工具类的拷贝方法,将新数组赋值给了底层数组,新的数组复制了原来的所有值,且容量更大。
计算新数组长度的方法:
private int newCapacity(int minCapacity) {
// minCapacity为size+1
// overflow-conscious code
// 取底层数组长度
int oldCapacity = elementData.length;
// 计算新数组长度,即旧容量+旧容量的一半(右移移位是除以2),扩大1.5倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果计算出来的新容量小于当前size+1,则再次进行判断,否则跳到return。
// 空集合第一次添加元素,minCapacity=0+1=1,newCapacity=0+0/2=0。满足条件,即新容量小于size+1.
if (newCapacity - minCapacity <= 0) {
// 底层数组是否未默认的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 如果是,则比较默认定义好的大小(10)和minCapacity,返回大的,调试时,空集合第一次添加一个元素情况,返回DEFAULT_CAPACITY
return Math.max(DEFAULT_CAPACITY, minCapacity);
// size+1小于0的话,说明溢出了int取值范围
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 新容量小于原来的容量,且不是默认数组,则说明不需要扩容。
return minCapacity;
}
// 判断新容量是否在ArrayList允许的最大范围 int最大取值范围减8
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
// 如果超过了,就调用如下方法重新计算
: hugeCapacity(minCapacity);
}
当新计算出来的容量超过int最大值时重新计算的方法:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
为什么要多一层判断?
这是因为数组的索引从0开始,而不是1。如果MAX_ARRAY_SIZE等于Integer.MAX_VALUE,则分配一个长度为MAX_ARRAY_SIZE的数组将导致数组索引超出int类型的最大值。因此,MAX_ARRAY_SIZE需要比int类型的最大值小至少1,以确保安全使用。另外8的值可能与VM使用的一些空间大小有关。
15-LinkedList基本使用
public class CollectionDemo08 {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
for (String s : list) {
System.out.println(s);
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
16-LinkedList特有方法
public class CollectionDemo09 {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.addFirst("111");
list.addLast("222");
System.out.println("list = " + list);
System.out.println("list.getFirst() = " + list.getFirst());
System.out.println("list.getLast() = " + list.getLast());
list.removeFirst();
System.out.println("list = " + list);
list.removeLast();
System.out.println("list = " + list);
}
}
17-LinkedList源码浅读
LinkedList
底层是双向链表:
按ctrl+f12
可以查找类中成员。
可以找到Node
是LinkedList
中的一个内部类,就是节点。内部类Node
对象,就是LinkedList
中的节点对象。
private static class Node<E> {
// 数据
E item;
// 前置指针
Node<E> next;
// 后置指针
Node<E> prev;
// 全参构造
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList
中维护了两个核心的成员变量:
创建好空的LinkedList
后,链表为空,只有头结点和尾结点,此时LinkedList
对象在堆内存中如下:
且都为null
添加元素时调用:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
// 定义节点变量l 和last指向同一变量
final Node<E> l = last;
// 调用Node的带参构造,创建新节点,新节点的前置指针为l,
// 因为l是添加这个节点之前的尾节点。后置指针为null
final Node<E> newNode = new Node<>(l, e, null);
// 将新创建出来的节点设置为尾结点
last = newNode;
// 判断创建新节点前,尾结点是否为null
if (l == null)
// 是则表示第一次添加元素,赋给头结点
first = newNode;
else
// 否则将新节点作为尾节点并和添加新节点之前的尾结点连接
l.next = newNode;
size++;
modCount++;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加了一个元素后:
LinkedList
中的两个变量first
和last
都被赋值为了这个Node
对象,即新元素。图中节点都是在堆内存中,方便看,所以画在外面。
添加第二个元素时,first
不变,第二个元素变为last
,第一个元素aaa
的next被赋值为了第二个元素,也就是第一个元素的next指针指向第二个元素。同时第二个元素bbb
的prev又指向了aaa
,在代码中可以看到,调用全参构造的时候把l
传过去了。l
的作用就是记录添加元素前的尾结点,用于创建下一个节点。图中节点都是在堆内存中,方便看,所以画在外面。
以此类推,不需要扩容,动态new出来的节点,设置好指针即可。
查找元素时源码:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// 将链表长度除以2,和需要查找的索引值进行比较,判断从头结点开始找还是从尾节点开始找
if (index < (size >> 1)) {
// 从头结点开始找,则让x为LinkedList对象中的first成员,因为他记录了头节点
Node<E> x = first;
// 按指定次数移动指针,每次赋值x为x的next指向的元素
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 从头结点开始找,则让x为LinkedList对象中的last成员,因为他记录了尾节点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
// 按指定次数移动指针,每次赋值x为x的prev指向的元素
x = x.prev;
return x;
}
}
删除元素时:
public boolean remove(Object o) {
// 分两种情况,被删除的元素是null,则循环查找为null的元素,调用unlink从链表中移除
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 被删除的元素不为null,则循环查找元素,调用equals方法比较,结果为true的,调用unlink从链表中移除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
unlink方法:
E unlink(Node<E> x) {
// assert x != null;
// 记录被删除的元素的数据区
final E element = x.item;
// 记录被删除的元素的后置指针,在Java中就是后一个节点
final Node<E> next = x.next;
// 记录被删除的元素的前置指针,在Java中就是前一个节点
final Node<E> prev = x.prev;
// 如果被删除的元素的前置指针为null,说明被删除的元素是头结点
if (prev == null) {
// 设置整个链表的新的头节点为被删除的元素的下一个节点
first = next;
} else {
// 被删除的元素不是链表的头结点,则设置被删除的元素的上一个节点的next节点为被删除的元素的下一个节点
prev.next = next;
// 置空被删除的元素的前置指针
x.prev = null;
}
// 如果被删除的元素的后置指针为null,说明被删除的元素是尾结点
if (next == null) {
//设置整个链表的新的尾结点为被删除的元素的上一个节点
last = prev;
} else {
// 被删除的元素不是链表的尾结点,则设置被删除的元素的下一个节点的前置指针为被删除的元素的上一个节点
next.prev = prev;
// 被删除的元素的后置指针为null
x.next = null;
}
// 设置被删除元素的数据区为null
x.item = null;
size--;
modCount++;
return element;
}
9-3-泛型
18-概述
public class GenericsDemo01 {
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
Iterator iterator = list.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
System.out.println("next = " + next);
}
}
}
看这段代码,虽然不写泛型,也不会报错,依然正常运行:
但是获取到的对象是Object
类型,无法使用对象中的方法,要使用必须强转:
再看,如果不写泛型,默认是Object
,可以添加任意类型的对象,这时添加数字会怎么样?编译不报错,运行会报错:因为我们遍历时做了类型强转,Integer
强转String
报错了。
引出泛型:
jdk5想特性,提供了编译时类型安全检测机制。
好处:
- 把运行期间的问题提前到了编译期间。
- 避免了强制类型转换。
19-泛型类的使用
什么地方可以使用泛型?
- 类后面---泛型类
- 方法申明上---泛型方法
- 接口后面---泛型接口
平时开发最常用的泛型类就是ArrayList
:
如果一个类后面有类似<E>,表示这个类是一个泛型类。
创建泛型类对象时,必须要给这个泛型确定具体的数据类型。
比如:
ArrayList<String> list = new ArrayList<>();
就在创建集合时确定了这个集合对象存储的元素类型,存储的不是String
类型,就会在编译期间报错。
20-自定义泛型类
泛型定义的格式:
<类型>:指定一种类型格式,括号里可以任意书写,按照变量的定义规则即可,一般只写一个字母,如:
<Q> <T> <K> <V>
<类型1,类型2,...>:指定多种类型的格式,多种类型用逗号隔开
<K,V> <E,T> <Q,M>
泛型类的定义:
public class 类名<泛型类型>{}
如:
public class Generic<T>{}
// 此处T可以随便写任意标识,常见的有T、E、K、V等形式参数表示泛型
public class GenericsDemo02 {
public static void main(String[] args) {
// 泛型为String的box对象
Box<String> stringBox = new Box<>();
stringBox.setElement("给xx的土味情话");
System.out.println("stringBox = " + stringBox);
// 泛型为Integer的box对象
Box<Integer> integerBox = new Box<>();
integerBox.setElement(25);
System.out.println("integerBox = " + integerBox);
}
}
21泛型方法的使用
先来学习怎么使用泛型方法,这里拿ArrayList
中的toArray
方法演示:
public class GenericsDemo03 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 不使用泛型
Object[] array = list.toArray();
System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
// 使用泛型
String[] strings = list.toArray(new String[list.size()]);
System.out.println("Arrays.toString(strings) = " + Arrays.toString(strings));
}
}
可以看到,不使用泛型也可以正常输出,但是需要强转,使用泛型就不需要强转,返回的就是我们指定的泛型的数组。传入什么类型的数组,就返回什么类型的数组。
22-自定义泛型方法
泛型方法的定义格式:
修饰符 <类型> 返回值类型 方法名(类型 变量名){}
举例:
public <T> void show(T t){}
为什么在修饰符后面又要一个<T>,表示定义一个泛型, 不然就会认为show方法中的参数类型T是一个类或者类型。
在定义方法是,T是没有具体类型的,只有在调用时,泛型T才会有具体的类型。
练习:
定义一个泛型方法,传递一个集合和四个元素,将元素添加到集合中并返回。
public class GenericsDemo04 {
public static void main(String[] args) {
ArrayList<String> l1 = new ArrayList<>();
ArrayList<String> stringArrayList = add2List(l1, "aaa", "bbb", "ccc", "ddd");
System.out.println("stringArrayList = " + stringArrayList);
ArrayList<Integer> l2 = new ArrayList<>();
ArrayList<Integer> integerArrayList = add2List(l2, 1, 2, 3, 4);
System.out.println("integerArrayList = " + integerArrayList);
}
public static <T> ArrayList<T>add2List(ArrayList<T> list,T t1,T t2,T t3,T t4){
list.add(t1);
list.add(t2);
list.add(t3);
list.add(t4);
return list;
}
}
23-泛型接口
泛型接口的使用方式:
实现类中也不给出具体的类型, 继续沿用泛型,如
List<T>
的实现类ArrayList<T>
:实现类中给出具体的数据类型
泛型接口的定义格式:
格式:
修饰符 interface 接口名<类型>{}
举例:
public interfate Generic<T>{}
public class GenericsDemo05 {
public static void main(String[] args) {
// 创建沿用接口泛型的泛型接口实现类对象,不指定泛型,默认Object
GenericsImpl<String> objectGenerics = new GenericsImpl();
objectGenerics.print("哈哈哈");
// 创建沿用接口泛型的泛型接口实现类对象,指定泛型为Integer
GenericsImpl<Integer> integerGenerics = new GenericsImpl();
integerGenerics.print(123);
// 创建沿用接口泛型的泛型接口实现类对象,指定泛型为Double
GenericsImpl<Double> doubleGenerics = new GenericsImpl();
doubleGenerics.print(123.321);
// 创建给出具体类型的泛型接口实现类对象 实现类已经给出具体类型,只能用Integer
GenericsIntegerImpl genericsInteger = new GenericsIntegerImpl();
genericsInteger.print(321);
}
}
/**
* 泛型接口
*
* @author lzc
* @date 2023/05/11
*/
interface Generics<E> {
public abstract void print(E e);
}
/**
* 泛型impl 不给出具体类型,沿用接口的泛型
*
* @author lzc
* @date 2023/05/11
*/
class GenericsImpl<E> implements Generics<E>{
@Override
public void print(E e) {
System.out.println("e = " + e);
}
}
/**
* 泛型impl 给出具体的类型为Integer,实现泛型接口
*
* @author lzc
* @date 2023/05/11
*/
class GenericsIntegerImpl implements Generics<Integer>{
@Override
public void print(Integer integer) {
System.out.println("integer = " + integer);
}
}
24-通配符
概述:
- 类型通配符
<?>
ArrayList<?>
表示元素类型位置的ArrayList
,它的元素可以匹配任何类型- 但是并不能把元素添加到
ArrayList
中了,因为获取出来的也是父类类型
类型通配符作用:
- 类型通配符上限限定:<? extends 类型>
如:ArrayList<? extends Number>
:表示类型是Nubmber
或者其子类型。
- 类型通配符下限限定:<? extends 类型>
public class GenericsDemo06 {
public static void main(String[] args) {
ArrayList<Number> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
ArrayList<Object> list3 = new ArrayList<>();
method1(list1);
method1(list2);
// 限制元素? extends Number,传入Object报错,Object不是Number的子类
method1(list3);
method2(list1);
// 限制元素? super Number,传入Integer报错,Integer不是Number的父类,而是子类
method2(list2);
method2(list3);
}
public static void method1(ArrayList<? extends Number> list){
// ? extends Number 表示集合中的元素是Number或者Number的子类
}
public static void method2(ArrayList<? super Number> list){
// ? super Number 表示集合中的元素是Number或者Number的父类
}
}
9-4~9-5-Set、红黑树
01~02-概述和基本使用
单列集合Collection
分为List
和Set
,Set
又分为HashSet
和TreeSet
。Set
集合最主要的特点就是不可重复,所有可以去除重复。
特点:
- 元素不可重复
- 元素存取顺序不一致
- 没有带索引的方法,不能通过普通for来遍历,也不能通过索引获取、删除元素
基本使用:
实现类有TreeSet
和HashSet
,没有索引:
public class CollectionDemo10 {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("aaa");
set.add("bbb");
set.add("ccc");
set.add("ddd");
// 没有索引,所以通过迭代器来遍历
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
System.out.println("iterator.next() = " + iterator.next());
}
// 增强for原理也是迭代器,Collection实现了Iterable,所以可以使用增强for来遍历
for (String s : set) {
System.out.println("s = " + s);
}
}
}
为了验证元素不重复和存取顺序不一致。稍微调整:
03-TreeSet基本使用
TreeSet
集合特点:
- 不包含重复元素的集合
- 没有带索引的方法
- 可以将元素按照规则排序
public class CollectionDemo11 {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
set.add(1);
set.add(5);
set.add(3);
set.add(4);
set.add(2);
set.add(2);
System.out.println("set = " + set);
}
}
从结果中可以看到,已经帮我们排序了。
那么自定义类型能排序吗?
自定义Student
类,创建几个对象,测试能否帮我们排序:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private Integer age;
}
public class CollectionDemo12 {
public static void main(String[] args) {
TreeSet<Student> set = new TreeSet<>();
set.add(new Student("小敏",15));
set.add(new Student("小红",16));
set.add(new Student("小明",19));
set.add(new Student("小花",23));
set.add(new Student("小赖",25));
System.out.println("set = " + set);
}
}
运行报错了:
为什么报错?
TreeSet
可以按照规则排序,必须要指定排序规则。
04-TreeSet自然排序Comparable使用
- 使用空参构造创建
TreeSet
集合 - 自定义
Student
类实现Comparable
接口 - 重写里面的
compareTo
方法
文档:
修改Student
类,实现Comparable
接口,重写compareTo
方法:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Comparable<Student>{
/**
* 名字
*/
private String name;
/**
* 年龄
*/
private Integer age;
@Override
public int compareTo(Student o) {
// 按照对象年龄进行排序
return this.age-o.age;
}
}
再次运行,可以看到已经按照年龄大小将Student
对象进行排序了:
自然排序简单原理:
compareTo
返回负数,表示当前存入的值是较小值,放左边。compareTo
返回0,表示当前存入的值跟已有元素重复,不存。compareTo
返回正数,表示当前存入的值是较大值,放右边。
假设现在TreeSet
中已有一个元素,age为28,再存一个age为27的Student
对象,则调用compareTo
进行比较,this就是当前要存的对象,比较后,27-28返回负数,说明要放在左边。
再存一个age为29的Student
对象,compareTo
,29-27=2返回了正数,说明要放在右边
发现右边还有元素,再比较一次,29-28=1返回正数,说明要放右边,得到最终结果。
05-自然排序练习
需求:Student
存入TreeSet
,按照年龄排序,如果年龄一样,则按照姓名首字母排序,如果姓名年龄都一样,则认为重复,不存入了。
先看一个现象:上面代码,增加一个年龄、姓名不一样的Student
对象,再输出TreeSet
,发现,后面添加的对象没有存入。
为什么会这样?
按照自然排序原理以及我们重写的compareTo
方法可以知道,当age相同时,会认为TreeSet
中元素重复,不进行存入,显然是不符合需求的。
所以需要修改我们重写的compareTo
方法:
@Override
public int compareTo(Student o) {
// 按照对象年龄进行排序 this对象(要存入的对象)的年龄-参数对象(当前比较对象)o的年龄
int result = this.age-o.age;
result = result==0?this.getName()
// 这个方法是String类中重写的,按照字典顺序进行比较
.compareTo(o.getName()):result;
return result;
}
再次运行:
为了验证先按年龄排序,如果年龄相同就再按照姓名首字母排序,修改姓名为英文的:
String
中compareTo
方法是逐个比较ASCII值,相同则比较下一个字符,不相同则返回ASCII码值的差。
06-比较器排序
需求:使用比较器的方式,将老师对象存入TreeSet
集合,按年龄排序,如果年龄一样,则按照姓名首字母排序。
当没有比较器时,添加元素到集合会报错
public class CollectionDemo13 {
public static void main(String[] args) {
TreeSet<Teacher> treeSet = new TreeSet<>();
Teacher t1 = new Teacher("zhangsan", 23);
Teacher t2 = new Teacher("lisi", 22);
Teacher t3= new Teacher("wangwu", 24);
Teacher t4 = new Teacher("zhaoliu", 24);
treeSet.add(t1);
treeSet.add(t2);
treeSet.add(t3);
treeSet.add(t4);
}
}
看构造函数:
构造函数可以传入比较器的实现类对象,查看比较器:
可以使用匿名内部类或者lamda表达式进行重写,修改代码:
public class CollectionDemo13 {
public static void main(String[] args) {
TreeSet<Teacher> treeSet = new TreeSet<>((o1, o2) -> {
int result = o1.getAge() - o2.getAge();
result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
return result;
});
Teacher t1 = new Teacher("zhangsan", 23);
Teacher t2 = new Teacher("lisi", 22);
Teacher t3 = new Teacher("wangwu", 24);
Teacher t4 = new Teacher("zhaoliu", 24);
treeSet.add(t1);
treeSet.add(t2);
treeSet.add(t3);
treeSet.add(t4);
System.out.println("treeSet = " + treeSet);
}
}
运行可以看到,结果满足要求。按照年龄进行排序了,且年龄相同的就按名字首字母排序。
07-两种比较方式的对比
- 自然排序:自定义类实现
Comparable
接口,重写CompareTo
方法, 根据返回值排序。 - 比较器排序:创建
TreeSet
对象的时候传递Comparator
的实现来对象,重写CompareTo
方法, 根据返回值排序。
两种比较方式都是一样,按找第四节课的比较器原理来插入元素。
有什么区别?
在使用的时候,默认使用自然排序,当自然排序不满足需求时,就使用比较器排序。比如:
要按照字符串长度排序,如果字符串长度一样,则按照字典排序,但是String
类已经实现了自然排序接口,我们又不能改String
类的源码。这时就需要使用比较器排序,在创建TreeSet
对象时传入比较器实现类对象。
TreeSet<String> set = new TreeSet<>(((o1, o2) -> {
int result = o1.length() - o2.length();
result = result == 0 ? o1.compareTo(o2) : result;
return result;
}));
08-二叉树
树节点每个节点的子节点数称为度。
二叉树:任意节点的度小于等于2。
根节点的左子树、右子树是什么?
09-二叉查找树
二叉查找树又称二叉排序树或二叉搜索树,特点如下:
- 每个节点最多有两个节点,即二叉树
- 每个节点的左子节点都小于自己
- 每个节点的右节点都大于自己
10-二叉查找树添加节点
每次添加节点时从根节点开始比较,决定放在左边还是右边。一直递归这个操作。
参考:点击跳转
11-平衡二叉树
将7、10、11、12、13依次添加到二叉查找树中,最终得到的结果如下:
会发现,拉链过长,变成了链表,查询效率太低,因为二叉查找树也是从根节点开始找的,如果要查13,那么这个树要查5次。要提高查询效率,要在二叉查找树的基础上,让左右子树的高度尽量相同。
平衡二叉树应该满足以下要求:
- 二叉树左右子树高度差不超过1
- 任意节点的左右子树都是一颗平衡二叉树
节点10不满足要求,左子树高度0,右子树高度3,超过1。
12-平衡二叉树左旋
先说说平衡二叉树的引出:
二叉树查找效率低,要一个个遍历。
所以有了二叉查找树,因为二叉查找树中的每个节点的左子节点都小于自己,每个节点的右节点都大于自己。查找一个节点的最坏情况也只需要树的高度那么多次。
二叉查找树会有退化成链表的情况,导致查找次数变多,所以引出了二叉平衡树,使得每个节点的左右子树高度差不超过1,降低树的高度来减少查询次数。
为了维持平衡二叉树,需要进行左旋和右旋。
触发时机:当添加一个节点后,该树不再是平衡二叉树。
旋转的目的就是保证这棵树再次变为一颗平衡二叉树。
简单的左旋:
平衡二叉树:
添加一个节点12后,发现根节点左子树高度1,根节点右子树高度3,失去平衡。
因为根节点右子树高度大于根节点左子树,所以需要进行左旋:
稍微复杂一点的左旋:
平衡二叉树:
由于添加了节点12,失去平衡:
因为跟节点右子树高度为3,根节点左子树高度为1,所以需要左旋,左旋时,假设节点9不存在,只需要记住,9节点是10节点的左子节点:
补上9节点:
将9节点作为7节点的右子节点:
左旋:将将根节点的右侧往左拉,原先的右子节点变为新的跟节点,并把多余的左子节点出让,给已经降级的根节点当右子节点
13-平衡二叉树右旋
和左旋一样,直接看图。
简单右旋
平衡二叉树:
添加节点1,导致失去平衡:
根节点左子树高度3,根节点右子树高度1,所以需要右旋:
稍微复杂一点的右旋:
平衡二叉树:
添加节点1,失去平衡:
根节点左子树高度3,根节点右子树高度1,所以需要右旋,并且把5节点当做不存在,记住5节点是4节点的右子树:
补齐5节点,4节点就会有两个右子节点,不符合二叉树
所以需要把5节点给原来的根节点7作为左子树或左子节点:
右旋:将根节点的左侧往右拉,左子节点变成新的父节点,并把多余的右子节点出让,给已经降级的根节点当做左子节点
15-平衡二叉树的左左和左右
左左:当平衡二叉树根节点左子树的左子树有节点插入,导致二叉树不平衡:
平衡二叉树:
根节点的左子树的左子树为:
添加节点1:
因为根节点左子树高度3,右子树高度1,失去平衡,所以右旋,按右旋的步骤走,就可以保持平衡,最终结果:
左右:根节点左子树的右子树添加节点,导致二叉树不平衡
平衡二叉树:
根节点的左子树的右子树为:
插入节点6后,失去平衡:
此时应该怎么办?和原来一样,右旋可以吗?直接右旋的结果:
右旋完以后,可以发现,还是没有保持平衡,根节点左子树的高度为1,根节点右子树的高度为3,还是不平衡。
正确做法:
先左旋根节点左子树,再右旋整棵树。
需要左旋的左子树:
左旋左子树后:
现在就变成了左左的情况,再对整棵树进行右旋:
16-平衡二叉树的右右和右左
和上一节课类似。
右右:当根节点的右子树的右子树有节点插入,导致不平衡。
平衡二叉树:
根节点的右子树的右子树:
根节点的右子树的右子树,添加节点12:
失去平衡,左旋,新节点原来的左节点给降级的根节点做右节点即可:
右左:根节点的右子树的左子树有节点插入,导致二叉树不平衡
平衡二叉树:
根节点的右子树的左子树:
根节点的右子树的左子树插入节点8:
直接左旋,还是不能保持平衡:
正确做法:
先将根节点的右子树进行右旋,变成右右的情况,再整体左旋,即可保持平衡。
根节点的右子树右旋:
整体左旋,可以保持平衡:
18-红黑树-概述
又叫平衡二叉B树。
是一种特殊的二叉查找树,红黑树的每个节点都有存储位来表示节点的颜色。
每个节点可以是红色或黑色,红黑树不是高度平衡的,它的平衡是通过红黑规则进行实现的。
平衡二叉树是高度平衡的,当左右子树高度差超过1,就会进行旋转,来保证平衡。
红黑树是二叉查找树,但不是高度平衡的,旋转的条件有自己的红黑规则。
红黑树:
- 也叫平衡二叉B树,是特殊的二叉查找树。
- 每一个节点可以是红色或黑色。
- 不是高度平衡的,它的平衡是通过“自己的红黑规则"实现的。
19-红黑树-红黑规则
红黑规则:
- 每一个节点是红色或黑色
- 根节点必须是黑色
- 一个节点没有子节点或父节点,则相应的指针属性值为
Nil
,这些Nil
视为叶节点,每个叶节点(Nil)是黑色的。 - 如果某个节点是红色,那他的子节点必须是黑色。(不能出现两个红色相连)
- 对于每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
20-红黑树-添加节点的默认颜色
学习完红黑树的红黑规则,利用红黑规则将节点添加到红黑树中。
添加节点时会产生两个疑问:
- 添加的这个节点应该是红色还是黑色?
- 如果添加这个节点导致平衡二叉B树失去平衡了,怎么办?
假设默认添加的节点是黑色,且现在有三个黑色的节点:
一颗空的红黑树,依次添加上面这三个节点:
第一次添加20节点,根节点,必须是黑色,添加完后,没有子节点,所以有两个Nil叶子节点,都是黑色
第二次添加,通过比较,可知,18节点应在20的左子树中,且18节点没有子节点,所有有两个黑色的叶节点Nil,得:
这时候再来看红黑规则,第19节课中的前四条都满足,第五条不满足,即:每一个节点,到其后代的简单路径上的黑色节点数相同。显然20->18->Nil和20->Nil两条简单路径上的黑色节点数量不相同。所以需要将18节点变为红色,使得相同。
第三次添加,23节点,根据二叉查找树规则,可得23应该在根节点的右子树,且23节点没有子节点,所以有两个Nil叶节点:
这时候,又会发现,失去平衡了,20->18->Nil只有两个黑色节点,而20->23->Nil有3个黑色节点,所以又要将23节点变为红色,使得黑色节点数量相同:
总结,当默认颜色是黑色时,添加三个节点需要调整两次
假设默认添加的节点是红色,且现在有三个红色节点:
一颗空的红黑树,依次添加上面三个红色节点:
第一次添加节点20,作为根节点:
已经破坏了红黑规则,因为根节点必须是黑色。需要调整
第二次添加18节点,在左子树,没有子节点,所以有两个黑色的叶节点(Nil),此时没有破坏红黑规则:
第三次添加23节点,在20节点右子树,23节点没有子节点,有两个黑色的Nil,且没有破坏红黑规则:
总结:当默认颜色是红色时,添加3个节点只需要调整一次,所以,默认节点颜色是红色时,添加的效率要高
21~22-红黑树添加节点后如何保证红黑规则
学习完红黑树默认节点,还需要学习,当红黑树添加多个节点时,如何保持红黑规则。
通过上一节课,已经推断出来 :默认节点是红色,添加效率高。我们假设有以下节点:
- 假设根节点是20,先添加根节点:
- 此时,违反红黑规则:红黑树根节点必须是黑色,所以直接把20节点变为黑色:
- 再添加18节点,因为18比20小,所以放左边:
- 18的父节点是黑色,不需要做任何操作,没有违反红黑规则。23节点也是如此:
- 再次添加22节点。先和根节点20比,得出22节点在20节点的右边,再和23节点比较,得出22节点应该是23节点的左子节点:
- 此时,违背红黑规则:不能出现两个红色节点相连。怎么改?我们知道当添加一个节点,其父节点是黑色不需要做任何操作,但是如果其父节点(23)是红色,叔叔(18节点)节点也是红色,需要按照以下规则进行修改:(相对22节点)
- 将父节点(23)设为黑色,将叔叔节点(18)设为黑色
- 将爷爷节点(20)设为红色
- 如果爷爷节点为根节点,则将根节点再次变成黑色
按上面的规则修改后:
添加16节点,比较后得出,16节点是18节点的左子节点,不需要做任何操作,因为18节点是黑色。
剩余两个节点24和19,通过比较确定位置,父节点都是黑色,不需要做任何操作,得到:
假设我们还需要添加两个节点,15、14,先添加15:
- 确定位置,是16节点的左子节点,此时,父节点16是红色,叔叔节点19也是红色,需要调整
- 将父节点16和叔叔节点19设置为黑色
- 将爷爷节点18设置为红色
- 爷爷节点18不是根节点,再次设置为黑色
最终如下:
添加14节点,确定位置是15的左子节点
此时,14的父节点15是红色,叔叔节点nil是黑色,则需要按另一种规则进行修改
- 将父节点15设置为黑色
- 将爷爷节点16设置为红色
- 以爷爷节点作为支点进行旋转(左旋还是右旋?和平衡二叉树一样,看哪边的子树更高)
旋转步骤,16节点的左子树高度大于右子树,所以进行右旋,旋转时,可以把
nil
节点当做不存在,旋转后补上。旋转前
去掉
nil
,当做不存在,并右旋:15节点作为父节点,16节点作为右子节点。补上
nil
节点此时符合红黑规则。
小结:
添加节点使红黑树保持符合红黑规则时的处理方案:
当我们在定义自然排序时,
this
代表的就是已经添加到红黑树中的节点,o
就是正在添加的节点。
24-HashSet-概述
底层数据结构是哈希表
不能保证存储和取出的顺序完全一致
没有带索引的方法,不能使用普通for进行循环遍历,只能使用迭代器和增强for
属于
Set
集合,元素唯一
demo:
public class CollectionDemo14 {
public static void main(String[] args) {
HashSet<String> stringHashSet = new HashSet<>();
stringHashSet.add("hello");
stringHashSet.add("world");
stringHashSet.add("java");
stringHashSet.add("java");
stringHashSet.add("java");
stringHashSet.add("java");
stringHashSet.add("java");
stringHashSet.add("java");
Iterator<String> iterator = stringHashSet.iterator();
while (iterator.hasNext()){
System.out.println("iterator.next() = " + iterator.next());
}
}
}
验证了:不重复,没有顺序,不能通过索引获取。
25-HashSet-哈希值
哈希值:是jdk根据对象的地址或属性值,算出来的int类型的整数
哈希值的特点:
- 如果没有重写
hashCode()
方法,那么是根据对象的地址值计算出哈希值,同一个对象调用多次hashCode()
方法返回的哈希值返回的值是相同的,不同对象的哈希值是不一样的。 - 如果重写了
hashCode()
方法,一般都是通过对象的属性值计算出哈希值,如果不同的对象的属性值是一样的,那么计算出来的哈希值也是一样的。
问:Java中,同样的代码,每次启动运行,某个对象的hashcode都是一样的,为什么?和地址没关系吗?
答:Java中每个对象都有一个默认的hashcode方法,该方法会根据对象的地址计算出一个hashcode值。但是,在某些情况下,同一个对象的hashcode值可能会发生变化,具体取决于具体的实现和运行环境。在某些情况下,同一个对象的hashcode值会保持不变,即使在不同的运行中也是一样的。这是因为一些对象的hashcode方法实现是基于对象的某些属性计算出来的,而这些属性在对象生命周期内一直保持不变。在这种情况下,对象的地址不会影响hashcode值。总之,对于同一个对象,其hashcode值是否与地址有关,取决于具体的对象实现。
如果对
hashcode()
方法进行了重写,一般是根据对象的属性值进行计算,跟地址值就没有关系了。
26-HashSet-JDK7底层原理解析
哈希表
- jdk8以前才用数组+链表实现
- jdk8,底层进行了优化,由数组+链表+红黑树
HashSet<String> hashSet = new HashSet<>();
- 创建了一个默认长度为16,默认加载因子为0.75的数组,数组名为table。
- 根据元素的哈希值和数组长度计算出应该存入的位置。
- 判断当前位置是否为null,如果是null直接存入。
- 如果应存入的位置不是null,表示由元素,调用equals方法比较属性值。
- 如果一样,不存入,如果不一样,则存入数组,老元素挂在新元素的下面(依此类推,当存入位置的链表中有多个元素,需要多次比较,只要有一个相同,则不存入)。
- 当数组存放的元素超过加载因子*数组长度时,就会扩容为原来的两倍。
- 获取元素时,首先会获取元素的哈希值,计算出元素在数组中的下标。
27-HashSet-jdk8底层优化
当数组某个下标中,存放的元素非常多,链表非常长,如果哈希值相同,每次存入都需要拿存入的值依次比较链表上的每个元素,效率很低。所以jdk8底层做了优化,当哈希碰撞时,链表长度超过8时,数组上挂的不再是链表,而是红黑树。红黑树是排序好的,查找时根据左边小,右边大,就能快速找到。
9-6-Map
01-Map概述
双列集合:
- Interface Map<K,V>:K:键的数据类型;V:值的数据类型
- 键不能重复,值可以重复
- 键和值是一一对应的
- (键+值)整体成为键值对或键值对对象,Java中为
Entry
对象。
02-Map常用方法
03~04-Map的三种遍历方式
public class CollectionDemo15 {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1,"a");
map.put(2,"b");
map.put(3,"c");
map.put(4,"d");
Set<Integer> keyedSet = map.keySet();
for (Integer integer : keyedSet) {
String value = map.get(integer);
System.out.println("key:"+integer+",value:"+value);
}
System.out.println("===========================");
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println("key:"+key+",value:"+value);
}
System.out.println("===========================");
map.forEach((key,value)->{
System.out.println("key:"+key+",value:"+value);
});
}
}
05-HashMap原理解析
jdk8以前:数组+链表
jdk8以后:数组+链表+红黑树
使用无参构造创建HashMap
以后,会默认创建一个长度为16的数组,加载因子为0.75。当我们使用put
方法添加数据时,不会将数据直接放到数组中去,而是创建Entry
对象,记录键值对,并放到数组中去。会根据Entry
中的key
计算出hashcode
,对数组长度进行取余计算出索引,与值无关。
如果再次添加时,key计算出的hashcode
与已经存在的一致,计算出的数组索引也会一直,则为哈希冲突,需要将新添加的元素放在最上面,老的挂在新元素的下面,形成链表,当链表长度达到8时,就会转为红黑树。
HashMap的底层是哈希表结构,依赖
hashCode()
和equals
方法来保证键的唯一,如果键要存储的是自定义对象,需要重写这两个方法。
07-TreeMap-原理解析
TreeMap
和TreeSet
底层一样是红黑树结构。
把Entry
对象作为红黑树的节点。
- 当我们第一次添加时(默认节点是红色,效率高):
- 违反红黑规则,将根节点设置为黑色:
- 再次添加时,需要按照key(与值无关)的自然排序或者是传入的比较器进行排序,确定节点的位置
- 再次添加节点3
- 违反红黑规则。父节点是红色,叔叔节点是黑色。需要将父节点设置为黑色,爷爷节点设置为红色,以爷爷节点作为支点进行旋转:
TreeMap
底层是红黑树,依赖自然排序或者比较器排序对键进行排序,如果键时自定义对象,需要实现Comparable
接口或者在创建TreeMap
时传入自定义比较器给出排序规则。
09-可变参数
可变参数在底层就是一个数组。
格式:
修饰符 返回值类型 方法名 (数据类型...变量名){}
举例:
public static int sum(int...a){}
可变参数需要放在最后面,否则会报错。
10-创建不可变集合
在之前学习的集合中,使用构造器进行创建集合,集合都是可以crud元素的,有些场景是要求集合不可变化的,可以用以下方式进行创建:
当我们对不可变集合进行操作时,会抛出异常UnsupportedOperationException
。
可以进行批量添加来创建可变集合:
List<Integer> integerArrayList = new ArrayList<>(List.of(1, 2, 3, 4));
9-7-Stream流
13-获取Stream流的方式
Stream流的三类方法
- 获取Stream流
- 创建一条流水线,并把数据放到流水线上准备进行操作
- 中间方法
- 流水线上的操作
- 一次操作完毕之后,还可以继续进行其他操作
- 终结方法
- 一个Stream流只能有一个终结方法
- 是流水线上的最后一个操作
- 获取Stream流
生成Stream流的方式
Collection体系集合
使用默认方法stream()生成流, default Stream<E> stream()
Map体系集合
把Map转成Set集合,间接的生成流
数组
通过Arrays中的静态方法stream生成流
同种数据类型的多个数据
通过Stream接口的静态方法of(T... values)生成流
Stream流无法修改源数据