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 类 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
char | java.lang.Character |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.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
)要初始化,也没有调用 List
的 get()
方法。
因为 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 个元素,但它仅有两个,因为包含值 10
的 Integer
对象仅添加了一次。
在迭代 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
检索的 Integer
的 toString()
方法。Map
返回它的键的 List
,因为 Map
具有键,而且每个键是唯一的。唯一性(而不是顺序)是 Set
的独特特征(这可以解释为什么没有 keyList()
方法)。
测试您的理解情况
- 以下哪句关于 Java 数组的描述是正确的?
- 创建之后,数组的大小是固定的(即不能更改)。
- 要获得数组的大小,可以使用
.length
属性。 - 数组具有索引,这意味着数组的每个元素可通过一个唯一整数编号获得。
- 数组可包含原语类型,也可以包含 Java 对象。
- 上述所有选项。
- 编写一个包含方法
intInit()
的程序,该方法将一个int
数组初始化为值 1、2、3、5、7、11、13、17、19、23、27 和 29,然后将该数组返回给调用方。将您的 Java 类命名为Unit10
,并在本测验的所有剩余编码练习中使用该类。 - 编写一个 JUnit 测试案例来测试
intInit()
方法,验证您的int[]
的元素是否与问题 3 中指定的完全相同。使用元素访问语法(例如使用array[0]
获取第一个元素,依此类推)。 - 丰富您为问题 2 编写的
intInit()
方法,使用速记式for
循环语法打印int[]
的元素。 - 您能否找到以下代码中的错误?
public void question5() {
int[] intArray = new int[4];
intArray[0] = 1;
intArray[1] = 2;
intArray[2] = Integer.valueOf(3);
intArray[3] = Integer.MAX_VALUE;
}
- 向
intArray
赋给了太多值。在运行时会出现异常。 - 不允许将
Integer
对象和int
存储在同一个数组中。 - 该代码没有错误。通过自动拆箱,可将
Integer
对象赋给int
数组。 - 上述选项都不是。
- 向
- 向您的
Unit10
类添加一个名为problem6()
的方法,该方法创建包含以下元素的List
:-
32
-
这是一个字符串
-
Integer.valueOf(238)
-
-410
-
null
List
是按正确顺序创建的,而且包含您指定的元素值。 -
- 以下哪个接口仅允许使用某个特定值的一个实例?
-
列表
-
映射
-
集
- 上述所有选项
-
- 以下哪个接口允许您使用唯一键存储键/值对?
-
列表
-
映射
-
集
- 上述所有选项
-
- 以下哪个接口可保证保持它包含的项目顺序?
-
列表
-
映射
-
集
- 上述所有选项
答案: -
-
- 以下哪句关于 Java 数组的描述是正确的?(e)
- 创建之后,数组的大小是固定的(即不能更改)。
- 要获得数组的大小,可以使用
.length
属性。 - 数组具有索引,这意味着数组的每个元素可通过一个唯一整数编号获得。
- 数组可包含原语类型,也可以包含 Java 对象。
- 上述所有选项。
- 编写一个包含方法
intInit()
的程序,该方法将一个int
数组初始化为值 1、2、3、5、7、11、13、17、19、23、27 和 29,然后将该数组返回给调用方。将您的 Java 类命名为Unit10
,并在本测验的所有剩余编码练习中使用该类。public int[] intInit() { int[] intArray = { 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 27, 29 }; return intArray; }
- 编写一个 JUnit 测试案例来测试
intInit()
方法,验证您的int[]
的元素是否与问题 3 中指定的完全相同。使用元素访问语法(例如使用array[0]
获取第一个元素,依此类推)。@Test public void testIntInit() { Unit10 testclass = new Unit10(); int[] intArray = testclass.intInit(); assertEquals(intArray[0], 1); assertEquals(intArray[1], 2); assertEquals(intArray[2], 3); assertEquals(intArray[3], 5); assertEquals(intArray[4], 7); assertEquals(intArray[5], 11); assertEquals(intArray[6], 13); assertEquals(intArray[7], 17); assertEquals(intArray[8], 19); assertEquals(intArray[9], 23); assertEquals(intArray[10], 27); assertEquals(intArray[11], 29); }
- 丰富您为问题 2 编写的
intInit()
方法,使用速记式for
循环语法打印int[]
的元素。public int[] intInit() { int[] intArray = { 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 27, 29 }; Logger l = Logger.getLogger(Unit10.class.getName()); for (int number : intArray) { l.info("Number: " + number); } return intArray; }
- 您能否找到以下代码中的错误?(c)
public void question5() { int[] intArray = new int[4]; intArray[0] = 1; intArray[1] = 2; intArray[2] = Integer.valueOf(3); intArray[3] = Integer.MAX_VALUE; }
- 向
intArray
赋给了太多值。在运行时会出现异常。 - 不允许将
Integer
对象和int
存储在同一个数组中。 - 该代码没有错误。通过自动拆箱,可将
Integer
对象赋给int
数组。 - 上述选项都不是。
- 向
- 向您的
Unit10
类添加一个名为problem6()
的方法,该方法创建包含以下元素的List
:-
32
-
这是一个字符串
-
Integer.valueOf(238)
-
-410
-
null
public List<Object> problem6() { List<Object> listOfObjects = new ArrayList<>(); listOfObjects.add(32); listOfObjects.add("这是一个字符串"); listOfObjects.add(Integer.valueOf(238)); listOfObjects.add(-410); listOfObjects.add(null); return listOfObjects; }
编写一个 JUnit 测试案例来确认您的List
是按正确顺序创建的,而且包含您指定的元素值。@Test public void testProblem6() { Unit10 testclass = new Unit10(); List<Object> listOfObjects = testclass.problem6(); assertEquals(32, listOfObjects.get(0)); assertEquals("这是一个字符串", listOfObjects.get(1)); assertEquals(Integer.valueOf(238), listOfObjects.get(2)); assertEquals(-410, listOfObjects.get(3)); assertEquals(null, listOfObjects.get(4)); }
-
- 以下哪个接口仅允许使用某个特定值的一个实例?(c)
-
列表
-
映射
-
集
- 上述所有选项
-
- 以下哪个接口允许您使用唯一键存储键/值对?(b)
-
列表
-
映射
-
集
- 上述所有选项
-
- 以下哪个接口可保证保持它包含的项目顺序?(a)
-
列表
-
映射
-
集
- 上述所有选项
-
- 以下哪句关于 Java 数组的描述是正确的?(e)