跳转到内容

集合

75% developed
来自维基教科书,开放世界中的开放书籍

导航 聚合 主题: v   d   e )

最基本的集合接口称为 Collection。此接口为用户提供了集合的通用用法。所有集合都需要具有相同的基本操作。它们是

  • 向集合中添加元素
  • 从集合中删除元素
  • 获取集合中元素的数量
  • 列出集合的内容(遍历集合)
Computer code 代码清单 5.1: CollectionProgram.java
import java.util.Collection;   // Interface
import java.util.ArrayList;    // Implementation

public class CollectionProgram {

  public static void main(String[] args) {
    Collection myCollection = new ArrayList();
    myCollection.add("1");
    myCollection.add("2");
    myCollection.add("3");
    System.out.println("The collection contains " + myCollection.size() + " item(s).");

    myCollection.clear();
    if (myCollection.isEmpty()) {
      System.out.println("The collection is empty.");
    } else {
      System.out.println("The collection is not empty.");
    }
  }
}
Computer code 代码清单 5.1 的控制台
The collection contains 3 item(s).
The collection is empty.

当您将一个对象放入集合时,该对象实际上并不集合中。只有它的对象引用被添加到集合中。这意味着,如果一个对象在被放入集合后发生了改变,集合中的对象也会改变。 代码清单 5.2 计算从明天开始的接下来的七天,并将每个日期存储在一个列表中,以便之后读取。看看发生了什么

Computer code 代码清单 5.2: SevenNextDays.java
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;

public class SevenNextDays {

  public static void main(String[] args) {
   
    // The calendar is set at the current date: today
    Calendar calendar = new GregorianCalendar();

    Collection collectionOfDays = new ArrayList();
    Date currentDate = new Date();
    for (int i = 0; i < 7; ++i) {
      // The calendar is now set to the next day
      calendar.add(Calendar.DATE, 1);
      currentDate.setTime(calendar.getTimeInMillis());

      collectionOfDays.add(currentDate);
    }

    for (Object oneDay : collectionOfDays) {
      System.out.println("The next day is: " + oneDay);
    }
  }
}
Computer code 代码清单 5.2 的控制台

 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024

所有集合项都应该更新到不同的日期,但它们都被更新到最后一个日期。这意味着每次更新都会更新所有集合项。currentDate 已用于填充所有集合项。集合没有跟踪添加的值(七个日期中的一个),而是添加的对象引用 (currentDate)。因此集合包含了同一个对象七次!为了避免这个问题,我们应该这样编码

Computer code 代码清单 5.3: ActualSevenNextDays.java
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;

public class ActualSevenNextDays {

  public static void main(String[] args) {
   
    // The calendar is set at the current date: today
    Calendar calendar = new GregorianCalendar();

    Collection collectionOfDays = new ArrayList();
    for (int i = 0; i < 7; ++i) {
      Date currentDate = new Date();
      // The calendar is now set to the next day
      calendar.add(Calendar.DATE, 1);
      currentDate.setTime(calendar.getTimeInMillis());

      collectionOfDays.add(currentDate);
    }

    for (Object oneDay : collectionOfDays) {
      System.out.println("The next day is: " + oneDay);
    }
  }
}
Computer code 代码清单 5.3 的控制台

 下一天是: Wed Oct 9 9:09:19 UTC 2024
 下一天是: Thu Oct 10 9:09:19 UTC 2024
 下一天是: Fri Oct 11 9:09:19 UTC 2024
 下一天是: Sat Oct 12 9:09:19 UTC 2024
 下一天是: Sun Oct 13 9:09:19 UTC 2024
 下一天是: Mon Oct 14 9:09:19 UTC 2024
 下一天是: Tue Oct 15 9:09:19 UTC 2024

现在,每次我们在集合中添加一个项目时,它都是一个不同的实例。所有项目都独立发展。要将一个对象添加到集合中,并避免每次源对象发生变化时该对象也被改变,您必须在将对象添加到集合之前复制或克隆该对象。

放入集合中的对象会被向上转型为 Object 类。这意味着您需要在从集合中取出元素时将对象引用转换回来。这也意味着,您需要知道取出对象时的类型。如果一个集合包含不同类型的对象,我们将在运行时难以找出从集合中获取的对象类型。例如,让我们使用这个集合,它包含两个对象

Example 代码部分 5.1: 集合填充。
Collection ageList = new ArrayList();
ageList.add(new Integer(46));
ageList.add("50");
Example 代码部分 5.2: 集合读取。
Integer sum = new Integer(0);
for (Object age : ageList) {
    sum = sum + ((Integer) age);
}

if (!ageList.isEmpty()) {
    System.out.println("The average age is " + sum / ageList.size());
}
Computer code 代码部分 5.2 的控制台
ClassCastException.

