持有对象-容器

基本概念

Java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:

  1. Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常与它们被括入的顺序相同)。

  2. Map。一组成对的“键值对”对象,允许你使用键来査找值。 ArrayList允许你使用数字来査找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找某个对象,它也被称为 “关联数组,因为它将某些对象与另外一些对象关联在了一起;或者被称为“字典”,因为你可以使用键对象来査找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。

完整容器分类

如下图:
在这里插入图片描述

你可以看到.其实只有四种容器: Map、List、Set和Queue,它们各有两到三个实现版本(Queue的java.util.concurrent实现没有包括在上面这张图中)。常用的容器用黑色組线框表示。

点线框表示接口,虚线框表示abstract类,实线框表示普通的(具体的)类。带有空心箭头的点线表示一个特定的类实现了一个接口,实心箭头表示某个类可以生成箭头所指向类的对象。例如任意的Collection可以生成Iterator,而List可以生成ListIterator(也能生成普通的Iterator,因为List继承自Collection)。

List

List承诸可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。有两种类型的List:

  1. 基本的ArrayList,它长于随机访问元素,但是在List的中间插入和移除元素时较慢。
  2. LinkedList,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。

示例:

//这是一个辅助类
public class Dog {

	private int id;
	
	public Dog(int i){
		id = i;
	}
	public int getId(){
		return id;
	}
	public static List<Dog> getDogList(int i){
		List<Dog> list = new ArrayList<Dog>();
		if(i>0){
			for(int ti = 0;ti < i;ti++){
				list.add(new Dog(ti));
			}
		}
		return list;
	}
	@Override
	public String toString() {
		return "Dog:"+id+"";
	}
}
public class ListDemo {

	public static void main(String[] args) {
		List<Dog> list = Dog.getDogList(7);
		
		Dog d = new Dog(1);
		list.add(d);
		System.out.println("contains():"+list.contains(d));
		//输出:contains():true
		System.out.println("remove():"+list.remove(d));
		//输出:remove():true
		d = list.get(2);
		System.out.println("indexOf():"+list.indexOf(d));
		//输出:indexOf():2
		List<Dog> sub = list.subList(1,4);
		System.out.println("subList():"+sub);
		//输出:subList():[Dog:1, Dog:2, Dog:3]
		System.out.println("containsAll():"+sub.containsAll(list));
		//输出:containsAll():false
		System.out.println("retainAll():"+list.retainAll(sub));
		//输出:retainAll():true
		List<Dog> list2 = new ArrayList<Dog>();
		list2.add(new Dog(2));
		System.out.println("removeAll():"+list.removeAll(list2));
		//输出:removeAll():false
		System.out.println("set():"+list.set(1, new Dog(3)));
		//输出:set():Dog:2
		System.out.println("isEmpty():"+list.isEmpty());
		//输出:isEmpty():false
		list.clear();
		System.out.println("clear():"+list);
		//输出:clear():[]
		list = Dog.getDogList(7);
		Dog[] dlist = list.toArray(new Dog[0]);
		System.out.println("toArray():"+dlist[1].getId());
		//输出:toArray():1
	}
}

List允许在它被创建之后添加元素、移除元素,或者自我调整尺寸。这正是它的重要价值所在:一种可修改的序列。你可以用contains()方法来确定某个对象是否在列表中。如果你想移除一个对象,则可以将这个对象的引用传递给remove()方法。同样,如果你有一个对象的引用,则可以使用indexOf()来发现该対象在List中所处位置的索引编号。

在List中间插入元素是可行的,但是这带来了一个问题:对于LinkedList在列表中间插入和删除都是廉价操作,但是对于ArrayList这可是代价高昂的操作。这是否意味着应该永远都不要在ArrayList的中间插入元素,并最好是切换到LinkedList?不,这仅仅意味着,你应该意识到这个问题,如果你开始在某个ArrayList的中间执行很多插入操作,并且你的程序开始变慢,那么你应该看看你的List实现有可能就是罪魁祸首。(发现此类瓶颈的最佳方式是使用仿真器,就像你在http://MindView.net/Books/BetterJava上的补充材料中所看到的一样)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到你发现需要担心它了(尽管理解这些问题总是一种好的思路)。

subList()方法允许你很容易地从较大的列表中创建出一个片断,而将其结果传递给这个较大的列表的containsAll() 方法时,很自然地会得到true。还有一点也很有趣,那就是我们注意到顺序并不重要。

retainAll()方法是一种有效的“交集”操作。

removeAll()方法的行为也是基于equals()方法的。就像其名称所表示的,它将从List中移除在参数List中的所有元素。

set()方法的命名显得很不合时宜,因为它与Set类存在潜在的冲突。在此处,replace可能会显得更通合,因为它的功能是在指定的索引处(第一个参数),用第二个参数替换整个位置的元素。上例还有isEmpty()和clear()方法。你可以通过使用toArray()方法,将任意的Collection转换为一个数组。这是一个重载方法,其无参数版本返回的是Object数组,但是如果你向这个重载版本传送目标类型的数据,那么它将产生指定类型的数据(假设它能通过类型检査)。如果参数数组太小,存放不下List中的所有元素(就像上例一样),toArray()方法将创建一个具有合适尺寸的数组。

正如你所看到的,基本的List很容易使用,大多数时候只是调用add()添加对象,使用get()一次取出一个元素,以及调用iterator()获取用于该序列的Iterator。

迭代器

任何容器类都必须有某种方式可以插入元素并将它们再次取回。毕竞持有事物是容器最基本的工作。对于List,add()是插入元素的方法之一,而get()是取出元素的方法之一。

如果从更高层的角度思考,会发现这里有个缺点:要使用容器,必须对容器的确切类型编程。初看起来这没什么不好,但是考虑下面的情况:如果原本是对着List编码的,但是后来发现如果能够把相同的代码应用于Set,将会显得非常方便,此时应该怎么做?或者打算从头开始编写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就可以应用于不同类型的容器?

送代器(也是一种设计模式)的概念可以用于达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为基量级对象:创建它的代价小。因此经常可以见到对迭代器有些奇怪的限制。例如Java的Iterator只能单向移动,这个Iterator只能用来:

  1. 使用方法iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
  2. 使用next()获得序列中的下一个元素。
  3. 使用hasNext()检査序列中是否还有元素。
  4. 使用remove()将迭代器新近返回的元素删除。

例:

public class Demo {
	public static void main(String[] args) {
		List<Dog> list = Dog.getDogList(7);
		Iterator<Dog> it = list.iterator();
		while(it.hasNext()){
			Dog d = it.next();
			System.out.print(d+",");
		}
	}
	//输出:Dog:0,Dog:1,Dog:2,Dog:3,Dog:4,Dog:5,Dog:6,
}

有了 Iterator就不必为容器中元素的数量操心了,那是由 hasnext()和 next()关心的事情

ListIterator

ListIterator是一个更加强大的Iterator的子类型,它只能用子各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。例:

public class Demo {
	public static void main(String[] args) {
		List<Dog> list = Dog.getDogList(7);
		
		ListIterator<Dog> li = list.listIterator();
		while(li.hasNext()){
			Dog d = li.next();
			System.out.print(d+",");
		}
		System.out.println("");
		li = list.listIterator(7);
		while(li.hasPrevious()){
			Dog d = li.previous();
			System.out.print(d+",");
		}
	}
}

LinkedList

LinkedList也像AmyList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问操作方面却要逊色一些。LinkedList还添加了可以使其用作栈、队列或双端队列的方法。这些方法中有些彼此之间只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在Queue中)。例如, getFirst()和element()完全一样,它们部返回列表的头(第一个元素)而并不移除它,如果List为空则批出NoSuchElementException。peek()方法与这两个方式只是稍有差异,它在列表为空时返回null。removeFirst()与remove()也是完全一样的,它们移除并返回列表的头,而在列表为空时抛出NoSuchElementException。poll()稍有差异,它在列表为空时返回null。addFirst()与add()和addLast()相同,它们都将某个元素括入到列表的尾(端)部。removeLast()移除并返回列表的最后一个元素。例:

