Java 集合
红色框:接口
蓝色框:具体的实现类
Collection :是所有单列集合的祖宗
List系列集合特点:
- 有序:按照插入顺序排列的
- 可重复:集合中可以通过一样的数据
- 有索引:有序的 , 可以根据索引来进行获取
set系列集合特点:
- 无序:根据计算好的hash值来存放的
- 不可重复的:集合同一份数据只能存在一份(可用于去重)
- 无索引:不是按照新增的顺序进行排序的,所以不能按照索引进行获取。
Collection 集合
Collection 接口定义的方法都是 List 系列和 Set 系列共同拥有的方法。
常用的方法
- add : 添加元素 , 如果添加成功返回 true ,否则返回false ,比如在 Set 系列集合添加已经存在的数据就是返回 false 。
- remove:删除元素
- contains:判断集合是否包含某个元素 (重点:当集合中存储的数据是自定义的类,该类必须要重写 equals 方法)
- 细节:
- contains 底层是根据 equals 方法来判断两个元素是否一致的。
- Object 中的 equals 方法是根据地址值进行比较的
- 如果自定义的类没有重写 equals 方法就是使用 父类(Object)中的 equals 方法
- 如果集合中存储的数据是自定义的类,一定要重写 equals 方法,否则按照地址值来进行比较两个元素是否一样了,一般情况下我们认为只要元素的中的属性值一样就代表这两个元素是一样的了。
- 细节:
- clear :清空集合中的所有元素
- isEmpty:判断集合是否为空,底层判断是集合中 size 属性是否 == 0
- size:获取集合的元素的个数。
拓展:为什么 equals 和 hashCode 必须一起重写
1. 简要说明 equals
和 hashCode
的作用
equals
方法:用于比较两个对象的内容是否相等。hashCode
方法:用于返回对象的哈希值,主要用于哈希表(如HashMap
、HashSet
)中快速定位对象。
2. 解释 equals
和 hashCode
的契约
- Java 规范规定:
- 如果两个对象通过
equals
方法比较相等,那么它们的hashCode
必须相同。 - 如果两个对象的
hashCode
相同,它们不一定通过equals
方法比较相等(哈希冲突是允许的)。
- 如果两个对象通过
- 这是为了确保对象在哈希表中能正确工作。
3. 结合哈希表的工作原理
- 哈希表(如
HashMap
、HashSet
)依赖hashCode
和equals
方法来存储和查找对象:- 当对象被放入哈希表时,先通过
hashCode
计算哈希值,确定存储位置(桶)。 - 当从哈希表中查找对象时,先通过
hashCode
定位到桶,再通过equals
方法在桶内比较对象。
- 当对象被放入哈希表时,先通过
- 如果两个对象通过
equals
方法比较相等,但hashCode
不同,它们会被存储在不同的桶中,导致哈希表无法正确找到对象。
4. 举例说明
Student 类:
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
// contains 必须重写 equals
@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);
}
// 依赖hash值进行存储必须重写 hashCode 方法
// 不重写 hashCode 方法,但是 equals 比较相同的两个元素,不在同一个hash桶中。
/* @Override
public int hashCode() {
return Objects.hash(name, age);
} */
}
public class Demo03 {
public static void main(String[] args) {
Student s1 = new Student("zhangsan", 12);
Student s2 = new Student("zhangsan", 12);
/*
* 1. Set 系列集合是不允许元素重复的
* 2. Set 系列中元素的位置是根据 hashCode 值的来决定元素存放的位置
* */
HashSet<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
/*
* 问题:上面 set 集合中添加两个元素的属性值都是一样,
* 应该认为是同一个元素才对,所以 size 的值应该是 1 ,
* 但是输出的值却是2 ,这里代表了两个属性值一的元素的hashCode 是不一样
* 违反了 equals 和 hashCode 的 契约
*
* 造成上面问题的原因是:类中重写了 equals 方法但是没有重写 hashCode 方法
*
* */
System.out.println(s1.equals(s2)); // true
System.out.println(students.size()); // 2
}
}
5. 总结
- 重写
equals
时必须重写hashCode
,以确保对象在哈希表中能正确工作。 hashCode
的一致性:如果两个对象通过equals
方法比较相等,它们的hashCode
必须相同。hashCode
的性能:尽量使不同对象的hashCode
分布均匀,以减少哈希冲突。- equals比较两个元素一样的时候两个元素的hashCode 必须一样。
遍历 Collection 集合
迭代器遍历
package com.zian.demo_08;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Demo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("a");
coll.add("b");
coll.add("c");
coll.add("d");
// 获取迭代器 ,就是一个指针, 这个指针指向集合中第一个元素的位置
Iterator<String> iterator = coll.iterator();
// 判断集合中是否有下一个元素
while (iterator.hasNext()) {
// 获取元素, 并且移动指针,指针指向一下元素的位置
String next = iterator.next();
System.out.println("next = " + next);
// 在迭代器遍历的代码不能进行删除或者新增操作
// 使用集合中自带的 remove 方法,会报出 ConcurrentModificationException (并发修改异常)
if ("b".equals(next)) {
// coll.add("e");
// coll.remove("b");
// 如果需要删除,要使用迭代器自带的删除方法进行删除操作
iterator.remove();
}
}
System.out.println(coll);
}
}
注意点:
next
的作用: 1. 获取当前元素 , 2. 移动指针指向下一个元素- 迭代器的指针是不会复位的
- 如果迭代器指向没有元素的位置了,继续调用
next
就出现NoSuchElementException
(元素不存在异常) - 迭代器循环中 , next 应该只调用一次 (如果调用多次可能导致指针指向没有元素的位置,还继续 next )
- 迭代进行中 ,不要使用集合自带的方法对集合进行新增或者删除,如果需要删除可以调用 迭代器中 remove 方法进行删除。
增强for
public class Demo02 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("a");
coll.add("b");
coll.add("c");
coll.add("d");
// 增强for本质上就是迭代器 , 所有的单列集合和数组都可以是用该方式遍历
// 注意:这里使用的深拷贝,将集合中元素赋值给变量 s , 改变 s 变量不会影响集合本身
for (String s : coll) {
System.out.println("s = " + s);
s = "cc";
}
}
}
使用 lambda 遍历
public class Demo03 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("a");
coll.add("b");
coll.add("c");
coll.add("d");
// forEach 接收的一个函数式接口的匿名内部类
// lambda 底层如果集合的拓展性数组,底层使用索引遍历,并且将遍历到的元素,传递给匿名内部类的accept方法
coll.forEach(s-> System.out.println("s = " + s));
}
}
拓展:函数式接口
1. 定义
函数式接口是 Java 8 引入的一个概念,指有且仅有一个抽象方法的接口。函数式接口的主要目的是支持 Lambda 表达式和方法引用,从而简化代码并支持函数式编程风格。
2. 特点
- 只有一个抽象方法:
- 函数式接口中只能有一个抽象方法。
- 可以有多个默认方法(
default
方法)或静态方法(static
方法)。 - 可以包含
Object
类中的公共方法(如toString
、equals
等),这些方法不计入抽象方法的数量。
@FunctionalInterface
注解:- 用于显式标记一个接口为函数式接口。
- 如果接口被
@FunctionalInterface
修饰,但不符合函数式接口的定义(如没有抽象方法或多个抽象方法),编译器会报错。
3. 示例
@FunctionalInterface
interface MyFunctionalInterface {
void doSomething(); // 唯一的抽象方法
default void doSomethingElse() {
System.out.println("Default method");
}
static void staticMethod() {
System.out.println("Static method");
}
}
- 上述接口
MyFunctionalInterface
被@FunctionalInterface
修饰,且只有一个抽象方法doSomething
,因此它是一个合法的函数式接口。 - default 和 static 修饰的方法都不是抽象方法
4. Lambda 表达式的支持
函数式接口可以用 Lambda 表达式或方法引用来实现。例如:
public class Main {
public static void main(String[] args) {
// 使用 Lambda 表达式实现函数式接口
MyFunctionalInterface func = () -> System.out.println("Doing something");
func.doSomething(); // 输出: Doing something
}
}
5. 常见的函数式接口
Java 标准库中提供了许多常用的函数式接口,位于 java.util.function
包中:
Consumer<T>
:接受一个输入参数,无返回值。Supplier<T>
:无输入参数,返回一个结果。Function<T, R>
:接受一个输入参数,返回一个结果。Predicate<T>
:接受一个输入参数,返回一个布尔值。Runnable
:无输入参数,无返回值。
6. 注意事项
@FunctionalInterface
是可选的:- 即使不使用
@FunctionalInterface
注解,只要接口符合函数式接口的定义,仍然可以用 Lambda 表达式实现。 - 使用注解的目的是为了显式声明接口的用途,并让编译器进行检查。
- 即使不使用
- 抽象方法的数量:
- 如果接口中有多个抽象方法,即使使用
@FunctionalInterface
注解,编译器也会报错。
- 如果接口中有多个抽象方法,即使使用
7. 总结
- 函数式接口的定义:有且仅有一个抽象方法的接口。
@FunctionalInterface
的作用:显式标记接口为函数式接口,确保接口中只有一个抽象方法。- 支持 Lambda 表达式:函数式接口可以用 Lambda 表达式或方法引用来实现,简化代码。
- 常见用途:简化代码,支持函数式编程风格。
重点:函数式接口特点:接口中有且只有一个抽象方法。
List 系列集合
注意点:
-
list 系列的特点:有序、有索引、可重复
-
其中的 remove 方法的注意点
public class Demo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); // 问题:这里删除索引为 1 的元素,也就是 2 ,为什么不是删除 1 元素呢 // 原因:当方法出现重载的时候,优先使用实参和形参数据类型一样的那个方法 // list.remove(1); // 如果将 1 进行装箱 Integer i = Integer.valueOf(1); list.remove(i); // 删除 元素1 System.out.println(list); // [1 , 3] } }
细节:当方法出现重载的时候,实参和形参数据类型一致的哪个方法会被优先调用。
ListIterator 迭代器遍历
使用 ListIterator 迭代器进行遍历
public class Demo02 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
ListIterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
System.out.println(next);
if (2 == next) {
// ListIterator 迭代器允许在迭代进行中进行使用迭代器自带的方法进行新增或者删除
// 但是不能同时进行, 同时进行 IllegalStateException (非法状态异常)
iterator.add(5);
// iterator.remove();
}
}
System.out.println(list);
}
}
License:
CC BY 4.0