文章目录
学习日记(集合内容之 Collection 集合体系详述)
集合类型可以不固定,大小是可变的,只能存储引用类型的数据,适合做数据个数不确定,且要做增删元素的场景。
集合类的体系结构:
一、Collection 集合
1. 认识
- 集合都是支持泛型的,可以在编译阶段约束集合只能操作某种数据类型。
- 集合和泛型都只支持引用数据类型,不支持基本数据类型,所以集合中存储的元素都认为是对象。
2. 常用 API
Collection 是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的。
方法名 | 说明 |
---|---|
add(E e) | 在集合中添加元素,返回值为 boolean 类型 |
remove(Object o) | 在集合中删除指定元素,返回值为 boolean 类型 |
isEmpty() | 判断集合是否为空,返回值为 boolean 类型 |
size() | 获取集合大小,返回值为 int 类型 |
contains(Object o) | 判断集合中是否包含某个元素,返回值为 boolean 类型 |
toArray() | 把集合转换为数组,返回值为 Object[] 类型 |
public String toString() | 把集合转换为字符串,继承的 Object 类中的方法,返回值为 String 类型 |
addAll(Collection<? extends E> c) | 把另一集合的元素全部倒入该集合中,返回值为 boolean 类型 |
clear() | 清空集合,没有返回值 |
示例
运行结果
3. 遍历方式
Collection 集合的迭代方式有三种:迭代器、foreach/增强 for 循环、lambda 表达式。
(1)迭代器
- 迭代器在 Java 中的代表是 Iterator,迭代器是集合的专用遍历方式。
- 遍历就是一个一个把容器中的元素访问一遍。
获取迭代器 it:Iterator<String> it = list.iterator();
,默认迭代器对象 it 一开始指向当前集合的索引 0。
Iterator 中的常用方法 | 说明 |
---|---|
boolean hasNext() | 询问当前位置是否有元素存在,返回值为 boolean 类型 |
E next() | 获取当前位置的元素,并同时将迭代器对象移向下一个位置,注意防止取出越界 |
(2)foreach/增强 for 循环
-
foreach/增强 for 循环既可以遍历集合又可以遍历数组。
-
是 JDK 5 之后出现的,其内部是一个 Iterator 迭代器,遍历集合相当于迭代器的简化写法。
-
实现 Iterable 接口的类才可以使用迭代器和增强 for 循环,Collection 接口已经实现了 Iterable 接口。
(3)Lambda 表达式
Collection 集合结合 Lambda 表达式遍历的 API:default void forEach(Consumer<? super T> action)
。
4. 存储自定义类型的对象
需求:某影院系统需要在后台存储三部电影,然后依次展示出来。
电影名 类型 放映时间 票价 评分 《冲刺吧》 励志 2022.09.01 30.5 99 《今天你学习了吗》 纪录 2022.10.12 52.1 98 《想吃红烧肉》 喜剧 2022.11.19 25.3 97
注意:集合中存储的是元素的地址,如果想看内容,则要重写这个类的 toString 方法。
二、List 集合
List 集合包括 ArrayList 集合和 LinkedList 集合,特点:有序、可重复、有索引。
1. 特有 API
因为 List 集合支持索引,所以多了很多索引操作的 API,并且继承了 Collection 类的方法。
List 独有的方法 | 说明 |
---|---|
void add(int index, E element) | 增 |
E remove(int index) | 删,返回被删除的元素 |
E get(int index) | 查 |
E set(int index, E element) | 改,返回被修改的元素 |
2. 遍历方式
List 集合的迭代方式有三种:迭代器、foreach/增强 for 循环、lambda 表达式、for 循环(有索引)。
3. ArrayList 集合底层原理
- ArrayList 底层是基于数组实现的,根据索引定位元素快,增删需要做元素的移位操作。
- 第一次创建集合并添加第一个元素的时候,在底层创建一个默认长度为 10 的数组。
- 当 List 集合存储的元素要超过容量时,会按当前长度的 1.5 倍扩容。
4. LinkedList 集合底层原理
LinkedList 集合底层数据结构是双链表,查询慢,首尾操作的速度是极快的,因此多了很多首尾操作的特有 API。
LinkedList 集合首尾操作的方法 | 说明 |
---|---|
public void addFirst(E e) | 在列表的开头插入指定的元素 |
public void addLast(E e) | 在列表的末尾插入指定的元素 |
public E removeFirst() | 从列表中删除并返回第一个元素 |
public E removeLast() | 从列表中删除并返回最后一个元素 |
public E getFirst() | 返回列表中的第一个元素 |
public E getLast() | 返回列表中的最后一个元素 |
- LinkedList 集合完成栈结构
- LinkedList 集合完成队列结构
三、补充知识 1
1. 并发修改异常问题
当从集合中找出某个元素并删除时,可能出现一种并发修改异常问题,如迭代器(可以避免)、增强 for 循环(不可避免)、for 循环(不出现异常,但可能会漏删除,可以避免)。
- 迭代器(可以避免)
改正:用迭代器调用 remove 方法
- for 循环(可以避免)
改正:用 i-- 或者倒着循环遍历
2. 泛型深入
(1)泛型的概述和优势
-
概述:泛型是 JDK5 中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。泛型只支持引用数据类型,集合体系的全部接口和实现类都是支持泛型的使用的。
-
优势:统一数据类型;把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为编译阶段类型就能确定下来。
泛型可以在很多地方进行定义:类后面(泛型类)、方法申明上(泛型方法)、接口后面(泛型接口)。
(2)自定义泛型类
泛型类的格式:public class MyArrayList<T> {}
,其中泛型变量常见的有:E
、K
、T
、V
。
作用:编译阶段可以指定数据类型,类似于集合的作用。
原理(核心思想):把出现泛型变量的地方全部替换为传输的真实数据变量。
需求:模拟 ArrayList 集合自定义一个集合 MyArrayList 集合,完成添加和删除功能的泛型设计。
(3)自定义泛型方法
泛型方法的格式:public <T> void show(T t) {}
。
作用:方法中可以使用泛型接收一切实际类型的参数,方法更具通用性。
原理(核心思想):把出现泛型变量的地方全部替换为传输的真实数据变量。
需求:给定任意一个类型的数组,都能返回它的内容,相当实现 Arrays.toString 的功能。
(4)自定义泛型接口
泛型接口的格式:public interface Data<E> {}
。
作用:泛型接口可以让实现类选择当前功能需要操作的数据类型。
原理(核心思想):实现类可以在实现接口的时候传入自己操作的数据类型,这样重写的方法都将是针对于该类型的操作。
需求:教务系统,提供一个接口可约束要完成数据(学生、老师)的增删改查操作。
(5)泛型通配符、上下限
- 通配符:
?
:表示所有类型。 ?
是在使用泛型的时候代表一切类型;E
、K
、T
、V
是在定义泛型的时候使用的。- 泛型的上下限:
? extends Car
:表示必须是 Car 类或者其子类(泛型上限);? super Car
:表示必须是 Car 类或者其父类(泛型下限)。
需求:飞车游戏,所有汽车都能一起参加比赛。
四、Set 集合
Set 集合包括 HashSet 集合和 TreeSet 集合,Set 集合特点:无序、不重复、无索引。
1. 常用 API
Set 集合的功能基本上与 Collection 的 API 一致。
2. HashSet 底层原理
HashSet 集合底层采取哈希表存储的数据,哈希表是一种对于增删改查数据性能都较好的结构。
哈希表的组成:
- JDK8 之前,底层采用”数组 + 链表“组成;
- JDK8 之后,底层采用”数组 + 链表 + 红黑树“组成。
哈希值:是 JDK 根据对象的地址,按照某种规则算出来的 int 类型的数值。
获取哈希值:对象调用 Object 类的 hashCode 方法,如:a1.hashCode()
。
哈希值特点:
- 同一个对象多次调用 hashCode 方法,返回的哈希值是相同的;
- 默认情况下,不同对象的哈希值是不同的。
(1)JDK1.7 版本原理
底层结构:数组+ 链表,哈希表流程如下:
- 创建一个默认长度为 16 的数组,默认加载因为 0.75,数组名为 table;
- 根据元素的哈希值跟数组的长度求余,计算出应存入的位置(哈希算法);
- 判断当前位置是否为 null,如果是,则直接存入;否则,表示有元素,调用 equals 方法比较;
- 如果比较结果一样,则不存(避免重复),如果不一样,则存入数据(JDK 7 新元素占老元素位置,指向老元素;JDK 8 中新元素挂在老元素下面)。
- 当数组存满到 16 x 0.75 = 12 时,会自动扩容,每次扩容到原先的二倍(16 x 2 = 32)。
结论:哈希表是一种对于增删改查数据性能都较好的结构。
(2)JDK1.8 版本之后原理
底层结构:数组 + 链表 + 红黑树,在之前的规则下,增加了一条:
- 当挂在元素下面的数据过多时,查询性能降低,从 JDK8 开始,当链表长度超过 8 时,会自动将链表转换为红黑树。
结论:从 JDK8 开始,哈希表对于红黑树的引入进一步提高了操作数据的性能。
3. HashSet 集合去重复
需求:创建一个存储学生对象的集合,存储多个学生,使用程序实现在控制台遍历该集合。
要求:学生对象的成员变量值相同,我们就认为是一个对象。
改正:重写了对象的 hashCode 和 equals 方法,快捷键:Alt + Insert
。
4. LinkedHashSet 集合
特点:有序、不重复、无索引。
原理:底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表的机制记录存储的顺序。
5. TreeSet 集合
特点:可排序(默认升序)、不重复、无索引。
原理:底层数据结构是基于红黑树的数据结构实现排序的,增删查改性能都很好。
注意:TreeSet 集合是一定要排序的,可以将元素按照指定的规则进行排序。
默认排序规则:
- 对于数值类型:Integer、Double 等,默认按照大小进行升序排序;
- 对于字符串类型:默认按照首字母的编号升序排序;
- 对于自定义类型:如 Student 类,无法直接排序,需要指定排序规则(两种方式)。
- 让自定义的类(如 Student 类)实现 comparable 接口,重写里面的 compareTo 方法,来制定比较规则;
- TreeSet 集合有参数构造器,可以设置 Comparator 接口对应的比较器对象,来制定比较规则。
指定排序规则第一种方式
解决
指定排序规则第二种方式
如果是浮点型数据排序
注意:如果通过方式一和方式二都同时指定了排序规则,则默认使用方式二排序,即使用集合自带的比较器排序!!!
五、补充知识 2
1. 可变参数
作用:在形参中可以接收多个数据,传输参数非常灵活、方便,可以传输一个或多个参数,也可以传输一个数组,也可以不传参数。
本质:可变参数在方法内部本质上就是一个数组。
注意:一个形参列表中只能有一个可变参数;可变参数必须放在形参列表的最后面。
2. 集合工具类 Collections
Collections 不属于 Collection 集合体系,它只是一个操作集合的工具类。
使用范围 | 方法名 | 说明 |
---|---|---|
对于 Collection 集合 | public static boolean addAll(Collection<? super T> c, T… elements) | 给集合中批量添加元素 |
只对于 List 集合 | public static void shuffle(List<?> list) | 打乱 List 集合元素的顺序 |
只对于 List 集合 | public static <T extends Comparable<? super T>> void sort(List list) | 排序方式一:类实现了接口 |
只对于 List 集合 | public static void sort(List list, Comparator<? super T> c) | 排序方式二:用比较器对象 |
自定义类型
六、Collection 体系的综合案例
需求:斗地主游戏完成功能:做牌、洗牌、定义 3 个玩家、发牌、排序、看牌。
package com.residue.collection2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 目标:斗地主游戏完成功能:
* 1. 做牌 √
* 2. 洗牌 √
* 3. 定义 3 个玩家 √
* 4. 发牌 √
* 5. 排序 √
* 6. 看牌 √
*
* 要求:总共 54 张牌,包括大小王,剩余 3 张牌为底牌。
*/
public class Game {
//1. 定义一个静态集合存储 54 张牌对象
public static List<Card> allCards = new ArrayList<>();
//2. 定义静态代码块初始化牌数据(做牌)
static {
String[] cardSize = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
String[] cardColor = {"♦", "♣", "♠", "♥"};
//3. 做 52 张牌
int index = 0;
for (String s : cardSize) {
index++;
for (String s1 : cardColor) {
Card card = new Card(s, s1, index); //创建 52 张牌对象
allCards.add(card);
}
}
//4. 做大小王,第 53 和 54 张牌
allCards.add(new Card("", "小🃏", ++index));
allCards.add(new Card("", "大🃏", ++index));
System.out.println(allCards);
System.out.println("============================================");
}
public static void main(String[] args) {
//5. 洗牌
Collections.shuffle(allCards);
System.out.println(allCards);
//6. 定义 3 个玩家
List<Card> xiaoZhang = new ArrayList<>();
List<Card> xiaoZhu = new ArrayList<>();
List<Card> xiaoWang = new ArrayList<>();
//7. 发牌
for (int i = 0; i < allCards.size() - 3; i++) {
if (i % 3 == 0) {
xiaoZhang.add(allCards.get(i));
} else if (i % 3 == 1) {
xiaoZhu.add(allCards.get(i));
} else if (i % 3 == 2) {
xiaoWang.add(allCards.get(i));
}
}
//8. 剩余 3 张牌单独为一个集合
List<Card> lastThreeCards = allCards.subList(allCards.size() - 3, allCards.size());
//9. 排序(从大到小),对三个人的牌都需要排序,为节省代码,用方法来实现
sortCards(xiaoZhang);
sortCards(xiaoZhu);
sortCards(xiaoWang);
//10. 看牌
System.out.println("小张的牌为:" + xiaoZhang);
System.out.println("小朱的牌为:" + xiaoZhu);
System.out.println("小王的牌为:" + xiaoWang);
System.out.println("最后三张底牌为:" + lastThreeCards);
}
//排序方法:按照牌的属性 index 排序,不关心花色
private static void sortCards(List<Card> cards) {
Collections.sort(cards, new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
return o2.getIndex() - o1.getIndex();
}
});
}
}
Card 类
package com.residue.collection2;
public class Card {
private String size;
private String color;
private int index; //每张牌实际的大小
public Card() {
}
public Card(String size, String color, int index) {
this.size = size;
this.color = color;
this.index = index;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
@Override
public String toString() {
return size + color;
}
}
运行结果
注意点:
- 用静态代码块来初始化牌,随着类加载而产生,在启动 main 方法之前优先加载,只运行一次。
- 用 List 集合中的
List<E> subList(int fromIndex, int toIndex)
方法,可以将原 List 集合按索引截取出新的 List 集合,如:List<Card> lastThreeCards = allCards.subList(allCards.size() - 3, allCards.size());
,仍然是左闭右开。 - 对牌进行排序时,新定义一个私有属性 index,表示每张牌的实际大小,依据此来进行排序。
- 为减少重复代码,可以将排序功能封装成一个方法,可以在 main 方法中写出方法名和参数,然后
Alt + Enter
来自动创建方法。
注意:
- 迭代器取元素越界会出现
NoSuchElementException
异常。
-
增强 for 循环修改无意义,不能修改原集合或数组中的元素。
-
用增强 for 循环边遍历边删除元素一定会出现并发修改异常,不可避免的!
- 三元运算符不是语句!不能用
;
结束。
- 自定义泛型方法中的举例,也可以这样。
- 与三、2、(5)图做对比。
- TreeSet 集合存储自定义类型变量时,要指定排序规则。
-
总结 Collection 集合
- 希望元素可以重复、有索引、索引查询快:用 ArrayList 集合,基于数组的。(用的最多)
- 希望元素可以重复、有索引、增删首尾操作快:用 LinkedList 集合,基于链表的。(栈、队列)
- 希望元素增删改查都快,但是元素不重复、无索引、无序:用 HashSet 集合,基于哈希表的。
- 希望元素增删改查都快,但是元素不重复、无索引、有序:用 LinkedHashSet 集合,基于哈希表和双链表的。
- 如果要对对象进行排序:用 TreeSet 集合,基于红黑树,也可以用 List 集合排序,看本文五、2,注意,List 集合排序元素可重复,不需要三元运算符。还要注意,TreeSet 集合是自排序,而用 List 集合排序是调用了 Collections 类的 sort 方法。
-
批量修改变量名快捷键:
Shift + F6
。 -
用 Lambda 表达式简化的快捷键:
选中接口名 + Alt + Enter
。