【Java集合框架】3 - 2 Collection 单列集合顶级接口
§3-2 Collection
单列集合顶级接口
3-2.1 单列集合类体系结构
在正式学习集合框架前,先来看看集合框架的大体体系结构。
整个集合框架大体可分为以下两部分:
Collection
:单列集合,一次存储一个数据;Map
:双列集合,一次存储一对数据;
这两部分都由大量的接口、抽象类和实现类构成。我们先来看看单列集合的类结构图:
可以看到,集合框架的设计是先定义好接口(定义一组标准的方法),再设计实现了接口的抽象类(对部分方法做通用实现或抛出异常 UnsupportedOperationException
),实现子类再通过继承抽象类和实现接口以实现某个具体的数据结构。这种设计提高了接口的灵活性(按需重写方法)、可扩展性(接口变动不影响实现类,只需要更改相应的抽象类实现),以及满足了抽象类和接口的互补(接口规范行为,抽象类补充属性和接口实现的解耦)。
这种设计方法也用在了在后面的多线程和并发编程所用的锁中。
注意到,单列集合中含有用于并发编程的并发集合,这些集合在并发环境中是线程安全的,这些集合会在多线程与 JUC 并发编程章节中介绍。
单列集合的类体系结构比较庞大,但我们只需要重点关注其中几个常用的数据结构即可。
3-2.2 单列集合常用的数据结构
现在,我们来看看 Collection
单列集合常用的几个数据结构,如下图所示:
Collection
集合又被分为了两类, List
和 Set
:
List
系列集合:添加的元素是有序的、可重复、有索引;Set
系列集合:添加的元素是无序的、无重复、无索引;
其中的 “有序” 并非指前一章节所言的经排序后的有序,而是数据的存取顺序一致。
其中,Vector
已经被市场所淘汰,仅作了解即可,不需要深入学习。
3-2.3 集合与泛型
在 JDK 5 前,Java 还不支持泛型,这意味着,对于一个集合,它可以允许任意一种数据类型(必须为引用类型)存入集合中。
这种方法在遍历的时候就会出现问题,此时集合中的元素的类型为 Object
,使得集合能够存放各种类型的数据,但受限于多态,在不进行强制类型转换的情况下,无法调用子类的特有方法。
而强行使用强制类型转换所带来的可能后果就是可能会抛出 ClassCastException
异常,而遍历时做类型判断和强制转换会使得代码变得十分臃肿,且不易于编写,十分麻烦。
JDK 5 引入的泛型就能很好地解决这一问题,它限定将集合中的元素类型,避免了多态所产生的上述问题,将运行时可能发生的异常提前到了编译时期,有助于开发者更好地定位、解决问题。
但也要注意,Java 中的泛型实际上是伪泛型。在集合中,Java 仅仅保证在添加元素时的元素的数据类型,实际上,元素一旦进入集合,其仍然是以 Object
的形式存储。当要取出元素时,再次通过泛型将元素强制转换。
这一点通过源码分析和反编译字节码文件可以很好地体现。
3-2.4 Collection
接口
Collection
是单列集合的父类接口,其功能可以由所有单列集合使用。
方法:
方法 | 描述 |
---|---|
boolean add(E e) |
把给定的对象添加到当前集合中 |
void clear() |
清空集合中所有元素 |
boolean remove(E e) |
删除集合中的给定对象 |
boolean contains(Object obj) |
判断当前集合中是否包含给定对象 |
boolean isEmpty() |
判断当前集合是否为空 |
int size() |
返回集合中元素个数 / 集合大小(长度) |
注意:
- 在
List
系列中使用add()
添加元素,方法总是返回true
,表示成功添加元素; - 在
Set
系列中使用add()
添加元素,若该元素不存在,则返回true
,并成功添加该元素;反之失败,返回false
; - 在
List
系列中,重载了remove()
方法,使得其可以通过索引删除指定元素,但该重载并不存在于Collection
和Set
中; - 使用
remove()
方法删除指定元素,若该元素存在,则将其删除,并返回true
;若不存在,则删除失败,返回false
; - 使用
contains()
方法判断元素存在性时,在底层调用的是equals()
方法,因此,在 Java Bean 类中,应当重写equals()
方法; isEmpty()
方法在底层实际上就是判断当前集合的大小是否为 0;
3-2.5 遍历方式
Collection
内元素有多种不同的遍历方式。
3-2.5.1 使用迭代器遍历
Iterator<E>
是一个接口,位于 java.util
包下。迭代器是集合专用的遍历方式,其最大特点是不依赖索引。
方法:
方法 | 描述 |
---|---|
Iterator<E> iterator() |
返回迭代器对象,默认指向当前集合的起始处 |
boolean hasNext() |
判断当前位置是否有元素,有则返回 true ,无则返回 false |
E next() |
获取当前位置元素,并将迭代器对象移向下一位置 |
void remove() |
从集合中,删除迭代器返回的最后一个元素(可选操作) |
示例:使用迭代器遍历
public static void main(String[] args) {
//创建集合
Collection<String> coll = new ArrayList<>();
//添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//使用迭代器遍历
//通过集合获取迭代器对象,默认指向起始处
Iterator<String> it = coll.iterator();
while (it.hasNext()) { //判断当前位置是否存在元素
System.out.println(it.next()); //返回当前位置的元素,并移动迭代器
}
}
注意:
-
遍历完毕后,迭代器不会复位,若需要二次遍历,只能用同样的方法获取新的迭代器对象;
-
若返回不存在的元素(不存在更多元素)时,会抛出异常
NoSuchElementException
; -
循环中只能用一次
next()
方法; -
使用迭代器遍历时,不能用集合的方法添加或删除元素,否则抛出异常
ConcurrentModificationException
(并发修改异常);此时,应当考虑使用迭代器中的
remove()
方法,但注意,该方法删除的是迭代器上一次返回的元素;暂时无法在此时临时添加元素;
3-2.5.2 增强 for
循环遍历
增强 for
自 JDK 5 后出现,其底层就是迭代器,为了简化迭代器的代码书写。
增强 for
使用了第三方变量接收可迭代对象或数组的对应值。
仅所有的单列集合和数组才可以使用增强 for
遍历。
示例:
public static void main(String[] args) {
//创建集合
Collection<String> coll = new ArrayList<>();
//添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//增强 for 遍历:单列集合和数组
for (String s: coll) {
System.out.println(s);
}
}
注意:
- 由于增强
for
使用第三方变量记录数据,修改第三方变量不会修改可迭代对象或数组中的值;
3-2.5.3 Lambda 表达式遍历
JDK 8 引入了 Lambda 表达式,提供了一种更简单、更直接的遍历方式。
方法 | 描述 |
---|---|
default void forEach(Consumer<? super T> action) |
结合 Lambda 表达式遍历集合 |
该方法位于接口 java.lang.Iterable
中,其中,Consumer<T>
是一个位于 java.util.function
包下的函数式接口,代表一种接受一个输入参数且无返回值的操作,可用 Lambda 表达式简化。
forEach()
默认方法体:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
示例:
public static void main(String[] args) {
//创建集合
Collection<String> coll = new ArrayList<>();
//添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//使用匿名内部类
coll.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//Lambda 表达式
coll.forEach(s -> System.out.println(s));
}
ArrayList
重写了父接口 Iterable
中的 forEach
方法,其内部使用标准 for
循环。
3-2.6 参考
Java集合为什么设计为:实现类继承了抽象类,同时实现抽象类实现的接口 - 渊渟岳 - 博客园 (cnblogs.com)