5.3 通用ArrayList
在某些编程语言中,特别是在C和C++中,必须在编译时固定所有数组的大小。程序员讨厌这一点,因为它迫使他们做出不舒服的权衡。一个部门有多少员工?当然不超过100。如果有一个拥有150名员工的庞大部门怎么办?我们是否要为每个只有10名员工的部门浪费90个条目?
在Java中,情况稍微好一点。可以在运行时设置数组的大小。
int actualSize = . . .;
var staff = new Employee[actualSize];
当然,这段代码并不能完全解决在运行时动态修改数组的问题。一旦设置了数组大小,就不能轻易地更改它。相反,在Java中,您可以通过使用另一个Java类(称为ArrayList)来处理这种常见情况。ArrayList类与数组类似,但它在添加和删除元素时自动调整其容量,而不需要任何附加代码。
ArrayList是一个带有类型参数的泛型类。要指定数组列表包含的元素对象的类型,可以附加一个用尖括号括起来的类名,例如ArrayList<Employee>
。您将在第8章中看到如何定义您自己的泛型类,但是您不需要知道使用Arraylist类型的任何技术细节。
以下部分将向您展示如何使用数组列表。
5.3.1 声明数组列表
以下是如何声明和构造包含Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
至于Java 10,使用var关键字避免重复类名是一个好主意:
var staff = new ArrayList<Employee>();
如果不使用var关键字,则可以省略右侧的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
这被称为“菱形”语法,因为空括号<>类似于菱形。使用菱形语法和new运算符。编译器检查新值会发生什么。如果它被分配给变量、传递到方法或从方法返回,编译器将检查变量、参数或方法的泛型类型。然后将该类型放入<>。在我们的示例中,new ArrayList<>()
被分配给ArrayList<Employee>
类型的变量。因此,通用类型是Employee。
小心
如果用var声明一个ArrayList,不要使用diamond语法。声明
var elements = new ArrayList<>();
生成一个
ArrayList<Object>
注意
在Java 5之前,没有泛型类。相反,只有一个ArrayList类,一个大小适合所有集合,其中包含类型为Object的元素。您仍然可以使用ArrayList而不使用
<...>
后缀。它被认为是“原始”类型,类型参数被删除。
注意
在更旧版本的Java中,程序员使用
Vector
类来实现动态数组。然而,ArrayList类效率更高,并且不再有任何好的理由使用Vector
类。
使用add
方法将新元素添加到数组列表中。例如,下面是如何用Employee对象填充数组列表:
staff.add(new Employee("Harry Hacker", . . .));
staff.add(new Employee("Tony Tester", . . .));
数组列表管理对象引用的内部数组。最终,该阵列将耗尽空间。这就是数组列表发挥其魔力的地方:如果调用add并且内部数组已满,则数组列表会自动创建一个更大的数组,并将所有对象从较小的数组复制到较大的数组。
如果您已经知道或猜测了要存储多少元素,请在填充数组列表之前调用ensureCapacity
方法:
staff.ensureCapacity(100);
该调用分配100个对象的内部数组。然后,前100个调用add将不会涉及任何昂贵的重新分配。
还可以将初始容量传递给ArrayList构造函数:
ArrayList<Employee> staff = new ArrayList<>(100);
小心
分配数组列表是
new ArrayList<>(100) // capacity is 100
与分配新数组不同
new Employee[100] // size is 100
数组列表的容量和数组的大小之间有一个重要的区别。如果您分配一个具有100个条目的数组,那么该数组有100个插槽,可以使用。一个容量为100个元素的数组列表有可能容纳100个元素(事实上,超过100个元素是以额外的重新分配为代价的),但在开始时,即使在初始构造之后,数组列表也根本不容纳任何元素。
size方法返回数组列表中的实际元素数。例如,
staff.size()
返回staff数组列表中的当前元素数。这相当于
a.length
对于数组a
一旦您合理地确定了数组列表的永久大小,就可以调用trimToSize
方法。此方法调整内存块的大小,使其使用的存储空间与保存当前元素数所需的存储空间完全相同。垃圾收集器将回收任何多余的内存。
缩减数组列表的大小后,添加新元素将再次移动块,这需要时间。只有在确定不再向数组列表中添加任何元素时,才应使用trimToSize。
C++ 注意
ArrayList类类似于C++ vector模板。Arraylist和vector都是泛型类型。但是C++向量模板重载了[]操作符以方便元素访问。Java没有操作符重载,因此必须使用显式方法调用。此外,C++向量是由值复制的。如果A和B是两个向量,则赋值A= B生成一个与B相同长度的新向量,所有元素都从B复制到A。Java中的相同赋值使得A和B都指向相同的数组列表。
java.util.ArrayList 1.2
ArrayList<E>()
构造一个空的数组列表ArrayList<E>(int initialCapacity)
构造一个空的数组列表,它有特定的大小- boolean add(E obj)
添加obj在数组列表的尾部。总是返回true。 - int size()
返回当前存储在数组列表的元素个数。(当然,这绝不会比数组列表的容量更大) - void ensureCapacity(int capacity)
确保数组列表具有存储给定数量元素的能力,而无需重新分配其内部存储数组。 - void trimToSize()
将阵列列表的存储容量减小到当前大小。
5.3.2 访问数组列表元素
不幸的是,没有什么是免费的。数组列表的自动增长方便性要求访问元素的语法更加复杂。原因是ArrayList类不是Java编程语言的一部分;它只是由某人编程并在标准库中提供的实用程序类。
您使用get和set方法来访问或更改数组元素,而不是令人愉快的[]语法。
例如,要设置第i个元素,请使用
staff.set(i, harry);
这相当于
a[i] = harry;
对于数组A(与数组一样,索引值以零为基。)
小心
不要调用
list.set(i, x)
,直到数组列表的大小大于i。例如,以下代码是错误的:var list = new ArrayList<Employee>(100); // capacity 100, size 0 list.set(0, x); // no element 0 yet
使用add方法而不是set来填充数组,并且只使用set来替换先前添加的元素。
要获取数组列表元素,请使用
Employee e = staff.get(i);
等价于
Employee e = a[i];
注意
当没有泛型类时,原始ArrayList类的get方法别无选择,只能返回一个Object。因此,get的调用方必须将返回的值强制转换为所需的类型:
Employee e = (Employee) staff.get(i);
原始ArrayList也有点危险。它的add和set方法接受任何类型的对象。调用
staff.set(i, "Harry Hacker");
编译时没有任何警告,只有在检索对象并尝试将其强制转换时,才会遇到问题。如果使用
ArrayList<Employee>
替代,编译器将检测到此错误。
您有时可以通过以下技巧获得两个世界中最好的灵活增长和方便的元素访问。首先,创建一个数组列表并添加所有元素:
var list = new ArrayList<X>();
while (. . .)
{
x = . . .;
list.add(x);
}
完成后,使用toArray方法将元素复制到数组中:
var a = new X[list.size()];
list.toArray(a);
有时,您需要在数组列表的中间添加元素。将add方法与索引参数一起使用:
int n = staff.size() / 2;
staff.add(n, e);
位置n及以上的元素向上移动,为新条目腾出空间。如果插入后数组列表的新大小超过了容量,则数组列表将重新分配其存储数组。
同样,可以从数组列表的中间删除元素:
Employee e = staff.remove(n);
它上面的元素被复制下来,数组的大小减少了一个。
插入和删除元素的效率不是很高。对于小的数组列表,这可能不值得担心。但是,如果您存储了许多元素,并且经常在集合的中间插入和删除元素,请考虑改用链接列表。我们将在第9章中解释如何使用链表编程。
可以使用“for each”循环遍历数组列表的内容:
for (Employee e : staff)
do something with e
此循环的效果与下面相同
for (int i = 0; i < staff.size(); i++)
{
Employee e = staff.get(i);
do something with e
}
清单5.11是对第4章EmployeeTest程序的修改。Employee[]数组替换为ArrayList<Employee>
。请注意以下更改:
- 不必指定数组大小。
- 使用“add”可以添加任意数量的元素。
- 使用
size()
而不是length
来计算元素的数量。 - 使用a.get(i)而不是[i]访问元素。
清单5.11
package arrayList;
import java.util.*;
/**
* This program demonstrates the ArrayList class.
* @version 1.11 2012-01-26
* @author Cay Horstmann
*/
public class ArrayListTest
{
public static void main(String[] args)
{
// fill the staff array list with three Employee objects
var staff = new ArrayList<Employee>();
staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15));
staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1));
staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15));
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
java.util.ArrayList 1.2
- E set(int index, E obj)
将值obj放在指定索引的数组列表中,返回先前的内容。 - E get(int index)
获取存储在指定索引中的值。 - void add(int index, E obj)
向上移动元素以在指定索引处插入obj。 - E remove(int index)
移除给定索引处的元素并向下移动其上的所有元素。返回删除的元素。
5.3.3 类型和原始数组列表之间的兼容性
在您自己的代码中,您总是希望使用类型参数来增加安全性。在本节中,您将看到如何与不使用类型参数的旧代码进行互操作。
假设您有以下遗留类:
public class EmployeeDB
{
public void update(ArrayList list) { . . . }
public ArrayList find(String query) { . . . }
}
可以将类型化数组列表传递给update方法,而不进行任何强制转换。
ArrayList<Employee> staff = . . .;
employeeDB.update(staff);
staff对象只是简单地传递给update方法。
小心
即使编译器没有错误或警告,这个调用也不是完全安全的。更新方法可能会将不属于Employee类型的元素添加到数组列表中。检索这些元素时,会发生异常。这听起来很可怕,但是如果你仔细想想,它的行为就像在泛型被添加到Java之前一样。虚拟机的完整性永远不会受到危害。在这种情况下,您不会失去安全性,但也不会从编译时检查中受益。
相反,当您将一个原始ArrayList分配给一个键入的数组列表时,会收到一个警告。
ArrayList<Employee> result = employeeDB.find(query); // yields warning
注意
要查看警告的文本,请使用选项-Xlint:unchecked进行编译。
使用强制转化不会使警告消失。
ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query); // yields another warning
相反,你会得到一个不同的警告,告诉你转换是误导性的。
这是Java中泛型类型有点不幸的结果。为了兼容性,编译器在检查类型规则是否被违反后,将所有类型化数组列表转换为原始Arraylist对象。在正在运行的程序中,所有数组列表都是相同的。虚拟机中没有类型参数。因此,强制转换(ArrayList)和(ArrayList<Employee>)执行相同的运行时检查。
在这种情况下你做不了什么。当您与遗留代码进行交互时,请研究编译器警告,并确保这些警告并不严重。
满足后,可以使用@SuppressWarnings(“unchecked”)注释标记接收强制转换的变量,如下所示:
@SuppressWarnings("unchecked") ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query); // yields another warning