第六章 集合
集合(Collection)是Java中的一种重要数据结构,用于存储一组对象。在Java中,集合主要分为两大类:实现Collection
接口的集合和实现Map
接口的集合。接下来,我们将详细讨论这两大类集合的概念、接口、实现类及相关操作。
6.1 集合概述
集合存储结构两大类
- Collection接口体系:包括
List
、Set
等接口及其实现类,用于存储一组不唯一(List允许重复,Set不允许重复)的对象。 - Map接口体系:用于存储键值对(Key-Value),其中键(Key)是唯一的,值(Value)可以重复。
思维图表示关系
集合框架 | |
├── Collection接口 | |
│ ├── List接口 | |
│ │ ├── ArrayList | |
│ │ ├── LinkedList | |
│ │ └── ... | |
│ └── Set接口 | |
│ ├── HashSet | |
│ ├── TreeSet | |
│ └── ... | |
└── Map接口 | |
├── HashMap | |
├── TreeMap | |
├── Properties | |
└── ... |
6.2 Collection 接口
概念
Collection
接口是Java集合框架中的根接口,提供了对集合对象进行基本操作的通用方法。
使用场景及条件
- 当你需要存储一组对象,而不关心对象存储的顺序和是否唯一时,可以使用
Collection
接口及其实现类。
注意事项
Collection
接口不支持存储重复元素(除List
外),且不允许包含null
元素(具体实现类可能有所不同)。
常用方法
方法声明 | 功能描述 |
---|---|
boolean add(E e) | 向集合中添加一个元素,若成功返回true |
boolean remove(Object o) | 从集合中移除一个元素,若成功返回true |
boolean contains(Object o) | 判断集合中是否包含某个元素 |
int size() | 返回集合中元素的个数 |
boolean isEmpty() | 判断集合是否为空 |
void clear() | 清空集合中的所有元素 |
Iterator<E> iterator() | 返回一个迭代器用于遍历集合 |
Object[] toArray() | 将集合转换为数组 |
<T> T[] toArray(T[] a) | 将集合转换为指定类型的数组 |
boolean containsAll(Collection<?> c) | 判断集合是否包含另一个集合的所有元素 |
boolean addAll(Collection<? extends E> c) | 向集合中添加另一个集合的所有元素 |
boolean removeAll(Collection<?> c) | 从集合中移除另一个集合的所有元素 |
boolean retainAll(Collection<?> c) | 保留集合中与另一个集合相同的元素 |
6.3 List接口
6.3.1 List接口简介
List
接口是Collection
接口的子接口,用于存储有序的集合,允许元素重复。
常用方法
方法声明 | 功能描述 |
---|---|
E get(int index) | 获取指定位置的元素 |
E set(int index, E element) | 将指定位置的元素替换为新的元素,并返回旧元素 |
void add(int index, E element) | 在指定位置插入元素 |
E remove(int index) | 移除指定位置的元素,并返回该元素 |
int indexOf(Object o) | 返回指定元素在集合中首次出现的位置 |
int lastIndexOf(Object o) | 返回指定元素在集合中最后一次出现的位置 |
List<E> subList(int fromIndex, int toIndex) | 返回集合中指定范围的子列表(视图) |
boolean isEmpty() | 判断列表是否为空 |
使用场景及条件
- 当你需要存储一组有序的对象,且允许对象重复时,可以使用
List
接口及其实现类。
注意事项
List
接口的实现类如ArrayList
和LinkedList
在底层实现上有所不同,ArrayList
基于数组实现,LinkedList
基于链表实现,因此它们在性能上各有优劣。
6.3.2 ArrayList集合
ArrayList
是基于数组实现的List
接口,支持动态扩容。
6.3.3 LinkedList集合
LinkedList
是基于链表实现的List
接口,支持高效的插入和删除操作。
LinkedList集合新增和删除元素过程
- 新增元素:在链表尾部新增元素时,只需调整尾指针和新增节点的指针;在链表中间或头部新增元素时,需要遍历找到插入位置,并调整相关节点的指针。
- 删除元素:在链表尾部删除元素时,只需调整尾指针和前一个节点的指针;在链表中间或头部删除元素时,需要遍历找到删除位置,并调整相关节点的指针。
6.3.4 Iterator接口
概念
Iterator
接口用于遍历集合中的元素,是一种通用的迭代器设计模式。
使用场景及条件
- 当你需要遍历集合中的元素,而不关心元素的存储顺序时,可以使用
Iterator
接口。
注意事项
- 在使用
Iterator
遍历集合时,不能通过集合对象的方法修改集合(如添加、删除元素),否则会抛出ConcurrentModificationException
异常。
Iterator对象迭代元素的过程
- 调用集合的
iterator()
方法获取Iterator
对象。 - 使用
Iterator
对象的hasNext()
方法判断集合中是否还有元素。 - 使用
Iterator
对象的next()
方法获取下一个元素。 - 可以使用
Iterator
对象的remove()
方法删除迭代器上一次返回的元素(可选)。
6.3.5 foreach 循环
概念
foreach
循环(也称为增强型for循环)是Java 5引入的一种简化数组和集合遍历的语法。
使用场景及条件
- 当你需要遍历数组或集合中的元素时,可以使用
foreach
循环。
注意事项
foreach
循环实际上是通过迭代器来实现的,因此它不支持在遍历过程中修改集合(如添加、删除元素)。
具体语法格式
for (类型 元素 : 数组或集合) { | |
// 循环体 | |
} |
foreach循环的局限性
- 无法在遍历过程中修改集合(如添加、删除元素)。
- 无法获取元素的索引(数组除外)。
6.4 Set接口
6.4.1 Set接口简介
Set
接口是Collection
接口的子接口,用于存储不重复的元素。
使用场景及条件
- 当你需要存储一组不重复的对象时,可以使用
Set
接口及其实现类。
注意事项
Set
接口的实现类如HashSet
和TreeSet
在底层实现上有所不同,HashSet
基于哈希表实现,TreeSet
基于红黑树实现,因此它们在性能上各有优劣。
6.4.2 HashSet集合
概念
HashSet
是基于哈希表实现的Set
接口,不保证集合的迭代顺序。
使用场景及条件
- 当你需要存储一组不重复的对象,且不关心对象的存储顺序时,可以使用
HashSet
。
注意事项
HashSet
允许null
元素的存在,但最多只能有一个null
元素。HashSet
不保证集合的迭代顺序,因此迭代顺序可能与添加顺序不同。
HashSet存储元素的流程
- 调用
HashSet
的add
方法添加元素。 - 计算元素的哈希值,并定位到哈希表中的桶(Bucket)。
- 如果桶中为空,则直接将元素添加到桶中。
- 如果桶中已有元素,则比较元素的哈希值和
equals
方法,如果相同则不添加,否则链式存储到桶中。
6.4.3 TreeSet集合
概念
TreeSet
是基于红黑树实现的Set
接口,保证集合的元素处于排序状态。
使用场景及条件
- 当你需要存储一组不重复的对象,且希望集合中的元素处于排序状态时,可以使用
TreeSet
。
注意事项
TreeSet
不允许null
元素的存在。TreeSet
中的元素必须实现Comparable
接口,或者构造TreeSet
时传入一个Comparator
对象,否则在添加元素时会抛出ClassCastException
异常。
6.5 Map 接口及常用实现
6.5.1 Map 接口简介
概念:
Map接口位于java.util
包中,专门用于表示一组键值对映射。Map中的每个键与值一一对应,且键是唯一的。Map本身并不继承自Collection接口,但它提供了一些类似于集合的操作方法。
使用场景:
- 需要键值对存储时,如数据库查询结果、缓存等。
- 需要快速根据键查找值时。
使用条件:
- 键必须是唯一的,不能重复。
- 值可以重复。
为什么要用:
- 提供了灵活的键值对存储和查找机制。
- 可以根据键快速获取值,性能高效。
注意事项:
- 选择合适的Map实现类,如HashMap、TreeMap、Properties等,根据具体需求选择。
- 注意线程安全性和性能之间的平衡。
常用方法:
方法声明 | 功能描述 |
---|---|
V put(K key, V value) | 将指定的键值对插入Map中,返回以前的值(如果有)或null。 |
V get(Object key) | 返回指定键所映射的值,如果不包含该键,则返回null。 |
V remove(Object key) | 移除键对应的键值对,返回被移除的值。 |
boolean containsKey(Object key) | 如果Map中包含指定的键,则返回true。 |
boolean containsValue(Object value) | 如果Map中存在一个或多个键映射到指定值,则返回true。 |
Set<K> keySet() | 返回Map中所有键的集合。 |
Collection<V> values() | 返回Map中所有值的集合。 |
Set<Map.Entry<K, V>> entrySet() | 返回Map中所有键值对的集合视图。 |
V replace(K key, V value) | 替换指定键的值。 |
boolean replace(K key, V oldValue, V newValue) | 如果当前映射包含指定键的指定值,则替换该值。 |
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | 尝试计算指定键及其当前映射值的重新映射函数。 |
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) | 如果指定键尚未与某个值相关联(或关联为null),则将其与给定值关联,或者如果已关联一个值,则使用给定的重新映射函数计算新值。 |
6.5.2 HashMap 集合
概念:
HashMap是Java集合框架中的一个重要实现,用于存储键值对。它基于哈希表的原理,通过哈希函数将键映射到桶(bucket)中,从而实现快速的插入、删除和查找操作。
使用场景:
- 适用于大多数对性能要求较高且不需要线程安全的场合。
- 需要快速插入、删除和查找键值对时。
使用条件:
- 允许使用null值和null键。
- 不保证键值对的顺序。
为什么要用:
- 提供了高效的键值对存储和查找机制。
- 性能优于其他Map实现类(如TreeMap)。
注意事项:
- 扩容机制:当元素数量超过容量乘以负载因子(默认0.75)时,HashMap会进行扩容,将容量扩大为原来的两倍,并重新哈希所有元素。
- 链表和红黑树:当多个键的哈希值相同时,这些键会被存储在同一个桶中,形成一个链表。在Java 8及以后,当链表长度超过一定阈值(默认是8)时,链表会转换为红黑树,以提高在高碰撞情况下的性能。
6.5.3 TreeMap 集合
概念:
TreeMap是一种基于红黑树(Red-Black tree)的NavigableMap的实现。它提供了一种可排序的键值对集合。
使用场景:
- 需要对键进行排序的场合。
- 需要按照键的顺序进行遍历的场合。
使用条件:
- 默认情况下采取自然排序,也可以在构造函数中接受一个Comparator实例来定制排序规则。
- 不允许使用null键。
为什么要用:
- 提供了有序的键值对存储和查找机制。
- 可以通过键的顺序进行遍历和查找。
注意事项:
- TreeMap的时间复杂度为O(log n),适用于大规模数据量下的高效存储和检索。
- 不允许使用null键,否则会抛出NullPointerException。
6.5.4 Properties集合
概念:
Properties集合是一种用于处理键值对数据的集合,它继承自Hashtable类,提供了一种方便的方式来存储和读取配置信息。
使用场景:
- 用于读取和写入配置信息,如应用程序的配置文件、数据库连接信息等。
- 需要将键值对数据持久化到文件中的场合。
使用条件:
- 键和值都是字符串类型。
- 线程安全,可以在多线程环境中使用。
为什么要用:
- 提供了方便的配置信息存储和读取机制。
- 支持持久化存储和读取键值对数据。
注意事项:
- 使用
setProperty(key, value)
方法添加键值对。 - 使用
getProperty(key)
方法获取指定键对应的值。 - 使用
load(InputStream)
方法从文件中加载Properties集合的数据。 - 使用
store(OutputStream, comments)
方法将Properties集合的数据保存到文件。
6.6 泛型
6.6.1 泛型概述
概念:
泛型是Java SE 5引入的一个语言特性,它提供了一种在编译时进行类型检查的机制,使得代码更加安全、灵活和易于维护。
使用场景:
- 需要编写可以处理多种类型数据的类或方法时。
- 需要提高代码的可重用性和类型安全性时。
使用条件:
- 泛型类、泛型方法、泛型接口等都需要在声明时指定泛型类型参数。
为什么要用:
- 提高了代码的可重用性和类型安全性。
- 减少了运行时类型转换错误的可能性。
注意事项:
- 泛型信息在运行时会被擦除,泛型类型参数会被替换为它们的限定类型(无限定类型时则为Object)。
- 无法创建泛型数组。
- 无法直接实例化泛型类型参数,需要通过反射或其他方式创建实例。
6.6.2 泛型类和泛型对象
概念:
泛型类是指在类定义时使用了泛型类型参数的类。泛型对象是指使用泛型类创建的实例。
声明格式:
public class GenericClass<T> { | |
private T field; | |
// 构造方法、方法等 | |
} |
创建语法格式:
GenericClass<String> genericObject = new GenericClass<>(); |
使用场景:
- 需要编写可以处理多种类型数据的类时。
- 需要提高代码的可重用性和类型安全性时。
使用条件:
- 在创建泛型对象时需要指定具体的泛型类型参数。
为什么要用:
- 提高了代码的可重用性和类型安全性。
- 使得类可以处理多种类型的数据,而不需要编写多个类似的类。
注意事项:
- 泛型信息在运行时会被擦除,因此无法通过反射获取泛型类型参数的具体类型。
6.6.3 泛型方法
概念:
泛型方法是指在方法定义时使用了泛型类型参数的方法。
定义格式:
public <T> void genericMethod(T param) { | |
// 方法体 | |
} |
使用场景:
- 需要编写可以处理多种类型参数的方法时。
- 需要提高方法的可重用性和类型安全性时。
使用条件:
- 在调用泛型方法时需要指定具体的泛型类型参数(有时可以通过类型推断自动确定)。
为什么要用:
- 提高了方法的可重用性和类型安全性。
- 使得方法可以处理多种类型的数据,而不需要编写多个类似的方法。
注意事项:
- 泛型方法可以在普通类和泛型类中定义。
- 泛型信息在运行时会被擦除,因此无法通过反射获取泛型方法的具体类型参数。
6.6.4 泛型接口
概念
泛型接口是Java泛型编程的一个重要特性,它允许在接口定义时引入类型参数,从而使得接口能够处理多种数据类型,提高了代码的复用性和灵活性。
声明泛型接口格式
声明泛型接口的格式与声明泛型类类似,在接口名称后添加尖括号,并在尖括号中指定类型参数。例如:
public interface MyGenericInterface<T> { | |
void doSomething(T param); | |
} |
定义泛型接口的子类两种方式
-
在子类定义时指定具体类型:
在子类实现泛型接口时,可以直接在子类名称后指定具体的类型参数。例如:
public class MyConcreteClass implements MyGenericInterface<String> {
@Override
public void doSomething(String param) {
System.out.println("Doing something with: " + param);
}
}
-
在子类定义时声明泛型类型:
子类也可以在定义时声明自己的泛型类型,然后在实现接口时使用这些泛型类型。例如:
public class MyGenericClass<T> implements MyGenericInterface<T> {
@Override
public void doSomething(T param) {
System.out.println("Doing something with generic type: " + param);
}
}
使用场景与条件
泛型接口常用于需要处理多种数据类型的场景,如集合框架中的List、Set、Map等接口。使用泛型接口可以确保集合中的元素类型一致,避免类型转换错误。
为什么用
使用泛型接口可以提高代码的复用性和安全性。复用性体现在可以通过泛型接口处理多种数据类型,而不需要为每种数据类型编写专门的接口。安全性则体现在编译器可以在编译时检查类型匹配,减少运行时错误。
注意事项
- 在定义泛型接口时,要确保接口中的方法参数和返回值类型都使用了泛型类型参数。
- 在实现泛型接口时,如果子类没有指定具体的泛型类型,则需要在子类定义时声明自己的泛型类型。
6.6.5 类型通配符
概念
类型通配符(Type Wildcard)是Java泛型编程中的另一个重要概念,它允许在泛型类型声明时使用问号(?)作为类型参数的占位符,从而提高了代码的灵活性和通用性。
使用场景与条件
类型通配符常用于需要处理未知类型的泛型集合时。例如,当你有一个List集合,但你不知道集合中元素的具体类型时,可以使用List<?>来表示。
为什么用
使用类型通配符可以提高代码的灵活性,允许在不知道具体类型的情况下操作泛型集合。同时,类型通配符还可以结合边界(extends和super)来限制泛型类型的范围,从而提高代码的安全性。
注意事项
- 当使用类型通配符时,不能在方法内部添加具体类型的元素到集合中,因为编译器无法确定集合的具体类型。
- 可以使用通配符捕获(Wildcard Capture)和辅助方法来处理这种情况,即在方法内部将类型通配符的集合转换为具体类型的集合。
- 在使用类型通配符时,要注意选择合适的边界(extends或super),以确保代码的正确性和安全性。
6.7 JDK8新特性——Lambda表达式
概念
Lambda表达式是Java 8引入的一项新特性,它允许以更简洁的方式表示匿名函数或代码块,主要用于函数式编程和简化代码。
六个Lambda表达式常用的语法格式
-
无参数,无返回值:
Runnable runnable = () -> System.out.println("Hello Lambda!");
-
一个参数,无返回值:
Consumer<String> consumer = x -> System.out.println(x);
-
有参数,有返回值(单条语句):
Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y);
-
有参数,有返回值(多条语句):
Comparator<Integer> comparatorWithStatements = (x, y) -> {
System.out.println("Comparing...");
return Integer.compare(x, y);
};
-
省略参数类型(类型推断):
Comparator<Integer> comparatorWithInference = (Integer x, Integer y) -> Integer.compare(x, y);
// 可以简化为:
Comparator<Integer> comparatorWithInferenceSimplified = (x, y) -> Integer.compare(x, y);
-
方法引用:
Consumer<String> consumerWithMethodReference = System.out::println;
使用场景与条件
Lambda表达式常用于需要传递行为(即函数式接口的实现)的场景,如集合操作(forEach、filter、map等)、排序和比较(Comparator接口)、并行和多线程(并行流)、事件处理等。
为什么用
使用Lambda表达式可以使代码更简洁、更紧凑,同时提高代码的可读性和可维护性。此外,Lambda表达式还支持并行流和多线程编程,可以更容易地编写并发代码。
注意事项
- Lambda表达式需要函数式接口的支持,即接口中只能有一个抽象方法。
- 在使用Lambda表达式时,要确保方法参数和返回值类型与函数式接口中的抽象方法一致。
- Lambda表达式中的局部变量必须是final的(或在Java 8中,实际上是“有效地final”的),即不能在Lambda表达式内部修改这些变量的值。
- 在使用Lambda表达式进行集合操作时,要注意避免类型转换错误和空指针异常。
6.8 本章小结
1. 集合概述
集合框架是Java中用于存储和操作一组对象的统一架构。它提供了一种标准的方式来处理对象集合,包括各种数据结构和算法。集合框架的主要目的是通过提供一种统一的操作集合对象的方式,来简化对象的管理和操作。
2. Collection 接口
Collection 是所有单列集合的根接口,它定义了集合的基本操作,如添加、删除和遍历。所有具体的集合类(如 `List`、`Set` 和 `Map`)都继承自 `Collection` 接口或其子接口。
3. List接口
List`是一个有序的集合,可以包含重复的元素。它继承自 `Collection` 接口。
3.1 List接口简介
List提供了对元素的有序访问,并且可以精确控制每个元素的插入位置。
3.2 ArrayList集合
ArrayList 是基于动态数组实现的 `List` 接口的实现类,它允许快速随机访问,但在插入和删除操作时可能需要数组复制,影响性能。
3.3 LinkedList集合
LinkedList是基于双向链表实现的 `List` 接口的实现类,它在进行插入和删除操作时性能较好,但随机访问性能较差。
3.4 Iterator接口
Iterator 提供了一种遍历集合的方法,它定义了 `hasNext()`、`next()` 和 `remove()` 等方法,用于遍历集合中的元素。
3.5 foreach 循环
Java 提供了增强的 `foreach` 循环,使得遍历集合更加简洁和直观。
4. Set接口
Set 是一个不允许重复的集合。
4.1 Set接口简介
Set 接口继承自 Collection,它的实现类必须保证元素的唯一性。
4.2 HashSet集合
HashSet 是基于哈希表实现的 `Set` 接口的实现类,它提供了快速的插入、删除和查找操作。
4.3 TreeSet集合
TreeSet 是基于红黑树实现的 `Set` 接口的实现类,它可以按照自然顺序或自定义顺序对元素进行排序。
5. Map接口
Map 提供了键值对的存储,它继承自 `Collection` 接口。
5.1 Map 接口简介
Map接口存储键值对,它不允许键重复,但允许值重复。
5.2 HashMap 集合
HashMap是基于哈希表实现的 `Map` 接口的实现类,它提供了快速的键值对插入、删除和查找操作。
5.3 TreeMap 集合
TreeMap是基于红黑树实现的 `Map` 接口的实现类,它可以按照自然顺序或自定义顺序对键进行排序。
5.4 Properties集合
Properties 是 `Hashtable` 的子类,它继承自 `Map` 接口,用于处理属性文件。
6. 泛型
泛型提供了一种类型安全的机制,允许在编译时检查类型错误。
6.1 泛型概述
泛型允许在类、接口和方法中使用类型参数,从而提高代码的复用性和类型安全性。
6.2 泛型类和泛型对象
可以创建泛型类和对象,使用类型参数来指定具体的类型。
6.3 泛型方法
可以创建泛型方法,它们在方法级别使用类型参数。
6.4 泛型接口
可以创建泛型接口,它们在接口级别使用类型参数。
6.5 类型通配符
类型通配符提供了一种方式来表示不同类型的集合,如 `List<?>` 表示任何类型的 `List`。
7. JDK8新特性——Lambda表达式
Lambda表达式是JDK 8引入的新特性,它允许你以简洁的方式表示单方法接口的实现。