可以通过使用泛型类型在编译时更早地发现此错误。 泛型 是从 JDK 1.5 版本开始添加的。它是对 Java 语言类型系统的增强。自 1.5 以来,所有集合实现现在都有一个参数化类型 <E>E 代表元素类型。在创建集合时,实际的元素类型将替换 E。在集合中,对象现在会被向上转型为E 类。

Example 代码部分 5.3: 带有泛型的集合。
Collection<Integer> ageList = new ArrayList<Integer>();
ageList.add(new Integer(46));     // Integer can be added
ageList.add("50");                // Compilation error, ageList can have only Integers inside

ageList 是一个集合,它只能包含 Integer 对象作为元素。当我们取出一个元素时,不需要进行类型转换。

Example 代码部分 5.4: 项目读取。
Integer age = ageList.get(0);

泛型不是强制性的,但通常与集合类一起使用。

集合类

[编辑 | 编辑源代码]

java.util.Collection 接口没有直接实现。Collection 接口有五个子接口。

图 1: java.util.Collection 接口的五个子接口。


一个集合包含唯一的元素,因此不允许重复。它类似于数学集合。在向集合中添加一个新项时,集合会调用项的 int hashCode() 方法,并将它的结果与所有已插入项的哈希码进行比较。如果找不到哈希码,则添加该项。如果找到了哈希码,集合会对所有与新项具有相同哈希码的集合项调用 boolean equals(Object obj); 方法。如果所有等于调用都返回 false,则将新项插入到集合中。如果等于调用返回 true,则不会将新项插入到集合中。

图 2: 集类图。


java.util.HashSet<E>
这是 Set 接口的基本实现。不是同步的。允许 null 元素
java.util.TreeSet<E>
元素是有序的,不是同步的。null 不允许
java.util.CopyOnWriteArraySet<E>
线程安全,在修改操作期间会创建新的副本。添加、更新、删除开销很大。
java.util.EnumSet<E extends Enum<E>>
枚举集中的所有元素都必须来自创建集合时显式或隐式指定的单个枚举类型。枚举集在内部表示为位向量。
java.util.LinkedHashSet<E>
与 HashSet 相同,另外它定义了迭代顺序,即元素插入到集合中的顺序。

在集中检测重复对象

[编辑 | 编辑源代码]

Set 中不能有重复项。您可能会想知道,当我们将一个对象添加到 Set 时,重复项是如何检测到的。我们需要查看该对象是否已经存在于 Set 中。仅仅检查对象引用是不够的,还需要检查对象的值。

幸运的是,每个 Java 对象都有一个 boolean equals(Object obj) 方法,它继承自 Object。您需要重写它。该方法将被 Set 实现调用,以比较两个对象,以查看它们是否相等。

不过,存在一个问题。如果我将两种不同类型的对象放入 Set 中怎么办。我放了一个苹果和一个橘子。它们无法比较。调用 equals() 方法会导致 ClassCastException。为此有两种解决方案

  • 解决方案一 : 重写 int hashCode() 方法,对于相同类型的对象返回相同的值,对于不同类型的对象返回不同的值。该 equals() 方法仅用于比较具有相同 hashCode 值的对象。因此,在添加对象之前,Set 实现需要
    • 在 Set 中查找所有与候选对象 hashCode 相同的 hashCode 的对象
    • 并对这些对象调用 equals() 方法,传入候选对象
    • 如果任何一个返回 true,则该对象不会被添加到 Set 中。
  • 解决方案二 : 为 Apple 和 Orange 创建一个超类,我们将其称为 Fruit 类。将 Fruits 放入 Set 中。您需要执行以下操作
    • 不要重写 Apple 和 Orange 类中的 equals()hashCode() 方法
    • 在 Apple 类中创建 appleEquals() 方法,并在 Orange 类中创建 orangeEquals() 方法
    • 重写 Fruit 类中的 hashCode() 方法并返回相同的值,以便 Set 实现调用 equals()
    • 重写 Fruit 类中的 equals() 方法,类似于以下内容。
Example 代码段 5.5: equals 方法实现。
public boolean equals(Object obj) {
    boolean ret = false;
    if (this instanceof Apple &&
          obj instanceof Apple) {
        ret = this.appleEquals(obj);
    } else if (this instanceof Orange &&
              obj  instanceof Orange) {
        ret = this.orangeEquals(obj);  
    } else {
        // Can not compare Orange to Apple
       ret = false;
    }
    return ret;
}