public class ListDemo {

	public static void main(String[] args) {
		LinkedList<Dog> ld = Dog.getDogLinkedList(7); 
		System.out.println("getFirst():"+ld.getFirst());
		//输出:getFirst():Dog:0
		System.out.println("element():"+ld.element());
		//输出:element():Dog:0
		System.out.println("peek():"+ld.peek());
		//输出:peek():Dog:0
		System.out.println("remove():"+ld.remove());
		//输出:remove():Dog:0
		System.out.println("removeFirst():"+ld.removeFirst());
		//输出:removeFirst():Dog:1
		System.out.println("poll():"+ld.poll());
		//输出:poll():Dog:2
		Dog d = new Dog(-1);
		ld.addFirst(d);
		System.out.println("addFirst():"+ld.getFirst());
		//输出:addFirst():Dog:-1
		Dog d2 = new Dog(8);
		ld.addLast(d2);
		System.out.println("addLast():"+ld.getLast());
		//输出:addLast():Dog:8
		System.out.println("removeLast():"+ld.removeLast());
		//输出:removeLast():Dog:8
		getFirst():Dog:0
	}
}

Stack

“栈”通常是指“后进先出(last in first out,LIFO)的容器。有时栈也被称为叠加栈,因为最后“压入”栈的元素,第一个“弹出”栈。经常用来类比栈的事物是装有弹簧的存放器中的自助餐托盘,最后装入的托盘总是最先拿出使用的。

LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用 。 不过有时一个真正的“栈”更能把事情讲清楚:

public class Demo {
	public static void main(String[] args) {
		Stack<String> s = new Stack<String>();
		for(String str : "My dog has fleas".split(" ")){
			s.push(str);
		}
		while(!s.empty()){
			System.out.print(s.pop()+",");
		}
	}
	//输出:fleas,has,dog,My,
}

Set

Set不保存重复的元素(至于如何判断元素相同则较为复杂,消后便会看到)。如果你试图将相同对象的多个实例添加到Set中,那么它就会阻止这种重复现象。Set中最常被使用的是测试归属性,你可以很容易地询问某个对象是否在某个Set中。正因如此査找就成为了Set中最重要的操作,因此你通常都会选择一个HashSet的实现,它专门对快速査找进行了优化。

Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)set是基于对象的值来确定归属性的。例:

public class Demo {
	public static void main(String[] args) {
		Random r = new Random(47);
		Set<Integer> set = new HashSet<Integer>();
		for(int i = 0;i<10000;i++){
			set.add(r.nextInt(30));
		}
		System.out.println(set);
	}
	//输出:[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所维护的顺序与TreeSet或LinkedHashSet都不同,因为它们的实现具有不同的元素存储方式TreeSet将元素存储在红—黑树数据结构中,而HashSet使用的是散列函数。LinkedHashList因为査询速度的原因也使用了散列,但是看起来它使用了链表来维护元素的括入顺序。

不同的Set实现不仅具有不同的行为,而且它们对于可以在特定的Set中放置的元素的类型也有不同的要求:
在这里插入图片描述
在 Hashset上打星号表示,如果没有其他的限制,这就应该是你默认的选择,因为它对速度进行了优化

SortedSet

SortedSet中的元素可以保证处于排序状态,这使得它可以通过在SortedSet接口中的下列方法提供附加的功能:Comparator comparator()返回当前Set使用的Comparator;或者返回null,表示以自然方式排序。

Object first()返回容器中的第一个元素。

Object last()返回容器中的最末一个元素。

SortedSet subSet(fromElement,toElement)生成此Set的子集,范围从fromElement(包含)到toElement(不包含)。

SortedSet headSet(toElement)生成此Set的子集,由小于toElement的元素组成。

SortedSet tailSet(fromElement)生成此Set的子集,由大于或等于fromElement的元素组成。

Map

Map映射表(也称为关联数组)的基本思想是它维护的是键—值(对)关联,因此你可以使用键来査找值。标准的Java类库中包含了Map的几种基本实现,包括:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap和IdentityHashMap。它们都有同样的基本接口Map。但是行为特性各不相同,这表现在效率、键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定“键”等价的策略等方面。Map接口实现的数量应该可以让你感觉到这种工具的重要性。

你可以获得对Map更深入的理解,这有助你观察关联数组是如何创建的。下面是个简单的例子:

public class AssociativeArray<K,V> {
	private Object[][] pairs;
	private int index;
	public AssociativeArray(int length){
		pairs = new Object[length][2];
	}
	public void put(K k,V v){
		if(index >= pairs.length){
			throw new ArrayIndexOutOfBoundsException();
		}else{
			pairs[index++] = new Object[]{k,v};
		}
	}
	@SuppressWarnings("unchecked")
	public V get(K k){
		for(int i = 0;i<index;i++){
			if(k.equals(pairs[i][0])){
				return (V)pairs[i][1];
			}
		}
		return null;
	}
	public String toString(){
		StringBuffer s = new StringBuffer();
		for(int i = 0;i < index;i++){
			s.append(pairs[i][0].toString());
			s.append(" : ");
			s.append(pairs[i][1].toString());
			if(i < index-1){
				s.append("\n");
			}
		}
		return s.toString();
	}
	
