【Java集合框架】3 - 2 Collection 单列集合顶级接口

§3-2 Collection 单列集合顶级接口

3-2.1 单列集合类体系结构

在正式学习集合框架前,先来看看集合框架的大体体系结构。

整个集合框架大体可分为以下两部分:

  • Collection:单列集合,一次存储一个数据;
  • Map:双列集合,一次存储一对数据;

这两部分都由大量的接口、抽象类和实现类构成。我们先来看看单列集合的类结构图:

image

可以看到,集合框架的设计是先定义好接口(定义一组标准的方法),再设计实现了接口的抽象类(对部分方法做通用实现或抛出异常 UnsupportedOperationException),实现子类再通过继承抽象类和实现接口以实现某个具体的数据结构。这种设计提高了接口的灵活性(按需重写方法)、可扩展性(接口变动不影响实现类,只需要更改相应的抽象类实现),以及满足了抽象类和接口的互补(接口规范行为,抽象类补充属性和接口实现的解耦)。

这种设计方法也用在了在后面的多线程和并发编程所用的锁中。

注意到,单列集合中含有用于并发编程的并发集合,这些集合在并发环境中是线程安全的,这些集合会在多线程与 JUC 并发编程章节中介绍。

单列集合的类体系结构比较庞大,但我们只需要重点关注其中几个常用的数据结构即可。

3-2.2 单列集合常用的数据结构

现在,我们来看看 Collection 单列集合常用的几个数据结构,如下图所示:

image

Collection 集合又被分为了两类, ListSet

  • 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() 方法,使得其可以通过索引删除指定元素,但该重载并不存在于 CollectionSet 中;
  • 使用 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());    //返回当前位置的元素,并移动迭代器
     }
 }

注意

  1. 遍历完毕后,迭代器不会复位,若需要二次遍历,只能用同样的方法获取新的迭代器对象;

  2. 若返回不存在的元素(不存在更多元素)时,会抛出异常 NoSuchElementException

  3. 循环中只能用一次 next() 方法;

  4. 使用迭代器遍历时,不能用集合的方法添加或删除元素,否则抛出异常 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)

posted @ 2023-08-03 23:30  Zebt  阅读(49)  评论(0)    收藏  举报