注意

  • 只有具有相同 hashCode 的对象才会被比较。
  • 您有责任重写 equals()hashCode() 方法。Object 中的默认实现将无法正常工作。
  • 如果您想消除值重复项,则仅重写 hashCode() 方法。
  • 如果您知道对象的价值不同,或者您只想阻止添加完全相同的对象,则不要重写 hashCode() 方法。
  • 注意,hashCode() 可能用于其他集合实现,例如在哈希表中快速查找对象。重写默认的 hashCode() 方法可能会影响那里的性能。
  • 默认的 hashCodes 对于每个创建的对象都是唯一的,因此如果您决定不重写 hashCode() 方法,则没有必要重写 equals() 方法,因为它不会被调用。

SortedSet

[edit | edit source]

SortedSet 接口与 Set 接口相同,只是 SortedSet 中的元素已排序。它扩展了 Set 接口。SortedSet 中的所有元素都必须实现 Comparable 接口,此外所有元素都必须可以相互比较。

请注意,如果排序集要正确地实现 Set 接口,则排序集维护的排序必须与 equals 一致。这是因为 Set 接口是根据 equals 操作定义的,但排序集使用其比较方法执行所有元素比较,因此被该方法视为相等的两个元素,从排序集的角度来看,是相等的。

由于 'Set' 的排序性质,SortedSet 接口具有其他方法。这些是

E first(); 返回第一个元素
E last(); 返回最后一个元素
SortedSet headSet(E toElement); 从第一个元素返回,到非包含 toElement
SortedSet tailSet(E fromElement); 从包含 fromElement 返回到结尾
SortedSet subSet(E fromElement, E toElement); 返回从 fromElement(包含)到 toElement(非包含)的元素范围。(如果 fromElement 和 toElement 相等,则返回的排序集为空。)

在列表集合中,元素按特定顺序放置,并且可以通过索引访问。允许重复,相同的元素可以添加到列表中两次。它具有以下实现

图 3: List 类图。


java.util.Vector<E>
同步,在多线程访问中使用,否则使用 ArrayList
java.util.Stack<E>
它扩展了 Vector 类,并具有五个操作,允许将向量视为堆栈。它表示对象的先进后出 (LIFO) 堆栈。
java.util.ArrayList<E>
List 接口的基本实现是 ArrayList。ArrayList 不是同步的,也不是线程安全的。Vector 是同步的,并且线程安全的。Vector 较慢,因为额外的开销使其线程安全。当只有一个线程访问列表时,请使用 ArrayList。每当您从列表中插入或删除元素时,都会有额外的开销来重新索引列表。当您有一个大型列表并且您有很多插入和删除操作时,请考虑使用 LinkedList
java.util.LinkedList<E>
非同步,更新操作比其他列表更快,易于用于堆栈、队列、双端队列。名称 LinkedList 意味着一种特殊的数据结构,其中元素/节点通过指针连接。
 Head               Node 1                   Node 2                     Node n
  ______
 | Size |          _________________        _______________            _____________
 |______|         |      | point   |       |      | point  |          |      |      |  
 | First|-------->| Data | to next |------>| Data | to next|-- ... -->| Data | null |
 | elem |         |______|_________|       |______|________|          |______|______|
 |______|                                                                 ^
 | Last |                                                                 |
 | elem |-----------------------------------------------------------------
 |______|

每个节点都与链表中的一个项目相关。要从链表中删除元素,需要重新排列指针。删除节点 2 后

 Head               Node 1                   Node 2                     Node n
  ______                                 _____________________
 | Size |          _________________    |   _______________   |       ______________
 |_- 1__|         |      | point   |    |  |      | point  |  |       |      |      |  
 | First|-------->| Data | to next |----   | Data | to next|   -...-->| Data | null |
 | elem |         |______|_________|       |______|________|          |______|______|
 |______|                                                                 ^
 | Last |                                                                 |
 | elem |-----------------------------------------------------------------
 |______|
javax.management.AtributeList<E>
表示 MBean 属性的值列表。在 AttributeList 中插入 Attribute 对象所使用的方法将覆盖超类 ArrayList 中的相应方法。这是为了确保 AttributeList 中包含的对象仅为 Attribute 对象。
javax.management.relation.RoleList<E>
RoleList 表示角色 (Role 对象) 列表。它在创建关系时用作参数,并且在尝试设置关系中的多个角色时(通过 'setRoles()' 方法)使用。它作为 RoleResult 的一部分返回,以提供成功检索的角色。
javax.management.relation.RoleUnresolvedList<E>
RoleUnresolvedList 表示 RoleUnresolved 对象列表,这些对象表示由于在尝试访问(读取或写入角色)时遇到的问题而未从关系中检索到的角色。

Queue

[edit | edit source]

Queue 接口提供额外的插入、提取和检查操作。有 FIFO(先进先出)和 LIFO(后进先出)队列。此接口向 Collection 接口添加了以下操作