	public static void main(String[] args) {
		AssociativeArray<String, String> as = new AssociativeArray<String, String>(10);
		as.put("sky", "blue");
		as.put("grass", "green");
		as.put("ocean", "dancing");
		System.out.println(as.get("ocean"));
		System.out.println(as);
	}
	//输出:
	//dancing
	//sky : blue
	//grass : green
	//ocean : dancing
}

Map与数组和其他的Collection一样,可以很容易地扩展到多维。而我们只需将其值设置为Map(这些Map的值可以是其他容器,甚至是其他Map)。因此我们能够很容易地将容器组合起来从而快速地生成强大的数据结构。例如假设你正在跟踪拥有多个宠物的人,你所需只是一个Map<Person,List>;

性能

性能是映射表中的一个重要问题,当在get()中使用线性捜索时,执行速度会相当地慢,而这正是HashMap提高速度的地方,HashMap使用了特殊的值,称作散列码,来取代对键的缓慢搜索。散列码是“相对唯一”的、用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。hashCode()是根类Object中的方法,因此所有Java对象都能产生散列码。HashMap就是使用对象的hashCode()进行快速查询的,此方法能够显著提高性能。

下面是基本的Map实现。在HashMap上打星号表示如果没有其他的限制,它就应该成为你的默认选择,因为它对速度进行了优化。其他实现强调了其他的特性,因此都不如HashMap快。
在这里插入图片描述

SortedMap

使用SortedMap(TreeMap是其现阶段的唯一实现),可以确保键处于排序状态。这使得它具有额外的功能,这些功能由SortedMap接口中的下列方法提供:

Comparator comparator():返回当前Map使用的Comparator;或者返回null,表示以自然方式排序。T firstKey()返回Map中的第一个键。T lastKey()返回Map中的最末一个键。SortedMap subMap(fromKey,toKey)生成此Map的子集,范围由fromKey(包含)到toKey(不包含)的键确定。SortedMap headMap(toKey)生成此Map的子集,由键小于toKey的所有键值对组成。SortedMap tailMap(fromKey)生成此Map的子集,由键大于或等手fromKey的所有键值对组成。

LinkedHashMap

为了提高速度,LinkedHashMap散列化所有的元素,但是在遍历键值对时,却又以元素的插入顺序返回键值对(System.out.println()会迭代遍历该映射,因此可以看到遍历的结果)。此外,可以在构造器中设定LinkedHashMap,使之采用基于访同的最近最少使用(LRU)算法,于是没有被访问过的(可被看作需要删除的)元素就会出现在队列的前面。对于需要定期清理元素以节省空间的程序来说,此功能使得程序很容易得以实现。下面就是一个简单的例子,它演示了LinkedHashMap的这两种特点:

public class Demo {
	
	public static LinkedHashMap<Integer, String> getMapData(int l){
		LinkedHashMap<Integer, String> linkedMap = new LinkedHashMap<Integer, String>();
		for(int i = 0;i<l;i++){
			linkedMap.put(i, i+"");
		}
		return linkedMap;
	}
	
	public static void main(String[] args) {
		LinkedHashMap<Integer, String> linkedMap = new LinkedHashMap<Integer, String>();
		linkedMap.putAll(getMapData(7));
		System.out.println(linkedMap);
		linkedMap = new LinkedHashMap<Integer, String>(16,0.75f,true);
		linkedMap.putAll(getMapData(7));
		System.out.println(linkedMap);
		for(int i = 0 ; i < 5;i++){
			linkedMap.get(i);
		}
		System.out.println(linkedMap);
	}
	//输出:
	//{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6}
	//{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6}
	//{5=5, 6=6, 0=0, 1=1, 2=2, 3=3, 4=4}
}

散列与散列码

当你自己创建用作HashMap的键的类,有可能会忘记在其中放置必需的方法,而这是通常会犯一个错误。例:

public class Dog {
	private int id;
	public Dog(int i){id = i;}
	public int getId(){return id;}
}

public class Person {
	private int id;
	public Person(int i){id = i;}
	public int getId(){return id;}
}

public class Demo {
	public static void main(String[] args) {
		Map<Person, Dog> map = new  HashMap<Person,Dog>();
		for(int i = 0;i<7;i++){
			map.put(new Person(i), new Dog(i));
		}
		Person p = new Person(3);
		System.out.println(map.get(p));
	}
	//输出:null
}

这看起来够简单了,但是它不工作——它无法找到数字3这个键。问题出在Person自动地继承自基类Object,所以这里使用Object的hashCode()方法生成散列码,而它默认是使用对象的地址计算散列码。因此在循环里面生成的第一个实例的散列码与由后面new Person(3)生成的第二个实例的散列码是不同的,而我们正是使用后者进行査找的。

可能你会认为只需编写恰当的HashCode()方法的覆盖版本即可。但是它仍然无法正常运行,除非你同时覆盖equals()方法,它也是Object的一部分。HashMap使用equals()判断当前的键是否与表中存在的键相同。正确的equals()方法必须满足下列5个条件:

  1. 白反性。对任意x, x.equals(x)一定返回true.

  2. 对称性。对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。

