跳到内容

集合

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

导航 Aggregate 主题: 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 接口没有直接的实现。集合接口有五个子接口。

图 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 对象都从 Object 继承了 boolean equals(Object obj) 方法。你需要覆盖它。Set 实现将调用该方法来比较两个对象,以查看它们是否相等。

不过,这里存在一个问题。如果我把两种不同类型的对象放入 Set 中呢?例如,我放了一个苹果和一个橘子。它们不能被比较。调用 equals() 方法会导致 ClassCastException。这个问题有两个解决方案。

  • 解决方案一 : 覆盖 int hashCode() 方法,对于相同类型的对象返回相同的值,对于不同类型的对象返回不同的值。equals() 方法仅用于比较具有相同 hashCode 值的对象。因此,在添加对象之前,Set 实现需要:
    • 找到 Set 中所有与候选对象 hashCode 值相同的对象。
    • 对这些对象调用 equals() 方法,并将候选对象作为参数传递。
    • 如果其中任何一个方法返回 true,则不会将该对象添加到 Set 中。
  • 解决方案二 : 为苹果和橘子创建一个父类,我们称之为水果类。将水果放入 Set 中。你需要做以下操作:
    • 不要覆盖苹果类和橘子类中的 equals()hashCode() 方法。
    • 在苹果类中创建 appleEquals() 方法,在橘子类中创建 orangeEquals() 方法。
    • 覆盖水果类中的 hashCode() 方法,返回相同的值,这样 Set 实现就会调用 equals() 方法。
    • 覆盖水果类中的 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() 可能在其他集合实现中使用,例如在 Hashtable 中快速查找对象。覆盖默认的 hashCode() 方法可能会影响那里的性能。
  • 对于每个创建的对象,默认的 hashCodes 都是唯一的。所以,如果你决定不覆盖 hashCode() 方法,就没有必要覆盖 equals() 方法,因为它不会被调用。

SortedSet

[edit | edit source]

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

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

由于 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 相等,则返回的 SortedSet 为空。)

在 List 集合中,元素按特定顺序排列,可以通过索引访问。允许重复,同一个元素可以被添加到 List 中多次。它有以下实现:

图 3: List 类图。


java.util.Vector<E>
同步的,在多线程访问中使用,否则使用 ArrayList
java.util.Stack<E>
它扩展了 Vector 类,并增加了五个操作,允许将 Vector 视为一个栈。它表示一个后进先出 (LIFO) 对象栈。
java.util.ArrayList<E>
List 接口的基本实现是 ArrayList。ArrayList 不是同步的,不是线程安全的。Vector 是同步的,线程安全的。Vector 速度较慢,因为为了使其线程安全而增加了额外的开销。当只有一个线程访问 List 时,使用 ArrayList。每当你向 List 中插入或删除元素时,都会有额外的开销来重新索引 List。当你有很大的 List 并且有很多插入和删除操作时,可以考虑使用 LinkedList
java.util.LinkedList<E>
非同步的,更新操作比其他 List 更快,易于用作栈、队列、双端队列。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 操作,反之亦然,没有内部容量,即使是容量为 1 的情况也是如此,元素只有在你尝试取它时才会存在;除非另一个线程试图删除它,否则你不能添加元素 (使用任何方法)。

完整的 UML 类图

[编辑 | 编辑源代码]
图 5:Collection 接口及其实现的 UML 类图。


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

Java JDK 包在 Java 1.5 中引入了一个新包,即 java.util.concurrent。此包提供了一些专为在多线程环境中使用而设计的 Collection 实现。

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

同步 非同步
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) 方法,传入一个列表即可。您可以传入一个 Vector、一个 ArrayList、一个 HashSet、一个 TreeSet、一个 YourImpOfCollection……所有这些不同类型的集合将神奇地转换为一个 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

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


华夏公益教科书