E element() 检索但不删除此队列的头部。此方法与 peek 方法的区别仅在于如果此队列为空,它将抛出异常
boolean offer(E o) 如果可能,将指定元素插入此队列。
E peek() 检索但不删除此队列的头部,如果此队列为空,则返回 null
E poll() 检索并删除此队列的头部,如果此队列为空,则返回 null
E remove() 检索并删除此队列的头部。此方法与 poll 方法的区别在于如果此队列为空,它将抛出异常。
图 4: Queue 类图。


java.util.BlockingQueue<E>
在检索元素时等待队列变为非空,在存储元素时等待队列中有空间可用。最适合用于生产者-消费者队列。
java.util.PriorityQueue<E>
根据构造时指定的顺序/优先级对元素进行排序,不允许 null 元素。
java.util.concurrent.ArrayBlockingQueue<E>
按 FIFO 顺序对元素进行排序;同步,线程安全。
java.util.concurrent.SynchronousQueue<E>
每个 put 必须等待一个 take,反之亦然,没有内部容量,即使是容量为一的容量,元素只在您尝试获取它时才会出现;除非另一个线程试图删除它,否则您无法添加元素(使用任何方法)。

完整的 UML 类图

[edit | edit source]
图 5: Collection 接口及其实现的 UML 类图。


同步

[edit | edit source]

当您运行多个线程时,同步非常重要。请注意,同步并不意味着您的集合是线程安全的。线程安全的集合也称为并发集合。大多数流行的集合类都为单线程和多线程环境提供了实现。非同步实现始终更快。您可以在多线程环境中使用非同步实现,前提是您确保在任何给定时间只有一个线程更新集合。

Java 1.5 引入了新的 Java JDK 包,即java.util.concurrent。此包提供了一些专为多线程环境设计的集合实现。

下表列出了所有同步集合类

同步 非同步
List java.util.Vector java.util.ArrayList
java.util.Stack
java.util.LinkedList
java.util.concurrent.CopyOnWriteArrayList
java.util.TreeSet
java.util.HashSet
java.util.LinkHashSet
java.util.concurrent.CopyOnWriteArraySet

自定义集合

[编辑 | 编辑源代码]

Java JDK 集合实现非常强大且良好,因此您不太可能需要自己编写。不同集合的使用方式相同,但实现方式不同。如果现有的集合实现不满足您的需求,您可以编写自己的实现版本。您的实现版本只需要实现相同的java.util.Collection接口,然后您可以切换到使用您的实现,并且使用集合的代码无需更改。

如果您需要将相关(通常是相同类型的)对象保存在一个集合中,以便您可以使用 Collection 接口

  • 搜索特定元素
  • 列出元素
  • 通过使用集合基本操作(添加、删除、更新等)维护和/或更改元素的顺序
  • 通过索引号访问元素

使用Collection接口的优点是

  • 提供通用用法,正如我们上面所讨论的,很容易切换实现
  • 它使将一种类型的集合转换为另一种类型变得容易。

Collection接口定义了以下基本操作

boolean add(E o); 使用元素类型 E
boolean addAll(Collection c);
boolean remove(Object o);
boolean removeAll(Collection c);
boolean retainAll(Collection c); 如果集合因操作而更改,则返回true

请注意,在addAll()中,我们可以添加任何类型的集合。这就是使用 Collection 接口的妙处。您可以拥有一个LinkedList,只需调用addAll(list)方法并传入一个列表。您可以传入VectorArrayListHashSetTreeSetYourImpOfCollection等。所有这些不同类型的集合将被神奇地转换为LinkedList

让我们仔细看看这种魔法。转换很容易,因为Collection接口定义了遍历元素的标准方法。以下代码是LinkedListaddAll()方法的一种可能的实现。

Example 代码部分 5.6:集合传输。
import java.util.Collection
import java.util.Iterator
...
public boolean addAll(Collection coll) {
   int sizeBefore = this.size();
   Iterator iter = coll.iterator();
   while(iter.hasNext()) {
      this.add(iter.next());
   }
   if (sizeBefore > this.size()) {
      return true;
   } else {
      return false;
   }
}

上面的代码只是遍历传入的集合并将元素添加到链表中。您不必这样做,因为这已经定义了。您可能需要编写代码来遍历Customer集合

Example 代码部分 5.7:对集合进行迭代。
import java.util.Collection
import java.util.Iterator
import java.yourcompany.Customer
...
public String printCustomerNames(Collection customerColl) {
   StringBuffer buf = new StringBuffer();

   Iterator iter = customerColl.iterator();
   while(iter.hasNext()) {
      Customer cust = (Customer) iter.next();
      buf.append(cust.getName());
      buf.append( "\n" );
   }
  return buf.toString();
}

注意两点

  • 上面的代码将适用于所有类型的集合。
  • 我们必须知道集合中对象的类型,因为我们在其上调用了一个方法。


Clipboard

待办事项
添加一些类似于变量中的练习


华夏公益教科书