java容器之间的比较,List、Set、Stack、Queue等,持续更新...
一、一些基本类型的容器
package com.cy.container; //: holding/PrintingContainers.java // Containers print themselves automatically. import java.util.*; import static com.java.util.Print.*; public class PrintingContainers { static Collection fill(Collection<String> collection) { collection.add("rat"); collection.add("cat"); collection.add("dog"); collection.add("dog"); return collection; } static Map fill(Map<String,String> map) { map.put("rat", "Fuzzy"); map.put("cat", "Rags"); map.put("dog", "Bosco"); map.put("dog", "Spot"); return map; } public static void main(String[] args) { print(fill(new ArrayList<String>())); print(fill(new LinkedList<String>())); print(fill(new HashSet<String>())); print(fill(new TreeSet<String>())); print(fill(new LinkedHashSet<String>())); print(fill(new HashMap<String,String>())); print(fill(new TreeMap<String,String>())); print(fill(new LinkedHashMap<String,String>())); } } /* Output: [rat, cat, dog, dog] [rat, cat, dog, dog] [dog, cat, rat] [cat, dog, rat] [rat, cat, dog] {dog=Spot, cat=Rags, rat=Fuzzy} {cat=Rags, dog=Spot, rat=Fuzzy} {rat=Fuzzy, cat=Rags, dog=Spot} *///:~
ArrayList和LinkedList都是List类型,从输出可以看出,它们都按照被插入的顺序保存元素。
两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多于
ArrayList。这些将在本章后续部分更详细地讨论。
HashSet 、TreeSet和LinkedHashSet郁是Set类型,输出显示在Set中,每个相同的项只有保
存一次,但是输出也显示了不同的Set实现存储元素的方式也不同。
Hashset使用的是相当复杂的方式来存储元素的,这种方式将在第17章中介绍,此刻你只需要知道这种技术是最快的获取
元素方式,因此,存储的顺序看起来并无实际意义(通常你只会关心某事物是否是某个Set的成
员,而不会关心它在Set出现的顺序〉。如果存储顺序很重要,那么可以使用TreeSet ,它按照比
较结果的升序保存对象;或者使用LinkedHashSet ,它按照被添加的顺序保存对象。
你不必指定(或考虑) Map的尺寸,因为它自己会自动地调整尺寸。Map还知道如
何打印自己,它会显示相关联的键和值。键和值在Map中的保存顺序并不是它们的插入顺序,因为HashMap实现使用的是一种非常快的算法来控制顺序。
本例使用了三种基本风格的Map: HashMap 、TreeMap和LinkedHashMap。 与HashSet一
样, HashMap也提供了最快的查找技术,也没有按照任何明显的顺序来保存其元素。TreeMap
按照比较结果的升序保存键,而LinkedHashMap则按照插入顺序保存键,同时还保留了
HashMap的查询速度。
二、 List
有两种类型的List:
• 基本的ArrayList ,它长于随机访问元素,但是在List的中间插入和移除元素时较慢。
• LinkedList ,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较比较慢,但是它的特性集较ArrayList更大。
在List中提插入元素是可行的,也是这带来了一个问题:
对于LinkedList ,在列表中插入和删除都是廉价操作,但是对于ArrayList ,这可是代价高昂的操作。这是否意味着你应该永远都不要在ArrayList的中间插入元素,并最好是切换到LinkedList? 不,这仅仅意味着,你应该意识到这个问题,如果你开始在某个ArrayList的中间执行很多插入操作,并且你的程序开始变慢,那么你应该看看你的List实现有可能就是罪耻祸首(发现此类瓶颈的最佳方式是使用仿真器)优化是一个很棘手的问题,最好的策略就是置之不顾,直到你发现需要担心它了(尽管理解这些
问题总是一种好的思路)。
四、 LinkedList
LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问操作方面却要逊色一些。
LinkedList还添加了可以使其用作栈、队列、或双端队列的方法。这些方法中有些彼此之间只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在Queue中)。例如, getFirst()和element()完全一样,它们都返回列表的头(第一个元素) ,而并不移除它,如果List为空,则抛出NoSuchElementException
peek()方法与这两个方式只是稍有差异,它在列表为空时返回null.
removeFirst()与remove()也是完全一样的,它们移除并返回列表的头,而在列表为空时抛出NoSuchElementException。
poll()稍有差异,它在列表为空时返回null。
addFirst()与add()和addLast()相同,它们都将某个元素插入到列表的尾(端)部。
removeLast()移除并返回列表的最后一个元素。
package com.cy.container; //: holding/LinkedListFeatures.java import typeinfo.pets.*; import java.util.*; import static com.java.util.Print.*; public class LinkedListFeatures { public static void main(String[] args) { LinkedList<Pet> pets = new LinkedList<Pet>(Pets.arrayList(5)); print(pets); // Identical: print("pets.getFirst(): " + pets.getFirst()); print("pets.element(): " + pets.element()); // Only differs in empty-list behavior: print("pets.peek(): " + pets.peek()); // Identical; remove and return the first element: print("pets.remove(): " + pets.remove()); print("pets.removeFirst(): " + pets.removeFirst()); // Only differs in empty-list behavior: print("pets.poll(): " + pets.poll()); print(pets); pets.addFirst(new Rat()); print("After addFirst(): " + pets); pets.offer(Pets.randomPet()); print("After offer(): " + pets); pets.add(Pets.randomPet()); print("After add(): " + pets); pets.addLast(new Hamster()); print("After addLast(): " + pets); print("pets.removeLast(): " + pets.removeLast()); } } /* Output: [Rat, Manx, Cymric, Mutt, Pug] pets.getFirst(): Rat pets.element(): Rat pets.peek(): Rat pets.remove(): Rat pets.removeFirst(): Manx pets.poll(): Cymric [Mutt, Pug] After addFirst(): [Rat, Mutt, Pug] After offer(): [Rat, Mutt, Pug, Cymric] After add(): [Rat, Mutt, Pug, Cymric, Pug] After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster] pets.removeLast(): Hamster *///:~
Pets.arrayList()的结果交给了LinkedList的构造器,以便使用它来组装LinkedList。如果
你浏览一下Queue接口就会发现,它在LinkedList的基础上添加了element() 、offer() 、peek() 、
poll()和remove()方法,以使其可以成为一个Queue的实现。Queue的完整示例将在本章稍后
给出。
五、Stack
"栈"通常是指"后进先出" 的容器。有时栈也被称为叠加栈,因为最后"压入"
栈的元素,第一个"弹出"栈。经常用来类比栈的事物是装有弹簧的储放器中的自助餐托盘,
最后装入的托盘总是最先拿出使用的。
LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用。
不过,有时一个真正的"栈"更能把事情讲清楚:
package com.java.util; // Making a stack from a LinkedList. import java.util.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(); } }
这里通过使用泛型,引入了在栈的类定义中最简单的可行示例。类名之后的<T>告诉编译
器这将是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,
就是T。 大体上,这个类是在声明"我们在定义一个可以持有T类型对象的Stack 。" Stack是用
LinkedList实现的,而LinkedList也被告知它将持有τ类型对象。注意, push()接受的是T类型的
对象,而peek()和pop()将返回T类型的对象。peek()方法将提供栈顶元素,但是并不将其从栈顶
移除,而pop()将移除并返回栈顶元素。
六、Set
package com.cy.container; import java.util.*; public class SetOfInteger { public static void main(String[] args) { Random rand = new Random(47); Set<Integer> intset = new HashSet<Integer>(); for(int i = 0; i < 10000; i++) intset.add(rand.nextInt(30)); System.out.println(intset); } } //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 16, 19, 18, 21, 20, 23, 22, 25, 24, 27, 26, 29, 28]
在0到29之间的10000个随机数被添加到了Set中,因此你可以想象,每一个数都重复了许多
次。但是你可以看到,每一个数只有一个实例出现在结果中。
你还可以往意到,输出的顺序没有任何规律可蟹,这是因为出于速度原因的考虑, HashSet
使用了散列…散列将校第17章中介绍。HashSet所维护的顺序与TreeSet或LinkedHashSet都不
同,因为它们的实现具有不同的元素存错方式。TreeSet将无素存储在红…黑树数据结构中,而
HashSet使用的是散列函数。LinkedHashList因为查询速度的原因也使用了散列,但是看起来它
使用了链表来维护无素的插入顺序。
如果你相对结果排序,一种方式是使用TreeSet来代替HashSet:
package com.cy.container; import java.util.*; public class SortedSetOfInteger { public static void main(String[] args) { Random rand = new Random(47); SortedSet<Integer> intset = new TreeSet<Integer>(); for(int i = 0; i < 10000; i++) intset.add(rand.nextInt(30)); System.out.println(intset); } } /* Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] *///:~
你将会执行的最常见的操作之…,就是使用contains()测试Set的归属性,但是还有很多操作
会让你想起在上小学时所教授的文氏图(译者注:用圆表示集与集之间关系的图) :
11.10 Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。例如,考虑一个程序,它将
用来检查Java的Random类的随机性。理想状态下, Random可以将产生理想的数字分布,但要
想测试它,翻需要生成大量的随机数,并对落入各种不同范围的数字进行计数。Map可以很容
易地解决该问题。在本例中,键是由Random产生的数字,而值是该数字出现的次数:
package com.cy.container; import java.util.*; public class Statistics { public static void main(String[] args) { Random rand = new Random(47); Map<Integer,Integer> m = new HashMap<Integer,Integer>(); for(int i = 0; i < 10000; i++) { // Produce a number between 0 and 10: int r = rand.nextInt(10); Integer freq = m.get(r); m.put(r, freq == null ? 1 : freq + 1); } System.out.println(m); } } /* Output: {0=994, 1=1033, 2=1010, 3=1014, 4=958, 5=1000, 6=1052, 7=980, 8=946, 9=1013} *///:~
七、Queue
队列是一个典型的先进先出(FIFO) 的容器。即从容器的一端放人事物,从另一端取出,
并且事物放入容器的顺序与取出的顺序是相同的。队列常被当作一种可靠的将对象从程序的某
个区域传输到另一个区域的途径。队列在并发编程中特别重要,就像你将在第2 1章中所看到的,
因为它们可以安全地将对象从一个任务传输给另一个任务。
LinkedList提供了方法以支持队列的行为, 并且它实现了Queue接口,因此LinkedList可以
用作Queue的一种实现。通过将LinkedList向上转型为Queue ,下面的示例使用了在Queue接口
中与Queue相关的方法:
package com.cy.container; import java.util.*; public class QueueDemo { public static void printQ(Queue queue) { while(queue.peek() != null) System.out.print(queue.remove() + " "); System.out.println(); } public static void main(String[] args) { Queue<Integer> queue = new LinkedList<Integer>(); Random rand = new Random(47); for(int i = 0; i < 10; i++) queue.offer(rand.nextInt(i + 10)); printQ(queue); Queue<Character> qc = new LinkedList<Character>(); for(char c : "Brontosaurus".toCharArray()) qc.offer(c); printQ(qc); } } /* Output: 8 1 1 1 5 14 3 1 0 1 B r o n t o s a u r u s *///:~
offer()方法是与Queue相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者
返回false. peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返
回null, 而element()会抛出NoSuchElcmentException异常。poll()和remove()方法将移除并返回
队头,但是poll()在队列为空时返回null ,而remove()会抛出NoSuchElcmentException异常。
自动包装机制会自动地将nextInt()方法的int结果转换为queue所需的Integer对象,将char c
转换为qc所需的Character对象。Queue接口窄化了对LinkedList的方法的访问权限,以使得只
有恰当的方法才可以使用,因此,你能够访问的LinkedList的方法会变少(这里你实际上可以将
queuc转型回LinkedList ,但是至少我们不鼓励这么做)。
11 .11.1 PrìorìtyQueue
先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确
定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。例如,在飞机场,
当飞机临近起飞时,这架飞机的乘客可以在办理整机手续时排到队头。如果构建了一个消息系
统,某些消息比其他消息更重要, 因而应该更快地得到处理,那么它们何时得到处理就与它们
何时到达无关。PriorityQueue添加到JavaSE5中,是为了提供这种行为的一种自动实现。
当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。
默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这
个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法肘,获取的元素将是队
列中优先级最高的元素。
让PriorityQueue与Integer 、String和Character这样的内置类型一起工作易如反掌。在下面
的示例中,第一个值集与前一个示例中的随机值相同,因此你可以看到它们从PriorityQueue中
弹出的顺序与前一个示例不同:
package com.cy.container; import java.util.*; public class PriorityQueueDemo { public static void main(String[] args) { PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(); Random rand = new Random(47); for(int i = 0; i < 10; i++) priorityQueue.offer(rand.nextInt(i + 10)); QueueDemo.printQ(priorityQueue); List<Integer> ints = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25); priorityQueue = new PriorityQueue<Integer>(ints); QueueDemo.printQ(priorityQueue); priorityQueue = new PriorityQueue<Integer>(ints.size(), Collections.reverseOrder()); priorityQueue.addAll(ints); QueueDemo.printQ(priorityQueue); String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION"; List<String> strings = Arrays.asList(fact.split("")); PriorityQueue<String> stringPQ = new PriorityQueue<String>(strings); QueueDemo.printQ(stringPQ); stringPQ = new PriorityQueue<String>(strings.size(), Collections.reverseOrder()); stringPQ.addAll(strings); QueueDemo.printQ(stringPQ); Set<Character> charSet = new HashSet<Character>(); for(char c : fact.toCharArray()) charSet.add(c); // Autoboxing PriorityQueue<Character> characterPQ = new PriorityQueue<Character>(charSet); QueueDemo.printQ(characterPQ); } } /* Output: 0 1 1 1 1 1 3 5 8 14 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A A B C D E F H I L N O S T U W *///:~
你可以看到,重复是允许的,最小的值拥有最高的优先级(如果是String ,空格也可以算作
值,并且比字母的优先级高)。为了展示你可以使用怎样的方法通过提供自己的Comparator对
象来改变排序, 第三个对PriorityQueue<Integer>的构造器调用, 和第二个对PriorityQueue
<String>的调用使用了由Collection.reverseOrder() (新添加到IJava SE5中的)产生的反序的
Comparator 。
最后一部分添加了一个HashSet来消除重复的Character ,这么做只是为了增添点乐趣。
Integer 、String和Character可以与PriorityQueue一起工作,因为这些类已经内建了自然
排序。如果你想在PriorityQueue中使用自己的类,就必须包括额外的功能以产生自然排序,或
者必须提供自己的Comparator。在第17章中有一个更加复杂的示例将演示这种情况。
八、总结
4) 如果要进行大量的随机访问,就使用ArrayList ,如果要经常从表中间插入或删除元素,则应该使用LinkedList。
5) 各种Queue以及栈的行为,由LinkedList提供支持。
6) Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问,而
TreeMap保持"键"始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入
的顺序,但是也通过散列提供了快速访问能力。
7) Set不接受重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。
LinkedHashSet以插入顺序保存元素。
简单的容器分类:
你可以看到,其实只有四种容器: Map 、List 、Set和Queue ,它们各有两到三个实现版本
( Queue的java.util.concurrent实现没有包括在上面这张图中)。常用的容器用黑色粗线框表示。
点线框表示接口,实线框表示普通的(具体的)类。 带有空心箭头的点线表示一个特定的
类实现了一个接口,实心箭头表示某个类可以生成箭头所指向类的对象。例如,任意的
Collection可以生成Iterator , 而List可以生成ListIterator (也能生成普通的Iterator, 因为List继
承自Collection ) 。
-------------------------------