⭕在 Java 中,集合框架是开发过程中最常用的数据结构之一,其中 Collection 接口是整个集合框架的基础。Collection 是处理单列数据的接口,它定义了一些通用的操作,允许对一组对象进行操作。今天我们将深入介绍 Java 中的单列集合 Collection 接口,以及它的常见子接口和实现类。
在谈论集合时,我们都有一个问题,那就是数组和集合有什么区别?
相同点
集合和数组都是容器,可以用来存储多个数据
不同点
数组长度是不可变的,集合的长度是可以变的
集合只能存储引用数据类型,如果要存储基本数据类型,需要存对应的包装类
集合中集成了很多实用的数据处理方法,提供了功能各异的集合以达到提高程序运行效率的目的
想要深入了解Collection集合,我们就得先了解其在整个集合类的体系结构中的位置
一、集合体系结构图(Collection在集合体系中的位置)
二、创建Colletion对象的方式
由集合体系结构图可知Collection类是单列集合的顶层接口,所以不能通过它直接创建对象,我们需要用到其子接口的实现类或者通过多态的方式创建其对象。
2.1 多态的方式
2.2 具体实现类
三、Collection 核心(常用)方法
方法名称 | 返回类型 | 描述 |
---|---|---|
boolean add(E e) | boolean | 向集合中添加元素。如果集合因为此调用发生了变化,则返回 true 。 |
boolean remove(Object o) | boolean | 从集合中删除指定元素。如果集合包含该元素并成功移除,则返回 true 。 |
boolean contains(Object o) | boolean | 判断集合中是否包含指定的元素。如果集合包含此元素,则返回 true 。 |
int size() | int | 返回集合中元素的数量。 |
Iterator<E> iterator() | Iterator<E> | 返回一个用于遍历集合中元素的迭代器。 |
void clear() | void | 清空集合中的所有元素。 |
boolean isEmpty() | boolean | 判断集合是否为空。如果集合为空,则返回 true 。 |
3.1 add方法
向集合中添加元素。如果集合因为此调用发生了变化,则返回 true
。
细节:
- 如果往List系列集合中添加元素永远返回true,因为List系列集合元素可重复
- 如果往List系列集合中添加元素如果元素已经存在,则会返回false
3.2 remove方法
从集合中删除指定元素。如果集合包含该元素并成功移除,则返回 true,如果元素不存在则返回false
。
细节:因为Collection里面定义的是共性的方法(Set系列集合也要适用),所以此时不能通过索引进行删除,这里面具体涉及多态中方法重写和重载的区别,可以参考我另外一篇文章
在多态的方法调用中为什么会出现“左边编译左边运行”的现象https://blog.csdn.net/q251932440/article/details/142509834?spm=1001.2014.3001.5501
3.3 clear方法
清空集合中的所有元素。
3.4 contains方法
细节:contains方法依赖equals方法进行判断,所以如果集合中存储自定义对象时,需要在JavaBean类中重写equals方法。
Student中重写的equals方法:
3.5 size方法
返回集合中元素的数量。
3.6 isEmpty方法
判断集合是否为空。如果集合为空,则返回 true
。
3.7 iterator方法
返回一个用于遍历集合中元素的迭代器。(在下文迭代器中介绍)
四、Collection集合的遍历
Collection集合中通用的遍历方法有三种(均不依赖索引):
- 迭代器遍历
- 增强for遍历
- Lambda表达式遍历
4.1 迭代器遍历
迭代器:是集合专门用来遍历的工具。
在源码中可以看到,迭代器Iterator是一个接口类,所以不能直接创建其对象来使用,需要通过集合对象中的iterator()方法返回一个Iterator接口实现类的对象。
1. 接口无法直接创建对象
接口无法直接实例化是因为它没有实现方法的具体细节。接口只是一个契约,定义了类必须实现的方法。比如,
Iterator
接口定义了hasNext()
、next()
和remove()
方法,但并没有提供这些方法的具体实现。2.
ArrayList
的iterator()
方法当你调用
ArrayList
的iterator()
方法时,它内部实际上是返回了一个 匿名内部类 或 具体的类 的实例,这个实例实现了Iterator
接口。也就是说,虽然我们只看到了Iterator
接口,但它实际是ArrayList
内部某个私有类的对象,该类实现了Iterator
接口的所有方法。在
ArrayList
源代码中,iterator()
方法大概像这样实现:public Iterator<E> iterator() { return new Itr(); // 返回一个实现了 Iterator 接口的类的实例 } private class Itr implements Iterator<E> { // 实现 Iterator 接口的方法,如 hasNext(), next(), remove() public boolean hasNext() { // 具体实现 } public E next() { // 具体实现 } public void remove() { // 具体实现 } }
4.1.1 迭代器(Iterator)常用方法表格
方法名称 | 返回类型 | 描述 |
---|---|---|
boolean hasNext() | boolean | 判断集合中是否还有下一个元素。如果有,返回 true ;否则返回 false 。通常用于循环控制。 |
E next() | E | 返回集合中的下一个元素,并将迭代器的指针移动到下一个元素。如果已经没有元素可返回,调用该方法会抛出 NoSuchElementException 异常。 |
void remove() | void | 移除迭代器当前指向的元素(即最近一次调用 next() 方法返回的元素)。这是可选操作,如果集合不支持 remove() 方法,调用时会抛出异常。 |
4.1.2 示例1-遍历:
细节:迭代遍历结束后,指针不会复位,如果还想遍历则要新建一个迭代器
4.1.3 示例2-遍历的过程中删除元素
移除迭代器当前指向的元素(即最近一次调用 next()
方法返回的元素)
如果要删除,用迭代器的remove方法,如果用集合的方法进行增加或者删除会报错
正确用法:
4.1.4 迭代器使用的注意点
- 迭代器遍历完毕后,指针不会复位,如果还要继续遍历,需要新建一个迭代器
- 指针处已经没有元素仍要执行next方法,系统会报错 'NoSuchElementException' (空元素异常)
- 循环中,只能使用一次next方法,如果在一个循环中多次调用next方法,元素总数为奇数的时候也会有 'NoSuchElementException' (空元素异常)报错的风险
- 迭代遍历的过程中(循环里)不能用集合的方法进行增删改查
- 数组不能直接使用迭代器进行遍历,需要使用需转换为集合
4.2 增强for遍历
- 增强for于JDK5后问世,其内部原理是一个Iterator迭代器
- 所有单列集合和数组才能使用增强for进行遍历
作用:简化数组和集合的遍历,增加安全性
格式:
for(集合/数组中元素的数据类型 变量名 : 集合/数组名) { // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可 }
快捷:集合/数组.for 回车——自动生成增强for代码块
4.2.1 示例:
4.2.2 增强for注意点
增强for里面的Student student是一个第三方变量,student是其变量名,改变该变量值,不会影响集合中的数据
验证:
4.3 Lambda表达式遍历
利用forEach方法,结合Lambda表达式进行遍历(其底层是增强for)
作用:简化代码
4.3.1 示例:
使用forEach后,使用Lambda表达式前:
使用Lambda表达式后:
4.3.2 使用剖析
通过查看源码可以知道,forEach方法需要传入的形参是一个Consumer接口类型的数据,而且是一个函数式接口。所以我们在传入参数的时候需要传入一个Consumer接口的实现类对象,因此采用了匿名内部类的方式创建其实现类对象并传入到方法中,然后根据Lambda表达式格式改写成Lambda表达式以简化代码。
4.3.3 注意:
修改student一样不会改变集合的值,和增强for一样(因为底层就是一个增强for)
4.4 三种遍历方法的使用场景
五、Collection子接口——List集合
5.1 List集合的特点
- 存取有序(指的是存和取得顺序一样,跟排序不同)
- 可以重复
- 有索引
List继承了Collection,但List仍然是一个接口类,如果想要创建List集合,则需要创建List接口得实现类对象 (ArrayList、LinkedList),例如:
5.2 List集合的特有方法
- List继承了Collection,拥有Collection集合的所有方法
- 因为List集合支持索引,索引新增了一些处理索引的方法方法介绍
方法列表:
方法名 | 描述 |
---|---|
void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
5.2.1 add方法
在List集合中有两种add方法:
- Collection中的add
- void add(int index,E element) 在此集合中的指定位置插入指定的元素
这里重点分析第二种
代码示例:
打印结果:
由上述代码可知,使用add指定添加索引后,原本在该索引的元素会往后退一位让出索引,其后面的元素都会依次往后移动。
5.2.2 remove方法
List集合中remove方法也有两种:
- Collection中的remove
- E remove(int index) 删除指定索引处的元素,返回被删除的元素
这里仍然重点分析第二种
代码示例:
标识代码块中的打印结果:
由上述代码可知,当使用remove删除一个元素的时候,被删除所在元素的位置就会空了,其后面的元素会自动依次向前移动
细节问题
当创建一个整数类型的集合时,采用remove方法删除会使用删除索引的remove方法还是使用删除元素的remove方法?
代码演示:
如果一定要使用其删除1这个元素,就需要进行手动装箱:
5.2.3 set方法
5.2.4 get方法
5.3 List集合遍历方式及对比
5.3.1 迭代器遍历
5.3.2 列表迭代器
往前迭代那个方法有局限性,因为迭代器一开始默认实在0索引处的(基本不用)
遍历过程中添加元素:
5.3.3 增强for
5.3.4 Lambda表达式
5.3.5 普通for循环
5.4 List的实现类——LinkedList
5.4.1 底层核心步骤
刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null
添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值
添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值
LinkedList集合底层是链表结构实现的,查询慢,增删快
5.4.2 特有方法
但是如果操作首尾元素,速度也是非常快的,所以LinkedList多了一些操作首尾元素的方法:
方法名 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
但是这些方法用得比较少,在Collection和List的方法中也基本能实现。
5.5 List的实现类——ArrayList
5.5.1 底层核心步骤:
- 创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。
- 数组名字:elementDate,定义变量size。
- size这个变量有两层含义(添加元素,添加完毕后,size++):
- ①:元素的个数,也就是集合的长度
- ②:下一个元素的存入位置
当添加第一个元素的时候,底层会创建一个新的长度为10的数组
- 扩容时机一:
- 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。
- 如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。
- 扩容时机二:
- 如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。
六、Collection子接口——Set集合
Set中的方法基本和Collection中的方法一致
6.1 Set集合的特点
- 数据存取顺序不一致(LinkedHashSet除外)
- 不可存储重复元素(可以利用这个特点来对数据去重)
- add方法的返回值在Set中奏效了,如果重复的元素添加进集合,会添加失败并返回false
- 没有索引(遍历时不能使用普通for)
6.1.1 Set实现类与子实现类的特点
6.2 Set集合遍历方式
与Collection一样:
- 迭代器
- 增强for
- Lambda表达式
6.2.1 迭代器遍历
6.2.2 增强for
6.2.3 Lambda表达式
6.3 Set的实现类——HashSet
6.3.1 特点
- 底层数据结构是哈希表(JDK8以后由数组+链表+红黑树组成)
- 默认加载因子:填充的元素达到数组长度的75%就扩容一倍
- 存取无序
- 不可以存储重复元素
- 没有索引
- 增删改查的性能都很好
6.3.2 哈希值
JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
⭕哈希值的获取
Object类中的public int hashCode():返回对象的哈希值
⭕哈希值的特点
-
同一个对象多次调用hashCode()方法返回的哈希值是相同的
-
默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象而属性值相同的哈希值相同(String等java已经定义好的对象已经重写了该方法)
-
在小部分情况下,不同属性值或者不同地址值计算出来的哈希值也有可能一样(哈希碰撞)
-
因此为了避免哈希碰撞导致部分元素丢失,通常也要重写自定义对象(String等java已经定义好的对象已经重写了该方法)中的equals方法
-
原因:这样一来,即使哈希值相同的元素还会被进一步被equals方法确认是否为重复元素,放防止直接被丢掉不存。
-
-
6.3.3 代码示例
学生类:
package test01;
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;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
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);
}
}
测试类:
package test01;
import java.util.HashSet;
import java.util.Set;
public class SetTest01 {
public static void main(String[] args) {
Set<Student> set1 = new HashSet<>();
Student s1 = new Student("zhangsan",11);
Student s2 = new Student("lisi",13);
Student s3 = new Student("wangwu",12);
set1.add(s1);
set1.add(s2);
set1.add(s3);
set1.forEach(student -> System.out.println(student));
}
}
6.3.4 HashSet子类——LinkedHashSet
LinkedHashSet与HashSet不同的是:LinkedHashSet集合存取元素是有序的
为什么存取有序?
LinkedHashSet其底层数据结构依然是哈希表,只是每个元素又额外增加了一个双链表机制记录存储的顺序。
通过以下代码展示,我们可以知道LinkedHashSet集合存取元素是有序的
如果以后数据要去重,我们使用哪一个集合?
6.4 Set的实现类——TreeSet
6.4.1 特点
- 可以将元素按照规则排序(以下必选其一,不然获取集合会报错)
- 实现Comparable接口进行自然排序
- 使用比较排序器Comparator
- 不可存储重复元素(依赖上述两种比较方法实现的,因此不需要类重写HashCode和equals方法)
- 没有索引
- 底层数据结构为红黑树
6.4.2 在没有排序时的TreeSet集合:
当我们添加自定义对象进集合后,TreeSet不知道排序规则,会出现如下报错(类转换异常)
如果我们添加整数类型的对象进TreeSet中,会得到以下结果,其数据从小到大排列起来了
那为什么会出现这种状况呢?TreeSet的排序规则是什么呢?
6.4.3 TreeSet的默认排序规则
TreeSet的排序都是要实现Comparable接口进行自然排序或者使用比较排序器Comparator来进行的,之所以java中的一些定义好的数据类如Integer、String等能够直接排序(有默认排序规则)是因为java已经在这些类上实现了Comparable接口。
- 对于数值类型(如Integer、Double):默认按照从小到大顺序进行排序
- 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序(从小到大)
- 剖析字符串排序:
- 自定义对象,需要实现Comparable接口进行自然排序或者使用比较排序器Comparator
那么接下来我们就开始介绍这两种排序方法。
6.4.4 TreeSet的排序方法
⭕自然排序Comparable接口的使用
实现步骤
-
使用空参构造创建TreeSet集合
用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的 -
自定义的Student类实现Comparable接口
自然排序,就是让元素所属的类实现Comparable接口,指定要对比的类型,重写compareTo(T o)方法 -
重写接口中的compareTo方法(指定排序规则)
重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
代码示例:
存储学生对象并遍历,创建TreeSet集合使用无参构造方法
要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
学生类:
package test01; import java.util.Objects; 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; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "Student{name = " + name + ", age = " + age + "}"; } @Override public int compareTo(Student o) { //大于0说明this.age大,排在后面 int result = this.age - o.age; //this.name.compareTo(o.name),因为this.name是一个字符串对象 //调用字符串对象可以用到字符串类型中定义好的compareTo方法 result = result == 0 ? this.name.compareTo(o.name) : result; return result; } }
测试类:
package test01; import java.util.Set; import java.util.TreeSet; public class TreeSetTest01 { public static void main(String[] args) { Set<Student> t1 = new TreeSet<>(); Student s1 = new Student("zhangsan",11); Student s2 = new Student("lisi",13); Student s3 = new Student("wangwu",11); Student s4 = new Student("linchuqiao",21); t1.add(s1); t1.add(s2); t1.add(s3); t1.add(s4); for (Student s : t1) { System.out.println(s); } } }
运行结果:
⭕比较排序器Comparator的使用
Comparator也是一个接口,但是需要在带参构造方法使用
实现步骤
-
用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
-
比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法
-
重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
代码示例:
要修改默认排序,先对字符串长度排序
package test01; import java.util.Comparator; import java.util.Set; import java.util.TreeSet; public class TreeSetTest02 { public static void main(String[] args) { Set<String> set = new TreeSet<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { int result = o1.length() - o2.length(); return result == 0 ? o1.compareTo(o2) : result; } }); set.add("guangzhou"); set.add("chongqing"); set.add("beijing"); set.add("shanghai"); for (String s : set) { System.out.println(s); } } }
运行结果:
String原先实现了Comparable接口,但是为了修改默认排序采用了Comparator比较器,由此可知第二种和第一种方法同时存在的话会遵循第二种方法(Comparator比较器)。
⭕两种排序方法总结
-
自然排序: 自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序
-
比较器排序: 创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序
-
在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序(一般是需要修改已经在源码中写好的排序规则,如String等类型的对象)
两种方式中关于返回值的规则:
如果返回值为负数,表示当前存入的元素是较小值,存左边
如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
如果返回值为正数,表示当前存入的元素是较大值,存右边
七、全文总结
Collection
接口及其子接口 List
和 Set
构成了 Java 集合框架的核心部分,它们通过不同的实现类为我们提供了灵活的方式来处理数据。理解各个集合类的特点和适用场景,能够帮助开发者高效地组织和操作数据。
7.1 Collection集合性能对比总结
操作 | ArrayList | LinkedList | HashSet | TreeSet |
---|---|---|---|---|
添加元素 | 较快 | 较快(首尾) | 快 | 较慢(排序) |
删除元素 | 较慢 | 较快(首尾) | 快 | 较慢 |
查找元素 | 快 | 较慢(首尾快) | 快 | 较慢 |
元素存取顺序 | 有序 | 有序 | 无序 | 可排序 |
在实际开发中,选择合适的集合类是非常重要的。对于需要频繁查找、插入和删除的场景,应根据数据结构的特点选择合适的实现类。
通过本文的介绍,你应该能够初步掌握 Java 中 Collection
单列集合的基础知识,并且学会如何选择适合的集合类进行开发。在实际项目中,合理使用集合将有助于提升代码的性能和可维护性噢!~