第十一章:持有对象
如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这是一个非常简单的程序。
通常,程序总是根据运行时才知道的某些条件去创建新对象,在此之前,不会知道所需对象的数量,甚至不知道确切的对象。例如我们熟知的数组,它是编译器支持的类型,数组是保存一组对象的最有效的方式,如果你想保存一组基本类型的数据,也推荐使用这种方式。但是数组具有固定的尺寸,而在更一般的情况下中,你在写程序时并不知道需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制显得过于受限了。
Java实用类库提供了一套相当完整的容器类来解决这个基本问题,Java容器类都可以自动地调整自己的尺寸。Java容器类库的用途是“保存对象”,并将其划分成两个不同的概念:
Collection:一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象的产生顺序(通常与它们被插入的顺序相同)
Map:一组成对的“键值对”对象,允许你使用键来查找值
添加一组元素
Collections.addAll()方法接受一个Collection对象,以及一个数组或用逗号分隔的列表,将元素添加到Collection中。Collection.addAll()成员方法只能接受另一个Collection对象作为参数因此它不如Arrays.asList()或Collections.addAll()灵活,这两个方法都是使用的可变参数列表。
你也可以直接使用Arrays.asList()的输出作为List,但是在这种情况下,其底层表示的是数组,因此不能调整尺寸,如果你试图使用add()或delete()方法在这中列表中添加或删除元素,在运行时就会获得“java.lang.UnsupportedOperationException(不支持的操作)”异常。
import java.util.*;
public class AddingGroup {
public static void main(String[] args) {
Collection<Integer> collection =
new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Integer[] moreInts = { 6, 7, 8, 9, 10 };
collection.addAll(Arrays.asList(moreInts));
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
list.set(1, 99);
// list.add(21); // java.lang.UnsupportedOperationException
}
}
List:List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素,
ArrayList:长于随机访问元素,但是在List的中间插入和移除元素时较慢
LinkedList:它通过代价较低的在List中间进行插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对较慢,但它的特性集较ArrayList更大。此外,LinkedList还提供了可以使其用作栈、队列或双端队列的方法。这些方法中有些彼此之间只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是Queue中),如下:
getFirst()和element()完全一样,都是返回列表的头(第一个元素),而不移除它。如果List为空,则抛出NoSuchElementException。peek()方法与这两个方法只是稍有差异,它在列表为空时返回null。
removeFirst()和remove()也是完全一样的,它们移除并返回列表的头,而在列表为空时抛出NoSuchElementException。poll()方法稍有差异,它在列表为空时返回null。
addFirst()与add()和addLast()相同,它们都将某个元素插入到列表的尾(端)部。
removeLast()移除并返回列表的最后一个元素。
栈(Stack):通常是指“后进先出”(LIFO)的容器,即最后一个“压入”栈的元素,第一个“弹出”栈。LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用,不过,有时一个真正的“栈”更能把事情讲清楚:
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<>();
public void push(T v) {
storage.addFirst(v);
}
public T peek() {
return storage.getFirst();
}
public T pop() {
return storage.removeFirst();
}
public boolean isEmpty() {
return storage.isEmpty();
}
@Override
public String toString() {
return storage.toString();
}
}
迭代器:任何容器类,都必须有某种方式可以插入元素并将它们再次取回。毕竟,持有事务是容器最基本的动作。迭代器统一了对容器的访问方式。迭代器通常被称为轻量级对象,创建它的代价小,并且Java的Iterator只能单向移动,这个Iterator只能用来:
使用iterator()方法要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素
使用next()获得序列中的下一个元素
使用hasNext()检查序列中是否还有元素
使用remove()将迭代器新近返回的元素删除
注意:
由于Iterator可以移除由next()产生的最后一个元素,这意味着在调用remove()之前必须先调用next()。
ListIterator是一个更强大的Iterator的子类型,但它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。
Set:Set不保存重复的元素,Set具有与Collection完全一样的接口,因此没有任何额外的功能。HashSet所维护的顺序与TreeSet或LinkedHashSet都不同,HashSet使用的是散列函数,LinkedHashSet因为查询速度的原因也使用了散列,但是看起来它使用了链表来维护元素的插入顺序,它按照被添加的顺序保存元素。TreeSet默认会按照比较结果的升序保存对象。
Map:将对象映射到其它对象的能力是一种解决编程问题的杀手锏。例如,考虑一个程序,他将用来检查Java的Random类得随机性。理想状态下,Random可以产生理想的数字分布,但要想测试它,则需要生成大量的随机数,并对落入各种不同范围的数字进行计数,Map可以很容易的解决该问题,在本列中,键是Random产生的数字,值是该数字出现的次数。
public class Statistics {
public static void main(String[] args) {
Random rand = new Random(47);
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
// 随机生成一个0~20范围内的数字
int r = rand.nextInt(20);
Integer freq = map.get(r);
map.put(r, freq == null ? 1 : freq + 1);
}
System.out.println(map);
}
}
//output: {0=481, 1=502, 2=489, 3=508, 4=481, 5=503, 6=519, 7=471, 8=468, 9=549, 10=513,
// 11=531, 12=521, 13=506, 14=477, 15=497, 16=533, 17=509, 18=478, 19=464}