  3. 传递性。对任意x、y、z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。

  4. 一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,回的结果应该保持一致,要么一直是true,要么一直是false。

  5. 对任何不是null的x,x.equals(null)一定返回false。

再次强调,默认的Object.equals()只是比较对象的地址,所以一个Person(3)并不等于另一个Person(3)。因此,如果要使用自己的类作为HashMap的键,必须同时重载HashCode()和equals(),如下所示:

public class Person {
	private int id;
	public Person(int i){id = i;}
	public int getId(){return id;}
	@Override
	public int hashCode() {return id;}
	@Override
	public boolean equals(Object obj) {
		return obj instanceof Person && id == ((Person)obj).getId();
	}
	public static void main(String[] args) {
		Map<Person, Dog> map = new  HashMap<Person,Dog>();
		for(int i = 0;i<7;i++){
			map.put(new Person(i), new Dog(i));
		}
		Person p = new Person(3);
		System.out.println(map.get(p));
	}
	//输出:Dog@61e481c1
}

理解hashCode()

前面的例子只是正确的解决问題的第一步,它只说明如果不为你的键覆盖hashCode()和equals(),那么使用散列的数据结构(HashSet、HashMap)LinkedHashSet和LinkedHashMap)就无法正确处理你的键。然而要很好地解决此问题,你必须了解这些数据结构的内部构造。

首先使用散列的目的在于:想要使用一个对象来査找另一个对象。不过使用TreeMap或者你自己实现的Map也可以达到此目的。

为速度而散列

散列的价值在于速度:散列使得査询得以快速进行。由于瓶颈位于键的査询速度,因此解决方案之一就是保持键的排序状态,然后使用Collections.binarySearch()进行查询。

散列则更进一步,它将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以使用它来表示键的信息(请小心留意,我是说键的信息而不是键本身)。但是因为数组不能调整容量,因此就有一个问題:我们希望在Map中保存数量不确定的值,但是如果键的数量被数组的容量限制了,该怎么办呢?

答案就是:数组并不保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。 这个数字就是散列码,由定义在Object中的、且可能由你的类覆盖的hashCode()方法(在计算机科学的术语中称为散列函数)生成。

为解决数组容量被固定的问题,不同的键可以产生相同的下标。也就是说可能会有冲突。因此数组多大就不重要了,任何键总能在数组中找到它的位置。

于是査询一个值的过程首先就是计算散列码,然后使用散列码查询数组。如果能够保证没有冲突(如果值的数量是固定的,那么就有可能),那可就有了一个完美的散列函数,但是这种情况只是特例。通常冲突由外部链接处理:数组并不直接保存值,而是保存值的list,然后对list中的值使用equals()方法进行线性的査的。这部分的査的自然会比较慢,但是如果散列函数好的话,数组的每个位置就只有较少的值。因此不是査找整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这便是HashMap会如此快的原因。

理解了散列的原理,我们就能实现一个简单的散列Map了:

public class MapEntry<K,V> implements Map.Entry<K, V>{
	private K k;
	private V v;
	public MapEntry(K k ,V v) {
		this.k = k;
		this.v = v;
	}
	public K getKey(){return k;}
	public V getValue(){return v;}
	public V setValue(V value) {v = value;return v;}
}
public class SimpleHashMap<K,V> extends AbstractMap<K, V> {
	
	static final int SIZE = 997;
	@SuppressWarnings("unchecked")
	LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE];
	
	public V put(final K key,final V value){
		V oldValue = null;
		int index = Math.abs(key.hashCode()) % 997;
		if(buckets[index] == null){
			buckets[index] = new LinkedList<MapEntry<K, V>>();
		}
		LinkedList<MapEntry<K, V>> bucket = buckets[index];
		MapEntry<K, V> pair = new MapEntry<K,V>(key,value);
		
		boolean found = false;
		ListIterator<MapEntry<K, V>> it = bucket.listIterator();
		while(it.hasNext()){
			
			MapEntry<K, V> iPair = it.next();
			if(iPair.getKey().equals(key)){
				oldValue = iPair.getValue();
				it.set(pair);
				found = true;
				break;
			}
		}
		if(!found){
			buckets[index].add(pair);
		}
		return oldValue;
	}
	
	public V get(Object key){
		int index = Math.abs(key.hashCode()) % 997;
		if(buckets[index] == null) return null;
		for(MapEntry<K, V> iPir : buckets[index]){
			if(iPir.getKey().equals(key)){
				return iPir.getValue();
			}
		}
		return null;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Set<Map.Entry<K, V>> entrySet(){
		Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K,V>>();
		for(LinkedList<MapEntry<K, V>> bucket : buckets){
			if(bucket == null) continue;
			for(MapEntry<K, V> mpair : bucket){
				set.add((java.util.Map.Entry<K, V>) mpair);
			}
		}
		return set;
	}
	public static void main(String[] args) {
		SimpleHashMap<String, String> m = new SimpleHashMap<String, String>();
		m.put("1", "1");
		System.out.println(m);
	}
	//输出:{1=1}
}

由于散列表中的“槽位”(slot)通常称为桶位(buckcl),因此我们将表示实际散列表的数组命名为bucket。为使散列分布均匀,桶的数量通常使用质一数。注意,为了能够自动处理冲突,使用了一个LinkedList的数组,每一个新的元素只是直接添加到list末尾的某个特定桶位中。即使Java不允许你创建泛型数组,那你也可以创建指向这种数组的引用。这里向上转型为这种数组是很方便的,这样可以防止在后面的代码中进行额外的转型。

对于put()方法,hashCode()将针对键而被调用,并且其结果被强制转换为正数。为了使产生的数字适合bucket数组的大小,取模操作符将按照该数组的尺寸取模。如果数组的某个位置是null,这表示还没有元素被散列至此,所以为了保存刚散列到该定位的对象,需要创建一个新的LinkedList。一般的过程是,査看当前位置的list中是否有相同的元素,如果有则将旧的值赋给oldValue,然后用新的值取代旧的值。标记found用来跟除是否找到(相同的)旧的键值对,如果没有则将新的键值对添加到list的末尾。

get()方法按照与put()方法相同的方式计算在buckets数组中的索引(这很重要,因为这样可以保证两个方法可以计算出相同的位置)如果此位置有LinkedList存在就对其进行査询。

注意,这个实现并不意味着对性能进行了调优。它只是想要展示散列映射表执行的各种操作。如果你浏览一下java.util.HashMap的源代码,你就会看到一个调过优的实现。同样,为了简单,SimpleHashMap使用了与SlowMap相同的方式来实现entrySet(),这个方法有些过于简单,不能用于通用的Map。

事实证明,质数实际上并不是散列桶的理想容量。近来(经过广泛的测试)Java的散列函数都使用2的整数次方。对现代的处理器来说,除法与求余数是最慢的操作。使用2的整数次方长度的散列表,可用掩码代替除法。因为get()是使用最多的操作,求余数的%操作是其开销最大的部分,而使用2的整数次方可以消除此开销(也可能对hashCode()有些影响)。

覆盖hashCode()

在明白了如何散列之后,编写自己的hashCode()就更有意义了。

首先你无法控制bucket数组的下标值的产生。这个值依赖于具体的HashMap对象的容量,而容量的改变与容器的充满程度和负载因子有关。hashCode()生成的结果,经过处理后成为桶位的下标(在上例中只是对其取模,模数为bucket数组的大小) 。

设计hashCode()时最重要的因素就是:无论何时对同一个对象调用hashCode()都应该生成同样的值。如果在将一个对象用put()添加进HashMap时产生一个hashCode()值,而用get()取出时却产生了另一个hashCode()值,那么就无法重新取得该对象了。所以,如果你的hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()就会生成一个不同的散列码,相当子产生了一个不同的键。

此外也不应该使hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值,这只能产生很精糕的hashCode()。因为这样做无法生成一个新的键,使之与put()中原始的键值对中的键相同。应该使用对象内有意义的识别信息。

因此,要想使hashCode()实用,它必须速度快并且必须有意义。也就是说,它必须基于对象的内容生成散列码。记得吗,散列码不必是独一无二的(应该更关注生成速度而不是唯一性),但是通过hashCode()和equals()必须能够完全确定对象的身份。因为在生成桶的下标前,hashCode()还需要做进一步的处理,所以散列码的生成范围并不重要,只要是int即可。

还有另一个影响因素:好的hashCode()应该产生分布均匀的散列码。如果散列码都集中在一块,那么HashMap或者HashSet在某些区域的负载会很重,这样就不如分布均匀的散列函数快。

在Effective Java Programming Language Guide(Addison-Wesley 2001)本书中,Joshua Bloch为怎样写出一份像样的hashCode()给出了基本的指导:

  1. 给int变量result赋予某个非零值常量,例如17。
  2. 为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:
    在这里插入图片描述
  3. 合井计算得到的散列码:result = 37 * result + c
  4. 返回result。
  5. 检査hashCode()最后生成的结果,确保相同的对象有相同的散列码。

下面便是遵循这些指导的一个例子:

public class CountedString {
	private String s;
	private int id = 0;
	private static List<String> created = new ArrayList<String>();
	
	public CountedString(String str){
		s = str;
		created.add(s);
		for(String s2 : created){
			if(s2.equals(s)){
				id++;
			}
		}
	}
	@Override
	public String toString() {
		return "String:"+s+",id="+id+",hashCode():"+hashCode();
	}
	
	@Override
	public int hashCode() {
		int result = 17;
		result = 37 * result + s.hashCode();
		result = 37 * result + id;
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		return obj instanceof CountedString &&
				s.equals(((CountedString)obj).s) && 
				id == ((CountedString)obj).id;
	}
	public static void main(String[] args) {
		Map<CountedString, Integer> map = new HashMap<CountedString, Integer>();
		CountedString[] cs = new CountedString[5];
		for (int i = 0; i < cs.length ;i++ ) {
			cs[i] = new CountedString("hi");
			map.put(cs[i], i);
		}
		System.out.println(map);
		for(CountedString cstring : cs){
			System.out.println("Looking up "+cstring);
			System.out.println(map.get(cstring));
		}
	}
	//输出:
	//{String:hi,id=1,hashCode():146447=0, String:hi,id=2,hashCode():146448=1, String:hi,id=3,hashCode():146449=2, String:hi,id=4,hashCode():146450=3, String:hi,id=5,hashCode():146451=4}
	//Looking up String:hi,id=1,hashCode():146447
	//0
	//Looking up String:hi,id=2,hashCode():146448
	//1
	//Looking up String:hi,id=3,hashCode():146449
	//2
	//Looking up String:hi,id=4,hashCode():146450
	//3
	//Looking up String:hi,id=5,hashCode():146451
	//4
}

Queue

队列是一个典型的先进先出(first in first out,FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要,因为它们可以安全地将对象从一个任务传输给另一个任务。Queue在Java SE5中仅有的两个实现类是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能。

LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。可以将LinkedList向上转型为Queue。例:

public class Demo {
	public static void main(String[] args) {
		Queue<Character> qc = new LinkedList<Character>();
		for(char c : "Brontosaurus".toCharArray()){
			qc.offer(c);
		}
		while(qc.peek() != null){
			System.out.print(qc.remove()+",");
		}
	}
	//输出:B,r,o,n,t,o,s,a,u,r,u,s,
}

offer()方法是与Queue相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者返回false。peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementException异常。poll()和remove()方法将移除并返回
队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。

自动包装机制会自动地将nextlnt()方法的int结果转换为qc所需的Character对象。Queue接口窄化了对LinkedList的方法的访问权限,以使得只有恰当的方法才可以使用,因此你能够访问的LinkedList的方法会变少(这里你实际上可以将qc转型回LinkedList,但是至少我们不鼓励这么做)。

注意,与Queue相关的方法提供了完整而独立的功能。即,对于Queue所继承的Collection,在不需要使用它的任何方法的情况下,就可以拥有一个可用的Queue。

PriorityQueue

先进先出描述了最典型的**队列规则*。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。

优先级队列声明下一个弾出元素是最需要的元素(具有最高的优先级)。例如,构建一个消息系统,某些消息比其他消息更重要,因而应该更快地得到处理,那么它们何时得到处理就与它们何时到达无关。PriorityQueue添加到Java SE5中是为了提供这种行为的一种自动实现。

当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek(),poll()和remove()方法时获取的元素将是队列中优先级最高的元素。例:

public class Demo {
	public static void main(String[] args) {
		List<Integer> ilist = Arrays.asList(1,2,3,4,5,6,5,4,3,2,1);
		PriorityQueue<Integer> p = new PriorityQueue<Integer>(ilist);
		while(p.peek() != null){
			System.out.print(p.remove()+",");
		}
	}
	//输出:1,1,2,2,3,3,4,4,5,5,6,
}

双向队列

双向队列(双端队列)就像是一个队列,但是你可以在任何一端添加或移除元素。在LinkedList中包含支持双向队列的方法,但是在Java标准类库中没有任何显式的用于双向队列的接口。因此LinkedList无法去实现这样的接口,你也无法像在前面的示例中转型到Queue那样去向上转型到Deque。但是你可以使用组合来创建一个Deque类,并直接从LinkedList中暴露相关的方法:

public class Deque<T> {
	private LinkedList<T> deque = new LinkedList<T>();
	public void addFirst(T e){deque.addFirst(e);}
	public void addLast(T e){deque.addLast(e);}
	public T getFirst(T e){return deque.getFirst();}
	public T getLast(T e){return deque.getLast();}
	public T removeFirst(){return deque.removeFirst();}
	public T removeLast(){return deque.removeLast();}
	public int size(){return deque.size();}
	public String toString(){return deque.toString();}
	//and other method...
}

Collection接口

Collection是描述所有序列容器的共性的根接口,它可能会被认为是一个“附属接口”,即因为要表示其他若干个接口的共性而出现的接口。另外java.utiI.AbstractCollectjon类提供了Collection的默认实现,使得你可以创建AbstractCollection的子类型,而其中没有不必要的代码重复。

Collection和Iterator

使用接口描述的一个理由是它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。因此,如果我编写的方法将接受一个Collection,那么该方法就可以应用于任何实现了Collection的类——这也就使得一个新类可以选择去实现Collection接口,以便我的方法可以使用它。

在java中迭代器与Collection绑定到了一起,因为实现Collection就意味着需要提供iterator()方法。

Collection的功能方法

下面的表格列出了可以通过Collection执行的所有操作(不包括从0bject继承而来的方法)。因此它们也是可通过Set或List执行的所有操作(List还有额外的功能)。Map不是继承自Collection的。
在这里插入图片描述

Foreach与迭代器

foreach语法主要用于数组,但它也可以应用于任何Collection对象。例:

public class Demo {
	public static void main(String[] args) {
		List<Integer> ilist = Arrays.asList(1,2,3,4,5,6,5,4,3,2,1);
		PriorityQueue<Integer> p = new PriorityQueue<Integer>(ilist);
		for(Integer i : p){
			System.out.print(i+",");
		}
	}
}

之所以能够工作,是因为Java SE5 引入了一种新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被 foreach用来在序列中移动。

Arrays.asList()会生成一个List,它基于一个固定大小的数组,仅支持那些不会改变数组大小的操作,对它而言是有道理的。任何会引起对底层数指结构的尺寸进行修改的方法部会产生一个UnsupportedOperationException异常,以表示对未获支持操作的调用(一个编程错误)。而Collections.unmodifiableList()产生不可修改的列表。Arrays.asList()返回的List中的元素是可以修改的,因为这没有违反该List“尺寸固定”这一特性。但unmodifiableList()的结果在任何情况下都应该不是可修改的。

选择接口的不同实现

现在已经知道了,尽管实际上只有四种容器:Map、List、Set和Queue,但是每种接口都有不止一个实现版本。如果需要使用某种接口的功能,应该如何选择使用哪一个实现呢?

每种不同的实现各自的特征、优点和缺点。例如从容器分类图中可以看出,Hashtable、Vector和Stack的“特征”是它们是过去遗留下来的类,目的只是为了支持老的程序(最好不要在新的程序中使用它们)。不同类型的Queue只在它们接受和产生数值的方式上有所差异(但它在并发方面有其重要性)。

容器之间的区別通常归结为由什么在背后“支持”它们。也就是说所使用的接口是由什么样的数据结构实现的。例如因为ArrayList和LinkedList都实现了List接口,所以无论选择哪一个基本的List操作都是相同的。然而 ArrayList底层由数组支持,而LinkedList是由双向整表实现的,其中的每个对象包含数据的同时还包含指向链表中前一个与后一个元素的引用。因此如果要经常在表中插入或删除元素,LinkedList就比较合适(LinkedList还有建立在AbstractSequentialList基础上的其他功能),否则应该使用速度更快的ArrayList。

再举个例子,Set可被实现为TreeSet、HashSet或LinkedHashSet。每一种都有不同的行为:HashSet是最常用的,査询速度最快,LinkedHashSet保持元素插入的次序,TreeSet基于TreeMp,生成一个总是处于排序状态的Set。你可以根据所需的行为来选择不同的接口实现。

有时某个特定容器的不同实现会拥有一些共同的操作,但是这些操作的性能却并不相同。在这种情况下,你可以基于使用某个特定操作的频率,以及你需要的执行速度来在它们中间进行选择。

对List的选择

下面是对List操作中最本质部分的性能测试结果。
在这里插入图片描述
get和set测试是随机对List进行随机访问的。在输出中你可以看到对于背后有数组支撑的List和ArrayList,无论列表的大小如何,这些访同都很快速和一致。而对于LinkedList,访问时间对于较大的列表将明显增加。很显然如果你需要执行大量的随机访问,链接链表不会是一种好的选择。

iteradd测试使用迭代器在列表中间插入新的元素。对于ArrayList,当列表变大时其开销将变得很高昂,但是对于LinkedList相对来说比较低廉,并且不随列表尺寸而发生变化。这是因为ArrayList在插入时必须创建空间并将它的所有引用向前移动,这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此你可以认为无论列表尺寸如何变化其代价大致相同。

insert和remove测试都是使用了索引位置为5作为插入或移除点,而没有选择List两端的元素。 LinkedList对List的端点会进行特殊处理——这使得在将LinkedList用作Queue时,速度可以得到提高。但是如果你在列表的中间增加或移除元素,其中会包含随机访问的代价,我们已经看到了这在不同的List实现中变化很大。当执行在位置5的插入和移除时,随机访问的代价应该可以被忽略,但是我们将看不到对LinkedList端点所做的任何特殊优化操作。从输出中可以看出在LinkedList中的插入和移除代价相当低康,并且不随列表尺寸发生变化,但是对于ArrayList
插入操作代价特别高昂,并且其代价将随列表尺寸的增加而增加。

从Queue测试中,你可以看到LinkedList可以多么快速地从列表的端点插入和移除元素,这正是对Queue行为所做的忧化。

我们选择容器最佳的做法可能是将ArrayList作为默认首选,只有你需要使用额外的功能或者当程序的性能因为经常从表中间进行插入和删除而变差的时候,才去选择LinketlList。如果使用的是固定数量的元素,那么既可以选择使用背后有数组支持的List(就像Array.asList()产生的列表),也可以选择真正的数组。

对Set的选择

可以根据需要选择TreeSet、HashSet或者LinkedHashSet。下面是对Set操作中部分操作的性能测试结果。
在这里插入图片描述
HashSet的性能基本上总是比TreeSet好,特別是在添加和査询元素时,而这两个操作也是最重要的操作。TreeSet存在的唯一原因是它可以维持元素的排序状态。所以只有当需要一个排好序的Set时才应该使用TreeSet。因为其内部结构支持排序并且因为迭代是我们更有可能执行的操作,所以用TreeSet迭代通常比用HashSet要快。注意,对于插入操作,LinkedHashSet比HashSet的代价更高。这是由维护整表所带来额外开销造成的。

对Map的选择

下面是对Map中部分操作的性能测试结果。
在这里插入图片描述
除了IdentityHashMap,所有的Map实现的插入操作都会随着Map尺寸的变大而明显变慢。但是査找的代价通常比插入要小得多,这是个好消息,因为我们执行査找元素的操作要比执行插入元素的操作多得多。

Hashtable的性能大体上与HashMap相当。因为HashMap是用来替代Hashtable的,因此它们使用了相同的底层存储和査找机制,这并没有什么令人吃惊的。

TreeMap通常比HashMap要慢。与使用TreeSet一样,TreeMap是一种创建有序列表的方式。 树的行为是:总是保证有序,并且不必进行特殊的排序。一旦你填充了一个TreeMap,就可以调用keySet()方法来获取键的Set视图,然后调用toArray()来产生由这些键构成的数组。之后你可以使用静态方法Arrays.binarySearcb()在排序数组中快速査找对象。当然这只有在HashMap的行为不可接受的情况下方有意义,因为HashMap本身就被设计为可以快速查找键。你还可以很方便地通过单个的对象创建操作,或者是调用putAll(),从TreeMap中创建HashMap。最后,当使用Map时,你的第一选择应该是HashMap,只有在你要求Map始终保持有序时,才需要使用TreeMap。

LinkedHashMap在插入时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序)。正是由于这个列表使得其迭代速度更快。

IdentityHashMap则具有完全不同的性能,因为它使用==而不是equ als()来比较元素。

HashMap的性能因子

我们可以通过手工调整HashMap来提高其性能,从而满足我们特定应用的需求。为了在调整HashMap时让你理解性能问题,某些术语是必需了解的:

  1. 容量:表中的桶位数。
  2. 初始基量:表在创建时所拥有的桶位数。HashMap和HashSet都具有允许你指定初始容量的构造器。
  3. 尺寸:表中当前存储的项数。
  4. 负载因子:尺寸/容量。空表的负载因子是0,而半满表的负载因子是0.5,依此类推。负裁轻的表产生冲突的可能性小,因此对于插入和査找都是最理想的(但是会减慢使用迭代器进行遍历的过程)。HashMap和HashSet都具有允许你指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中(这被称为再散列)。

HashMap使用的默认负载因子是0.75(只有当表达到四分之三满时,方进行再散列)这个因子在时间和空间代价之间达到了平衡。更高的负载因子可以降低表所需的空间,但是会增加査找代价,这很重要,因为査找是我们在大多数时间里所做的操作(包括get()和put())。如果你知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。

实用方法

Java中有大量用于容器的卓越的使用方法,它们被表示为java.util.Collections类内部的静态方法。
在这里插入图片描述
注意,min()和max()只能作用于Collection对象,而不能作用于List。

快速报错

Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程介入其中并且插入、删除或修改此容器内的某个对象,那么就会出现问题:也许迭代过程已经处理过容器中的该元素了,也许还没处理,也许在调用size()之后容器的尺寸收缩了——还有许多灾难情景。Java容器类类库采用快速报错(fail-fast)机制。它会探査容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出ConcurrentModificationException异常。这就是快速报错的意思——即,不是使用复杂的算法在事后来检査问题。

持有引用

java.lang.ref类库包含了一组类,这些类为垃圾回收提供了更大的灵活性。当存在可能会耗尽内存的大对象的时候这些类显得特別有用。有三个继承自抽象类Reference的类:SoftReference、WeakReference和PhantomReference。当垃圾回收器正在考察的对象只能通过某个Reference对象才“可获得”时,上述这些不同的派生类为垃圾回收器提供了不同级别的间接性指示。

对象是可获得的(reachable),是指此对象可在程序中的某处找到。这意味着你在栈中有一个普通的引用,而它正指向此对象,也可能是你的引用指向某个对象,而那个对象含有另一个引用指向正在讨论的对象。也可能有更多的中间链接。如果一个对象是“可获得的”,垃圾回收器就不能释放它,因为它仍然为你的程序所用。如果一个对象不是“可获得的”,那么你的程序将无法使用到它,所以将其回收是安全的。

如果想继续持有对某个对象的引用,希望以后还能够访问到该对象,但是也希望能够允许垃圾回收器释放它,这时就应该使用Reference对象。这样你可以继续使用该对象,而在内存消耗殆尽的时使又允许释放该对象。

