【Java编程思想】11.持有对象
如果一个程序只包含固定数量的且生命周期都是已知的对象,那么这是一个非常简单的程序。
Java 类库中提供一套容器类,来存储比较复杂的一组对象。其中有 List
、Set
、Queue
、Map
等。这些类也被称为集合类,Java 的类库中使用 Collection 这个名字指代该类库的一个特殊子集(其实 Java 中大部分容器类都实现了 Collection
接口)。
11.1 泛型和类型安全的容器
在 Java SE5 之前的容器,编译器是允许向容器中插入不正确的类型的。因此在获取容器中对象时,一旦转型就会抛出一个异常。
一个类如果没有显式地声明继承自哪个类,那么他就自动地继承自 Object,因此对于容器来说,添加不同的类,无论是编译器还是运行期都不会有问题,问题在于使用容器中存储的对象时,会引发意想不到的错误。
因此,Java SE5引入了泛型的支持(15章有详解)。通过使用泛型,可以在编译期防止将错误类型的对象放置到容器中。
List<Apple> list = new ArrayList();
在定义泛型之后,从容器中取出对象时,容器会直接转成泛型对应的类型。同时向上转型也可以作用在泛型上。
11.2 基本概念
Java 容器类库划分为两种:
Collection
。一个独立元素的序列,这些元素都服从一条或多条规则。List
必须按照插入的顺序保存元素,Set
不能有重复元素,Queue
按照派对规则来确定对象产生的顺序(通常与其元素被插入的顺序相同)。Map
。一组成对的键值对对象,允许使用键来查找值。映射表允许我们使用另一个对象来查找某个对象,它也被称作关联数组,因为他将某些对象与另外一些对象关联在了一起;或者被称为字典,因为你可以使用键对象来查找值对象
使用容器的时候,可能有些情况不需要使用向上转型的方式,例如 LinkedList
具有 List
接口中未包含的方法;TreeMap
具有 Map
中未包含的方法,因此如果要使用这些方法,就不能将他们向上转型为接口。
所有的 Collection
都可以用 foreach
语法遍历。
11.3 添加一组元素
添加一组元素有多种方法:
Arrays.asList()
方法,接收一个数组或者是逗号分隔的元素列表(使用可变参数),并将其转换为一个List
对象。Collections.addAll()
方法,接收一个Collection
对象,以及一个数组或是用逗号分隔的列表,并将元素添加到Collection
中。
两者的使用都有限制,Arrays.asList()
方法的输出在底层的表示是数组,因此不能调整尺寸。而 Collections.addAll()
方法只能接受另一个 Collection
对象作为参数。
像如下这种初始化,可以告诉编译器,由 Arrays.asList()
方法产生的 List
类型(实际的目标类型)。这种成为显式类型参数说明。
List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy());
11.4 容器的打印
数组的打印必须借助 Arrays.toString()
来表示(或者遍历打印);但是容器的打印可以直接使用 print
(默认就能生成可读性很好的结果)。
Collection
在每个“槽”中只能保存一个元素List
以特定的顺序保存一组元素。Set
元素不能重复。Queue
只允许在容器的一“端”插入对象,并从另一“端”移出对象。
Map
在每个“槽”内保存两个对象,即键和与之相关联的值。
关于各种容器的实现类型特点:
ArrayList
:按插入顺序保存元素,性能高LinkedList
:按插入顺序保存元素,性能略低于ArrayList
HashSet
:最快的获取元素方式TreeSet
:按照比较结果升序保存对象,注重存储顺序LinkedHashSet
:按照添加顺序保存对象HashMap
:提供最快的查找技术,没有特定顺序TreeMap
:敖钊比较结果的升序保存键LinkedHashMap
:按照插入顺序保存键
11.5 List
ArrayList
:擅长于随机访问元素,在List
的中间插入和移出元素时较慢。LinkedList
:通过代价较低的在List
中间进行的插入和删除操作,提供优化的顺序访问。另外其特性集更大。
与数组不同,List
允许在他被创建之后添加元素、移除元素或者自我调整尺寸,是一种可修改的序列。
关于 List
的方法:
contains()
:确定对象是否在列表中remove()
:从列表中移出元素indexOf()
:获取对象在列表中的索引编号subList()
:从较大的列表中创建出一个片段containsAll()
:判断片段是否包含于列表retainAll()
:求列表交集removeAll()
:移出指定的全部元素replace()
:在指定索引处,用指定参数替换该位置的元素addAll()
:在初始列表中插入新的列表isEmpty()
:校验列表是否为空clear()
:清空列表toArray()
:列表转数组
11.6 迭代器
迭代器(也是一种设计模式)是一个对象那个,它的工作室遍历并选择序列中的对象,使用者不需要关心该序列的底层结构。
Java 中有迭代器 Iterator
,只能单向移动,Iterator
只能用来:
- 使用方法
iterator()
要求容器返回一个Iterator
。Iterator
将准备好返回序列的第一个元素。 - 使用
next()
获得序列中的下一个元素。 - 使用
hasNext()
检查序列中是否还有元素。 - 使用
remove()
将迭代器新近返回的元素删除。
使用 Iterator
时不需要关心容器中的元素数量,只需要向前遍历列表即可。
Iterator
可以移出有 next()
产生的最后一个元素,这意味着在调用 remove()
之前需要先调用 next()
。
ListIterator
是加强版的 Iterator
子类型,只能用于 List
类的访问
- 区别于
Iterator
,ListIterator
是可以双向移动的 - 还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引
- 可以使用
set()
方法替换它访问过的最后一个元素 - 可以通过调用
listIterator()
方法产生一个指向List
开始处的ListIterator
- 还可以通过调用
listIterator(n)
方法创建一个一开始就指向列表索引为 n 的元素处的ListIterator
11.7 LinkedList
LinkedList
在执行插入和删除时效率更高,随机访问操作方面要逊色一些。
除此之外,LinkedList
还添加了可以使其用作栈、队列和双端队列的方法。
getFirst()
/element()
:返回列表头,列表为空是抛出NoSuchElementException
。peek()
:返回列表头,列表为空返回 null。removeFirst()
/remove()
:移出并返回列表头,列表为空是抛出NoSuchElementException
。poll()
:移出并返回列表头,列表为空返回 null。addFirst()
/add()
/addLst()
:将某元素插入到列表尾部。removeLast()
:移出并返回列表最后一个元素。
LinkedList
是 Queue
的一个实现。对比 Queue
接口,可以发现 Queue
在 LinkedList
的基础上添加了 element()
、offer()
、peek()
、poll()
、remove()
方法。
11.8 Stack
栈通常是指”后进先出”(LIFO)的容器。有时栈也被称为叠加栈,因为最后压栈的元素最先出栈。
LinkedList
具有能够直接实现一个栈的所有功能和方法,因此可以直接将 LinkedList
作为栈使用。
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void push(T v) { storage.addFirst(v); }
public T peek() { return storage.getFirst(); }
public T pop() { return storage.removeFirst(); }
public boolean empty() { return storage.isEmpty(); }
public String toString() { return storage.toString(); }
}
11.9 Set
Set
是基于对象的值来确定归属性的,具有与 Collection
完全一样的接口(实际上 Set
就是 Collection
,只是行为不同--一种继承与多态的典型应用,表现不同的行为)。
HashSet
使用了散列(更多17章介绍),因此存储元素没有任何规律。TreeSet
将元素存储在红黑树数据结构中,输出结果是排序的。默认按照字典序排序(A-Z,a-z),如果想按照字母序排序(Aa-Zz),可以指定new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
。LinkedHashSet
使用了散列,但是也用了链表结构来维护元素的插入顺序。
11.10 Map
get(key)
方法会返回与键关联的值,如果键不在容器中,则返回 null。
containKey()
和 containValue()
可以查看键和值是否包含在 Map
中。
Map
实际上就是讲对象映射到其他对象上的工具。
11.11 Queue
队列是一个典型的先进先出(FIFO)的容器。从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。
队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径-->例如并发编程中,安全的将对象从一个任务传输给另一个任务。
LinkedList
是 Queue
的一个实现,可以将其向上转型为 Queue
。
Queue
接口窄化了对 LinkedList
的方法的访问权限,以使得只有恰当的方法才可以使用,因此能够访问的 LinkedList
的方法会变少。
队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素时最需要的元素(具有最高的优先级)。PriorityQueue
提供了这种实现。当在 PriorityQueue
上调用 offer()
方法插入一个对象时,这个对象会在队列中被排序(通常这类队列的实现,会在插入时排序-维护一个堆-但是他们也可能在移出时选择最重要的元素)。默认的排序会使用对象在队列中的自然顺序,但是可以通过提供自己的 Comparator
来修改这个顺序。 这样能保证在对 PriorityQueue
使用 peek()
、poll()
、remove()
方法时,会先处理队列中优先级最高的元素。
11.12 Collection 和 Iterator
Collection
是描述所有序列容器的共性的根接口。java.util.AbstractCollection
类提供了 Collection
的默认实现,可以创建 AbstarctCollection
的子类型,其中不会有不必要的代码重复。
实现了 Collection
意味着需要提供 iterator()
方法。
(这章没太看懂,回头再看一遍)
11.13 Foreach 与迭代器
foreach
是基于 Iterator
接口完成实现的,Iterator
接口被 foreach
用来在序列中移动。任何实现了 Iterator
接口的类,都可以用于 foreach
语句。
System.getenv()
方法可以返回一个 Map
,System.getenv().entrySet()
可以产生一个有 Map.Entry
的元素构成的 Set
,并且这个 Set
是一个 Iterable
,因此它可以用于 foreach
循环。
foreach
语句可以用于数组或其他任何实现了 Iterable
的类,但是并不意味这数组肯定也是一个 Iterable
,而且任何自动包装都不会自动发生。
在类实现 Iterable
接口时,如果想要添加多种在 foreach
语句中使用这个类的方法(在 foreach
中的条件中使用类的方法),有一种方案是适配器模式。-->在实现的基础上,增加一个返回 Iterable
对象的方法,则该方法就可以用于 foreach
语句。例如
// 添加一个反向的迭代器
class ReversibleArrayList<T> extends ArrayList<T> {
public ReversibleArrayList(Collection<T> c) { super(c); }
public Iterable<T> reversed() {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
@Override
public boolean hasNext() { return current > -1; }
@Override
public T next() { return get(current--); }
@Override
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}};
}};
}
}
// 调用
for(String s : ral.reversed()) {
System.out.print(s + " ");
}
使用 Collection.shuffle()
时,可以看到包装与否对于结果的影响。
Integer[] ia = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List<Integer> list1 = new ArrayList<>(Arrays.asList(ia));
System.out.println("Before shuffling: " + list1);
Collections.shuffle(list1, rand);
System.out.println("After shuffling: " + list1);
System.out.println("array: " + Arrays.toString(ia));
List<Integer> list2 = Arrays.asList(ia);
System.out.println("Before shuffling: " + list2);
Collections.shuffle(list2, rand);
System.out.println("After shuffling: " + list2);
System.out.println("array: " + Arrays.toString(ia));
输出:
Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9]
array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
从结果中可以看到,Collection.shuffle()
方法没有影响到原来的数组-->只是打乱了列表中的引用。
如果用 ArrayList
将 Arrays.asList(ia)
方法产生的结果包装起来,那么只会打乱引用;而不包装的时候,就会直接修改低层的数组。因此 Java 中数组和 List
的本质是不同的。
11.14 总结
Java 提供的持有对象的方式:
- 数组:数组保存明确类型的对象,查询对象的时候不需要对结果做类型转换。数组可以是多维的,也可以保存基本类型数据。但是数组的容量,在生成后就不能改变了。
- Collection/Map:
Collection
保存单一的元素,Map
保存关联的键值对。在使用了泛型指定了容器存放的对象类型后,查询对象时便也不需要进行类型转换。Collection
和Map
的尺寸是动态的,不能持有基本类型(但是自动包装机制-装箱-会将存入容器的基本类型进行数据转换)。 - List:数组和
List
都是排好序的容器,但是List
能够自动扩充容量。大量随机访问使用ArrayList
,插入删除元素使用LinkedList
。 - Queue:各种
Queue
以及栈的行为,由LinkedList
提供支持。 - Map:
Map
是一种将对象与对象相关联的设计。快速访问使用HashMap
,保持键的排序状态使用TreeMap
,但是速度略慢,保持元素插入顺序以及快速访问能力使用LinkedHashMap
。 - Set:
Set
不接收重复元素。需要快速查询使用HashSet
,保持元素排序状态使用TreeSet
,保持元素插入顺序使用LinkedHashSet
。 - 有一部分的容器已经过时不应该使用:
Vector
,Hashtable
,Stack
。
下面是 Java 容器的简图:
- 黑框:常用容器
- 点线框:接口
- 实线框:普通的(具体的)类
- 空心箭头点线:一个特定的类实现了接口
- 实心箭头:某个类可以生成箭头所指向类的对象