一、使用Collection 收集对象
1、认识Collection架构
Java SE提供了满足各种需求的API,在使用这些API前,建议先了解其继承与接口操作架构,才能了解何时使用哪个类,以及类之间如何彼此合作,而不会沦为死背API或抄写范例的窘境。
针对收集对象的需求,Java SE 提供了Collection API,其接口继承架构如图所示:
收集对象的行为,像是新增对象的add()方法,移除对象的remove()方法,都是定义在java.util.Collection中。既然可以收集对象,也要可以逐一取出对象,这就是java.util.Iterable定义的行为,它定义了Iterator()方法返回java.util.Iterator操作对象,可以让你逐一取得收集的对象。
收集对象的共同行为定义在Collection中,然而收集对象会有不同的需求。如果希望收集时记录每个对象的索引顺序,并可依据索引顺序取出对象,这样的行为定义在java.util.List接口中;如果希望收集的对象不重复,具有集合的行为,可以使用java.util.Set接口定义;如果希望收集对象时为队列方式,收集的对象从尾端加入,取出对象时从前端取出,则可以使用java.util.Queue接口定义;如果希望对Queue的两端进行加入、移除对象等操作,则可以使用java.util.Deque.
收集对象时会依据需求的不同使用不同的接口操作对象。如果想要收集时具有索引顺序,操作方式之一就是使用数组,而以数组操作List的就是java.util.ArrayList。
Java SE API 不仅提供许多已操作的类,也考虑到用户自行扩充API的需求,以收集对象的基本行为来说,其提供的java.util.abstractCollection操作了Collection的基本行为;java.util.AbstractList操作了List的基本行为,必要时可以继承AbstractCollection来操作自己的Collection,继承AbstractList来操作自己的List,这会比直接操作Collection与List接口方便许多。
2、具有索引的List
List是一种Collection,作用是收集对象,并以索引方式保留收集对象的顺序,其 操作类之一是java.util.ArrayList,其操作原理如以下范例:
public class Guest{
public static void main(String[] args){
List list = new ArrayList(); <----使用java SE的List与ArrayList
Scanner scanner = new Scanner(System.in);
String name;
while(true){
System.out.println(“访客名称:”);
name = scanner.nextLine();
if(name.equals(“quit”)){
break;
}
list.add(name);
}
System.out.println(“访客名单:”);
foreach(list);
}
private static void foreach(List list){
for(int i = 0;i < list.size(); i ++){
String guest = (String) list.get( i );
System.out.println(guest.toUpperCase()); <---- 使用get()依索引取得收集的对象
}
}
}
查看APi文件,可发现List接口定义了add()、remove()、set()等许多依索引操作的方法。java.util.LinkedList也操作可List接口。
· ArrayList 特性
使用java.util.ArrayList操作时,内部就是使用Object数组来保存收集的对象,也因此考虑是否使用ArrayList,就等于考虑是否要使用到数组的特性。
数组在内存中会是连续的线性空间,根据索引随机存取时速度快,如果操作上有这类需求时,像是排序,就可使用ArrayList,可得到较好的速度表现。
数组在内存中会是连续的线性空间,如果需要调整索引顺序时,会有较差的表现。例如若在已收集100对象的ArrayList中,使用可指定索引的add()方法,将对象新增到索引0的位置,那么原先索引0的对象必须调整至索引1,索引1的对象必须调整到2,。。。。使用ArrayList做这类操作不合适,很好内存和速度。
数组的长度固定也是要考虑的问题,在ArrayList内部数组长度不够时,会建立新的数组,并将旧数组的参考指定给新数组,这也是必须耗费时间与内存的操作。为此ArrayList有个可指定容量(Capacity)的构造函数,如果大致知道将收集的对象范围,事先建立足够长度的内部数组,可以节省以上所描述的成本。
· LinkedList特性
LinkedList 在操作List接口时,采用了链接(Link)结构。若不了解链接,可参考一下范例:
public class SimpleLinkedList{
prvate class Node{
Node(Object obj){
this.obj = obj;
}
Object obj;
Node next;
}
private Node first;
public void add(Object obj){
if(first == null){
first = new Node(obj);
}else {
Node last = first;
while(last.next != null){
last = last.next;
}
last.next = new Node(obj);
}
}
public int size(){
int count = 0;
Node last = forst;
while(last.next != null){
last = last.next;
count ++;
}
return count;
}
public Object get(int index){
int size = size();
if(index >= size){
throws new IndexOutOfBoundsException(
String.format(“Index: %d,Size :%d”,index,size);
}
int count = 0;
Node last = first;
while(last < index){
last = last.next;
count ++;
}
return last.obj;
}
}
在SimpleLinkedList内部使用Node封装新增的对象,每次add()新增对象之后,将会形成链状结构。如图9.4
所以每次add()对象时,才会建立新的Node来保存对象,不会事先耗费内存,若调用size(),则从第一个对象,逐一参考下一个对象并计数,则可取得收集的对象长度。若想调用get()指定索引取得对象,则从第一个对象,逐一参考下一个对象并计数,则可取得指定索引的对象。想要指定索引随机存取对象时,链接方式都得使用从第一个元素开始查找下一个元素的方式,效率比较低,像排序就不适合使用链接操作的List。
链接的每个元素会参考下一个元素,这有利于调整索引顺序。
新增的对象将建立Node实例封装,而first(或上一节点的next)重新参考至新建的Node对象,新建Node的next则参考至下一Node对象。因此,若收集的对象经常会有变动索引的情况,或许考虑连接方式操作的List会比较好,像是随时会有客户端登录或注销的客户端List,使用LindedList会有比较好的效率。
3、内容不重复的Set
同样是收集对象,在收集过程中若有相同对象,则不再重复收集,若有这类需求,可以使用Set接口来操作对象。例如,若有一个字符串,当中有许多的英文单词,你希望知道不重复的单词有几个,就可以撰写如下程序:
public class Words{
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
System.out.println(“请输入英文”);
String line = scanner.nextLine();
String[] tokens = line.split(“ “);
Set words = new HashSet();
for(String token : tokens){
words.add(token);
}
System.out.printf(“不重复的单字有 %d 个:%s%n“,words.size(),words);
}
}
String的aplit()方法,可以指定切割字符串的方式,在这里指定以空格切割,split()会返回String[],包括切割的每个字符串,接着将String[]中的每个字符串加入Set的操作HashSet中。由于Set的特性是不重复,所以若有相同的单词,则不会在重复加入,最后只要调用Set的size()方法,就可以知道收集的字符串个数,HashSet的toString()操作,则会包括收集的字符串。
Set集合会使用对象的hashCode()与equals()方法来判断对象是否相同。以HashSet为例,在内存中开设空间,每个空间都会有个哈希编码(Hash Code),如图:
上图中的这些空间成为哈希桶(Hash bucket),如果 对象要加入HashSet,则会调用对象的hashCode()取得哈希码,并尝试放入对应号码的哈希桶中,如果哈希桶中没对象,则直接放入,如图9.6;如果哈希桶中已经有对象,则会再调用对象的equals()进行比较,如图9.7所示。
如果同一个哈希桶中已有对象,调用该对象equals()与要加入的对象进行比较,若为false,则表示两个对象非重复对象,可以收集;若为true,则表示两个对象是重复对象,不可以收集。
事实上不止是HashSet,java中许多要判断对象是否重复时,都会调用hashCode()与equals()方法,因此规格书中建议,两个方法必须同时操作。
4、支持队列操作的Queue
如果希望收集对象时可以队列方式,收集的对象加入至尾端,取得对象时可以从前端,则可以使用Queue接口的操作对象。
Queue继承自Collection,所以也具有Collection的add()、remove()、element()等方法,然而Queue定义了自己的offer()、poll()与peek()等方法,最主要的差别在于,add()、remove()、element()等方法操作失败时会抛出异常,而offer()、poll()与peek()等方法操作失败时会返回特定值。
如果对象有操作Queue,并打算以队列方式使用,且队列长度受限,通常建议使用offer()、poll()与peek()等方法。
offer()方法用来在队列后端加入对象,成功后返回true,失败则返回false。poll()方法用来取出队列前端对象,若队列为空则返回null。
peek()方法用来取得(但不取出)队列前端对象,若队列为空则返回null。
LinkedList接口,它不仅操作了List接口,也操作了Queue行为,所以可将LinkedList当做队列来使用。
如果想对队列的前端与尾端进行操作,在前端加入对象与取出对象,在尾端加入对象与取出对象,Queue的子接口Deque就定义了这类行为。
Deque中定义了addFirst()、removeFirst()、addLast()、removeLast()、getLast()等方法,操作失败时会抛出异常。而offerFirst()、pollFirst()、peekFirst()、offerLast()、pollLast()、peekLast()等方法,操作失败时会返回特定值。
Queue的行为与Deque的行为有所重复,有几个操作时等义的,如表9.1所示:
java.util.ArrayDeque操作了Deque接口,以下范例是使用ArrayDeque来操作容量有限的堆栈:
public class Stack{
private Deque deque = new ArrayDeque();
private int capacity;
public Stack(int capacity){
this.capacity = capacity;
}
public boolean push(Object o){
if(deque.size() +1 > capacity){
return false;
}
return deque.offerLast(o);
}
public Object pop(){
return deque.pollLast();
}
public Object peek(){
return deque.peekLast();
}
public int size(){
return deque.size();
}
public static void main(String[] args){
Stack stack = new Stack(5);
stack.push(“Justin”);
stack.push(“Monica”);
stack.push(“Irene”);
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
}
}
堆栈结构是先进后出,所以只需结果最后才显示Justin。
5、访问对象的Iterator
写个foreach()方法,显示List收集的所有对象:
public static void foreach(List list){
for(int i = 0; i < list.size(); i++){
System.out.println(list.get( i ));
}
}
这个方法适用于所有操作List接口的对象,如ArrayList、LinkedList等。Set接口中有个toArray()方法,可以将Set收集的对象转为Object()返回。下一个foreach()方法显示Set收集的对象:
public static void foreach(Set set){
for(Object o : set.toArray()){
System.out.println(o);
}
}
这个方法适用于所有操作Set接口的对象,如HashSet、TreeSet等。
写一个foreach()方法可以显示Queue收集的所有对象:
public static void foreach(Queue queue){
while(queue.peek() != null){
System.out.println(queue.poll());
}
}
上面的方法中queue.poll()方法是取出堆栈中的对象,当显示完Queue中所有的对象,Queue也会空。
无论是List、Set还是Queue,都有一个Iterator()方法,这个方法在JDK1.4之前,定义在Collection接口中,而List、Set、Queue继承自Collection,所以都拥有Iterator()的行为。
Iterator()方法会返回java.util.Iterator接口的操作对象,这个对象包括了Collection收集的所有对象,可以使用Iterator的hasNext()查看是否有下一个对象,若有的话,再使用next()取得下一个对象。因此,无论是List、Set、Queue还是任何Collection,都可以使用以下的foreach()来显示所有收集的对象:
public static void foreach(Collection Collection){
Iterator iterator = Collection.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next);
}
}
在JDK5之后,原先定义在Collection中的iterator(),提升至新的java.util.Iterator父接口,因此在JDK5之后,可以使用以下的foreach()方法显示收集的所有对象:
publlic static void foreach(Iterator iterator){
Iterator iterator = iterator.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
在JDK5之后有了增强式for循环,实际上增强式for循环本质上就是一个Iterator迭代器。如以下范例:
public class ForEach{
private static void foreach(Iterator iterator){
for(Object o:iterator){
System.out.println(o);
}
}
public static void main(String[] args){
List list = Arrays.asList(“aaa”,“bbb”,“ccc”);
foreach(list);
foreach(new HashSet(list));
foreach(new ArrayDeque(list));
}
}
上面的范例使用了java.util.Arrays的static方法asList(),这个方法接受不定长度自变量,可将指定的自变量收集为List。List是一种Iterator,可以使用foreach()方法。
增强式for循环在运用Iterator对象时,底层会编译为:
private static void foreach(Iterator iterator){
Object o;
for(Iterator i$ = iterator.iterator();
i$.hasNext();
System.out.println(o){
o = i$.next();
}
}
实际上增强式for循环还是调用了iteartor()方法,运用返回的Iterator对象来迭代取得所有收集的对象。
6、排序收集的对象
在收集对象之后,常用的操作是对收集的对象进行排序,java.util.Collections提供有sort()方法。sort()方法由于必须有索引才能进行排序,所以sort()方法只接受List操作对象。
排序数字的范例:
public class Sort{
public static void main(String[] args){
List numbers = Arrays.asList(10,2,3,1,9,15,4);
Collections.sort(numbers);
System.out.println(numbers);
}
}
Collections.sort()方法的排序是正序,由小到大。
· 操作Comparable
Collections的sort()方法要求被排序的对象,必须操作java.lang.Conparable接口,这个接口有个compareTo()方法必须返回大于0、等于0或小于0的结果。
Collections的sort()方法在取得a对象与b对象进行比较时,会先将a对象扮演(Cast)为Comparable(也因此若对象没操作Comparable,将会抛出ClassCastException),然后调用a.CompareTo( b ),若a对象小于b对象,必须返回小于0的值;若顺序上相等则返回0;若顺序a对象大于b对象,则返回大于0的值。
· 操作Comparator
当操作的对象无法操作Comparable时,或者拿不到原始码,也不能修改原始码,就要使用Comparator来进行自定义排序。
Collections的sort()方法有另一个重载版本,可接受java.util.Comparator接口的操作对象,如果使用这个版本,排序方式将根据Comparator的compara()定义来决定。
7、使用泛型
使用泛型的范例: