《java JDK7 学习笔记》之Collection

一、使用Collection 收集对象

1、认识Collection架构

Java SE提供了满足各种需求的API,在使用这些API前,建议先了解其继承与接口操作架构,才能了解何时使用哪个类,以及类之间如何彼此合作,而不会沦为死背API或抄写范例的窘境。

针对收集对象的需求,Java SE 提供了Collection API,其接口继承架构如图所示:

《java JDK7 学习笔记》之Collection

收集对象的行为,像是新增对象的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接口方便许多。

《java JDK7 学习笔记》之Collection

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。

链接的每个元素会参考下一个元素,这有利于调整索引顺序。

《java JDK7 学习笔记》之Collection

新增的对象将建立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),如图:

《java JDK7 学习笔记》之Collection

上图中的这些空间成为哈希桶(Hash bucket),如果 对象要加入HashSet,则会调用对象的hashCode()取得哈希码,并尝试放入对应号码的哈希桶中,如果哈希桶中没对象,则直接放入,如图9.6;如果哈希桶中已经有对象,则会再调用对象的equals()进行比较,如图9.7所示。

《java JDK7 学习笔记》之Collection

如果同一个哈希桶中已有对象,调用该对象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 JDK7 学习笔记》之Collection

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()定义来决定。

《java JDK7 学习笔记》之Collection

7、使用泛型

《java JDK7 学习笔记》之Collection

使用泛型的范例:

《java JDK7 学习笔记》之Collection

《java JDK7 学习笔记》之Collection

上一篇:java中newInstance和new(转)


下一篇:《Java JDK7 学习笔记》课后练习题2