以Reference对象作为你和普通引用之间的媒介(代理),另外一定不能有普通的引用指向那个对象,这样就能达到上述目的。(普通的引用指没有经Reference对象包装过的引用。)如果垃圾回收器发现某个对象通过普通引用是可获得的,该对象就不会被释放。

SoftReference、WeakReference和PhantomReference。由强到弱排列,对应不同级别的“可获得性”。Softreference用以实现内存敏感的高速缓存。Weakreference是为实现“规范映射”(canonicalizing mappings)而设计的,它不妨碍垃圾回收器回收映射“键”(或“值”)。“规范映射”中对象的实例可以在程序的多处被同时使用,以节省存储空间。Phantomreference用以调度回收前的清理工作,它比java终止机制灵活。

使用SoftReference和WeakReference时,可以选择是否要将它们放入ReferenccQueue(用作回收前清理工作的工具)。而PhantomReference只能依赖于ReferenceQueue。

WeakHashMap

容器类中有一种特殊的Map,即WeakHashMap,它被用来保存WeakReference。它使得规范映射更易于使用。在这种映射中,每个值只保存一份实例以节省存储空间。当程序需要那个值的时候,便在映射中査的现有的对象,然后使用它(而不是重新再创建)。映射可将值作
为其初始化中的一部分,不过通常是在需要的时候才生成“值”。

这是一种节约存储空间的技术,因为WeakHashMap允许垃圾回收器自动清理键和值,所以它显得十分便利。对于向WeakHashMap添加键和值的操作,则没有什么特殊要求映射会自动使用WeakReference“包装它们。允许清理元素的触发条件是,不再需要此键了。

Java 1.0/1.1的容器

很不幸,许多老的代码是使用Java1.0/1.1的容器写成的,甚至有些新的程序也使用了这些类。因此,虽然在写新的程序时,决不应该使用旧的容器,但你仍然应该了解它们。不过旧容器功能有限,所以对它们也没太多可说的。毕竟它们都过时了,所以我也不想强调某些设计有多糟糕。

Vector和Enumeration
在Java 1.0/1.1中,Vector是唯一可以自我扩展的序列,所以它被大量使用。它的缺点多到这里都难以叙述。基本上可将其看作ArrayList,但是具有又长又难记的方法名。在订正过的Java容器类类库中,Vector被改造过,可将其归类为Collection和List。这样做有点不妥当,可能会让人误会Vector变得好用了,实际上这样做只是为了支持Java2之前的代码。

Java 1.0/1.1版的送代器发明了一个新名字——枚举,取代了为人熟知的术语(迭代器)。此Enumeration接口比Iterator小,只有两个名字很长的方法:一个为boolean hasMoreElements(),如果此枚举包含更多的元素,该方法就返回true;另一个为Object nextElement(),该方法返回此枚举中的下一个元素(如果还有的话),否则抛出异常。Enumeration只是接口而不是实现,所以有时新的类库仍然使用了旧的Enumeration,这令人十分遗憾,但通常不会造成伤害。虽然在你的代码中应该尽量使用Iterator,但也得有所准各, 类库可能会返回给你一个Enumeration。

Stack

前面在使用LinkedList时,介绍过“栈”的概念。Java 1.0/1.1的Stack很奇怪,竟然不是用Vector来构建Stack,而是继承Vector所以它拥有Vector所有的特点和行为,再加上一些额外的Stack行为。很难了解设计者是否意识到这样做特别有用处,或者只是一个幼稚的设计。唯一清楚的是,在匆忙发布之前它没有经过仔细审查,因此这个糟糕的设计仍然挂在这里(但是你永远都不应该使用它)。

前面曾经说过,如果需要栈的行为,应该使用LinkedList,或者从LinkedList类中创建的net.mindview.util.Stack类。

BitSet

如果想要高效率地存储大量“开/关”信息,BitSet是很好的选择不过它的效率仅是对空间而言,如果需要高效的访问时间,BitSet比本地数组稍慢一点。此外BitSet的最小容量是long:64位。如果存储的内容比较小,例如8位,那么BitSet就浪费了一些空间。因此如果空间对你很重要,最好撰写自己的类,或者直接来用数组来存储你的标志信息(只有在创建包含开关信息列表的大量对象,并且促使你做出决定的依据仅仅是性能和其他度量因素时,才属于这种情况。如果你做出这个决定只是因为你认为某些对象太大了,那么你最终会产生不需要的复杂性,并会浪费掉大量的时间)。另外普通的容器都会随着元素的加入而扩充其容量,BitSet也是。

如果拥有一个可以命名的固定的标志集合,那么EnumSet(枚举)与BitSet相比,通常是一种更好的选择,因为EnumSet允许你按照名字而不是数字位的位置进行操作,因此可以减少错误。EnumSet还可以防止你因不注意而添加新的标志位置,这种行为能够引发严重的、难以发现的缺陷。你应该使用BitSet而不是EnumSet的理由只包括:只有在运行时才知道需要多少个标志;对标志命名不合理;需要BitSet中的某种特殊操作(査看BitSet和EnumSet的JDK文档)。

总结

Java提供了大量持有对象的方式:
1)数组将数字与对象联系起来。它保存类型明确的对象,査询对象时不需要对结果做类型转换。它可以是多维的,可以保存基本类型的数据。但是数组一旦生成,其容量就不能改变。

2)Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,你就可以指定容器中存放的对象类型,因此你就不会将错误类型的对象放置到容器中,并且在从容器中获取元素时,不必进行类型转换。各种Collection和各种Map都可以在你向其中添加更多的元素时,自动调整其尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。

3)像数组一样,List也建立数字索引与对象的关联,因此数组和List都是排好序的容器。List能够自动扩充容量。

4)如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,则应该使用LinkedList。

5)各种Queue以及栈的行为,由LinkedList提供支持。

6)Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问;而TreeMap保持“键”始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问能力。

7)Set不接受重复元素。HashSet提供最快的査询速度,而TreeSet保持元素处于排序状态。 LinkedHashSet以插入顺序保存元素。

8)新程序中不应该使用过时的Vector、Hashtable和Stack。


  1. 本文来源《Java编程思想(第四版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值