内容


第 10 单元:Java 集合

创建和管理对象集合

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解 Java 集合框架的用途
  • 了解如何声明和使用 Java 数组、列表、集和映射
  • 了解装箱和拆箱
  • 了解如何让集合可迭代

Java 集合框架

大多数真实应用程序都会处理像文件、变量、来自文件的记录或数据库结果集这样的集合。Java 语言有一个复杂的集合框架,您可以使用它创建和管理各种类型的对象集合。本单元将介绍最常用的集合类并帮助您开始使用它们。

数组

大多数编程语言都包含数组 的概念来持有一个实体集合,Java 语言也不例外。数组实质上是相同类型的元素的集合。

可以通过两种方式声明数组:

  • 创建一个具有一定大小的数组,这个大小在数组的整个生存期中是固定的。
  • 创建一个具有一组初始值的数组。此集合的大小确定了数组的大小 — 它的大小恰好能够包含所有这些值,而且的它的大小在数组的整个生存期中是固定的。

声明一个数组

一般而言,可以像这样声明一个数组:

new elementType [arraySize]

可以通过两种方式创建一个整数元素数组。这条语句创建一个拥有 5 个元素的空间的数组,但它是空的:

// creates an empty array of 5 elements:
int[] integers = new int[5];

这条语句创建该数组并一次性初始化它:

// creates an array of 5 elements with values:
int[] integers = new int[] { 1, 2, 3, 4, 5 };

// creates an array of 5 elements with values (without the new operator):
int[] integers = { 1, 2, 3, 4, 5 };

初始值放在花括号内,使用逗号分隔。

另一种创建数组的方式是首先创建它,然后编写一个循环来初始化它:

int[] integers = new int[5];
for (int aa = 0; aa < integers.length; aa++) {
  integers[aa] = aa+1;
}

前面的代码声明一个包含 5 个元素的整数数组。如果尝试在该数组中放入 5 个以上的元素,Java 运行时就会抛出一个异常。您将在第 14 单元 中学习异常和如何处理它们。

加载数组

要加载数组,需要对从 1 一直到数组长度的整数执行循环(可以在数组上调用 .length 来获取它的长度,稍后将更详细地介绍此内容)。在本例中,循环在到达 5 时停止。

加载数组后,您可以像之前一样访问它:

Logger l = Logger.getLogger("Test");
for (int aa = 0; aa < integers.length; aa++) {
  l.info("This little integer's value is: " + integers[aa]);
}

此语法也有效,而且(因为它更容易使用)本单元全部使用此语法:

Logger l = Logger.getLogger("Test");
for (int i : integers) {
  l.info("This little integer's value is: " + i);
}

元素索引

可以将数组想象为一系列的桶,每个桶中放入一个某种类型的元素。每个桶均可通过一个元素索引 来访问:

element = arrayName [elementIndex];

要访问一个元素,您需要数组的引用(它的名称)和包含您想要的元素的索引。

length 属性

每个数组有一个 length 属性,该属性具有 public 可视性,可用于确定可将多少个元素放入该数组中。要访问此属性,可使用数组引用、一个句点 (.) 和关键字 length,就象这样:

int arraySize = arrayName.length;

Java 语言中的数组从 0 开始建立索引。也就是说,对于所有数组,数组中的第一个元素始终位于 arrayName[0],最后一个元素位于 arrayName[arrayName.length - 1]

对象数组

您已了解数组如何包含原语类型,但值得一提的是,它们还可以包含对象。创建一个 java.lang.Integer 对象数组与创建一个原语类型数组没有太大区别,您可以通过两种方式进行创建:

// creates an empty array of 5 elements:
Integer[] integers = new Integer[5];
// creates an array of 5 elements with values:
Integer[] integers = new Integer[] { Integer.valueOf(1),
Integer.valueOf(2)
Integer.valueOf(3)
Integer.valueOf(4)
Integer.valueOf(5));

装箱和拆箱

Java 语言中的每种原语类型都有一个对应的 JDK 类,如表 1 所示。

表 1. 原语和对应的 JDK 类
原语对应的 JDK 类
booleanjava.lang.Boolean
bytejava.lang.Byte
charjava.lang.Character
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double

每个 JDK 类都提供了相应方法来解析它的内部表示,并将其转换为相应的原语类型。例如,下面这段代码将十进制值 238 转换为一个 Integer

int value = 238;
Integer boxedValue = Integer.valueOf(value);

此技术称为装箱,因为您将原语放在一个包装器或箱子中。

类似地,要将 Integer 表示转换为对应的 int 类,可以对它执行拆箱

Integer boxedValue = Integer.valueOf(238);
int intValue = boxedValue.intValue();

自动装箱和自动拆箱

严格地讲,您不需要显式装箱和拆箱原语。可以使用 Java 语言的自动装箱和自动拆箱特性:

int intValue = 238;

Integer boxedValue = intValue;
//
intValue = boxedValue;

但是,建议您避免使用自动装箱和自动拆箱,因为它们可能导致代码可读性问题。装箱和拆箱代码段中的代码比自动装箱的代码更加一目了然,因此可读性更高;我相信投入这些额外工作是值得的。

解析和转换装箱的类型

您已经了解了如何获取一个装箱的类型,但如何将一个您怀疑拥有装箱类型的数字 String 解析到它的正确箱子中?JDK 包装器类还提供了一些方法来实现此目的:

String characterNumeric = "238";
Integer convertedValue = Integer.parseInt(characterNumeric);

也可以将 JDK 包装类型的内容转换为 String

Integer boxedValue = Integer.valueOf(238);
String characterNumeric = boxedValue.toString();

请注意,在 String 表达式中使用串联运算符时(您应该已在对 Logger 的调用中看到过),原语类型已自动装箱,而且包装器类型会自动在它们之上调用 toString()。非常方便。

列表

List 是一种有序集合,也称为序列。因为 List 是有序的,所以您能够完全控制项目进入 List 中的何处。Java List 集合只能包含对象(不能包含像 int 这样的原语类型),而且它为其行为方式定义了严格的契约。

List 是一个接口,所以不能直接实例化它。(您可以在 第 17 单元 中了解接口。)这里将使用它的最常用实现 ArrayList。可通过两种方式执行声明。第一种方式使用显式语法:

List<String> listOfStrings = new ArrayList<String>();

第二种方式使用 “菱形” 运算符(在 JDK 7 中引入):

List<String> listOfStrings = new ArrayList<>();

请注意,ArrayList 实例化过程中没有指定对象类型。这是因为表达式右侧的类的类型必须与左侧的类型匹配。在本学习路径的剩余部分中,两种类型都会用到,因为您可能在实际应用中看到这两种用法。

请注意,我将 ArrayList 对象赋给了一个 List 类型的变量。在 Java 编程中,可以将某种类型的变量赋给另一种类型,只要被赋值的变量是赋值变量所实现的超类或接口。在后面的单元中,将进一步介绍这些变量赋值类型的约束规则。

正式类型

前面的代码段中的 <Object> 被称为正式类型<Object> 告诉编译器,这个 List 包含一个 Object 类型的集合,这意味着您可将您喜欢的任何实体放在该 List 中。

如果您想对能或不能放入 List 中的实体进行更严格的约束,可通过不同方式定义该正式类型:

List<Person> listOfPersons = new ArrayList<Person>();

现在您的 List 仅能包含 Person 实例。

使用列表

使用 List(一般来讲就像使用 Java 集合一样)非常简单。以下是可对 List 执行的一些操作:

  • 将实体放入 List 中。
  • 询问 List 目前有多大。
  • List 中获取实体。

要将实体放在 List 中,可调用 add() 方法:

List<Integer> listOfIntegers = new ArrayList<>();
listOfIntegers.add(Integer.valueOf(238));

add() 方法将元素添加到 List 的末尾处。

要询问 List 有多大,可调用 size()

List<Integer> listOfIntegers = new ArrayList<>();

listOfIntegers.add(Integer.valueOf(238));
Logger l = Logger.getLogger("Test");
l.info("Current List size: " + listOfIntegers.size());

要从 List 中检索某一项,可调用 get() 并向它传递您想要的项的索引:

List<Integer> listOfIntegers = new ArrayList<>();
listOfIntegers.add(Integer.valueOf(238));
Logger l = Logger.getLogger("Test");
l.info("Item at index 0 is: " listOfIntegers.get(0));

在真实的应用程序中,一个 List 将包含记录或业务对象,您可能希望在处理过程中查看所有这些对象。您通常如何实现此目的?答案:您希望迭代 该集合,您可以这么做是因为 List 实现了 java.lang.Iterable 接口。

迭代变量 (Iterable)

如果一个集合实现了 java.lang.Iterable,那么该集合被称为迭代变量集合。您可从一端开始,逐项地处理集合,直到处理完所有项。

循环 单元中,我简要提及了迭代实现 Iterable 接口的集合的特殊语法。这里更详细地介绍了该语法:

for (objectType varName : collectionReference) {
  // Start using objectType (via varName) right away...
}

之前的代码比较抽象,这是一个更真实的示例:

List<Integer> listOfIntegers = obtainSomehow();
Logger l = Logger.getLogger("Test");
for (Integer i : listOfIntegers) {
  l.info("Integer value is : " + i);
}

这个小代码段所做的事情与下面这个长代码段相同:

List<Integer> listOfIntegers = obtainSomehow();
Logger l = Logger.getLogger("Test");
for (int aa = 0; aa < listOfIntegers.size(); aa++) {
  Integer I = listOfIntegers.get(aa);
  l.info("Integer value is : " + i);
}

第一个代码段使用了简写语法:它没有 index 变量(在本例中为 aa)要初始化,也没有调用 Listget() 方法。

因为 List 扩展了 java.util.Collection(它实现 Iterable),所以可以使用简写语法来迭代任何 List

集 (Set)

根据定义,Set 是一种包含唯一元素的集合结构 — 即没有重复元素。List 可包含同一个对象数百次,而 Set 仅可包含某个特定实例一次。Java Set 集合仅可包含对象,而且它为它的行为方式定义了严格的契约。

因为 Set 是一个接口,所以不能直接实例化它。我最喜欢的实现之一是 HashSet,它很容易使用且类似于 List

以下是可对 Set 执行的一些操作:

  • 将实体放入 Set 中。
  • 询问 Set 目前有多大。
  • Set 中获取实体。

Set 的一个独特特征是,它可保证其元素中的唯一性,但不关心元素的顺序。考虑以下代码:

Set<Integer> setOfIntegers = new HashSet<Integer>();
setOfIntegers.add(Integer.valueOf(10));
setOfIntegers.add(Integer.valueOf(11));
setOfIntegers.add(Integer.valueOf(10));
for (Integer i : setOfIntegers) {
  l.info("Integer value is: " + i);
}

您可能已料到,该 Set 中有 3 个元素,但它仅有两个,因为包含值 10Integer 对象仅添加了一次。

在迭代 Set 时请注意此行为,类似于:

Set<Integer> setOfIntegers = new HashSet();
setOfIntegers.add(Integer.valueOf(10));
setOfIntegers.add(Integer.valueOf(20));
setOfIntegers.add(Integer.valueOf(30));
setOfIntegers.add(Integer.valueOf(40));
setOfIntegers.add(Integer.valueOf(50));
Logger l = Logger.getLogger("Test");
for (Integer i : setOfIntegers) {
  l.info("Integer value is : " + i);
}

对象可能按照不同于添加它们的顺序打印出来,因为 Set 可以确保唯一性,但不保证顺序。如果您将前面的代码粘贴到您的 Person 类的 main() 方法中并运行它,可以看到此结果。

映射 (Map)

Map 是一种方便的集合构造,可以使用它将一个对象()与另一个对象()相关联。您可能已想象到,Map 的键必须是唯一的,而且可在以后用于检索值。Java Map 集合仅可包含对象,而且它为其行为方式定义了严格的契约。

因为 Map 是一个接口,所以不能直接实例化它。我最喜欢的实现之一是 HashMap

可对 Map 执行的操作包括:

  • 将实体放入 Map 中。
  • Map 获取实体。
  • Map 提供一个键 Set— 用于迭代它。

要将实体放入 Map 中,需要拥有一个表示它的键的对象和一个表示它的值的对象:

public Map<String, Integer> createMapOfIntegers() {
  Map<String, Integer> mapOfIntegers = new HashMap<>();
  mapOfIntegers.put("1", Integer.valueOf(1));
  mapOfIntegers.put("2", Integer.valueOf(2));
  mapOfIntegers.put("3", Integer.valueOf(3));
  //...
  mapOfIntegers.put("168", Integer.valueOf(168));
}

在这个示例中,Map 包含 Integer,采用一个 String 作为键,这恰好是它们的 String 表示。要检索某个特定的 Integer 值,需要使用它的 String 表示:

mapOfIntegers = createMapOfIntegers();
Integer oneHundred68 = mapOfIntegers.get("168");

结合使用 Set 和 Map

有时,您可能发现您拥有一个 Map 的引用,而且您希望遍历它的整个内容集合。在这种情况下,您需要向 Map 提供一个键 Set

Set<String> keys = mapOfIntegers.keySet();
Logger l = Logger.getLogger("Test");
for (String key : keys) {
  Integer  value = mapOfIntegers.get(key);
  l.info("Value keyed by '" + key + "' is '" + value + "'");
}

请注意,在用于 Logger 调用时,会自动调用从 Map 检索的 IntegertoString() 方法。Map 返回它的键的 List,因为 Map 具有键,而且每个键是唯一的。唯一性(而不是顺序)是 Set 的独特特征(这可以解释为什么没有 keyList() 方法)。

测试您的理解情况

  1. 以下哪句关于 Java 数组的描述是正确的?
    1. 创建之后,数组的大小是固定的(即不能更改)。
    2. 要获得数组的大小,可以使用 .length 属性。
    3. 数组具有索引,这意味着数组的每个元素可通过一个唯一整数编号获得。
    4. 数组可包含原语类型,也可以包含 Java 对象。
    5. 上述所有选项。
  2. 编写一个包含方法 intInit() 的程序,该方法将一个 int 数组初始化为值 1、2、3、5、7、11、13、17、19、23、27 和 29,然后将该数组返回给调用方。将您的 Java 类命名为 Unit10,并在本测验的所有剩余编码练习中使用该类。
  3. 编写一个 JUnit 测试案例来测试 intInit() 方法,验证您的 int[] 的元素是否与问题 3 中指定的完全相同。使用元素访问语法(例如使用 array[0] 获取第一个元素,依此类推)。
  4. 丰富您为问题 2 编写的 intInit() 方法,使用速记式 for 循环语法打印 int[] 的元素。
  5. 您能否找到以下代码中的错误?
    public void question5() {
      int[] intArray = new int[4];
      
      intArray[0] = 1;
      intArray[1] = 2;
      intArray[2] = Integer.valueOf(3);
      intArray[3] = Integer.MAX_VALUE;
      
    }
    1. intArray 赋给了太多值。在运行时会出现异常。
    2. 不允许将 Integer 对象和 int 存储在同一个数组中。
    3. 该代码没有错误。通过自动拆箱,可将 Integer 对象赋给 int 数组。
    4. 上述选项都不是。
  6. 向您的 Unit10 类添加一个名为 problem6() 的方法,该方法创建包含以下元素的 List
    • 32
    • 这是一个字符串
    • Integer.valueOf(238)
    • -410
    • null
    编写一个 JUnit 测试案例来确认您的 List 是按正确顺序创建的,而且包含您指定的元素值。
  7. 以下哪个接口仅允许使用某个特定值的一个实例?
    1. 列表
    2. 映射
    3. 上述所有选项
  8. 以下哪个接口允许您使用唯一键存储键/值对?
    1. 列表
    2. 映射
    3. 上述所有选项
  9. 以下哪个接口可保证保持它包含的项目顺序?
    1. 列表
    2. 映射
    3. 上述所有选项

核对您的答案。

进一步探索

关于 Java Collections API 您不知道的 5 件事,第 1 部分

关于 Java Collections API 您不知道的 5 件事,第 2 部分

Java 教程:集合

集合框架概述

来自 Rebel Labs 的 Java 集合备忘单

Java 教程:数组

上一单元:循环下一单元:存档 Java 代码


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1038584
ArticleTitle=第 10 单元:Java 集合
publish-date=10172016