欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

黑马程序员——Java 类集,黑马程序员java

来源: javaer 分享于  点击 1056 次 点评:30

黑马程序员——Java 类集,黑马程序员java


------- android培训java培训、期待与您交流! ----------


Java类集

 

1. 集合接口

 

1.1 将集合的接口与实现分离

 

与现代的数据结构类库的常见情况一样,Java集合类库也将接口(interface)与实现(implemenetations)分离。首先,看一下数据结构——队列(Queue)是如何分离的。

队列接口可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照FIFO(先进先出)的规则检索对象时就应该使用队列(Queue)。

一个列队接口的最小形式可能类似下面这样:

interface Queue<E> 
{ //a simplified form of the interface in the standard
	void add(E e);
	E element();
	E remove();
	int	size();
}

 

这个接口并没有说明队列是如何实现的。一般队列有两种实现方式:一种是使用循环数组,另一种是使用链表。

每一个实现都可以通过一个实现了Queue接口的类表示:

class CircularArrayQueue<E> implements Queue<E>//not an actual library class
{     
   public CircularArrayQueue(int capacity) {//... } 
   public boolean offer(E newElement) {//... } 
   public E poll() {//... } 
   public int size() {//... }
   private E[] elements; 
   private int head; 
   private int tail; 
}	
//not an actual library class
class LinkedListQueue<E> implements Queue<E>
{
	LinkedListQueue(){//...}
	public void add(E element){//...}
	public E remove(){//...}
	public int size(){//...}
	private Link head;
	private Link tail;
}

注意:实际上,Java类库中没有名为CircularArrayQueue和LinkedListQueue的类。这里,只是以这些类作为示例,解释一下集合接口和实现在概念上的不同。如果需要一个循环数组队列,直接可以用JDK提供的ArrayDeque类。如果需要一个链表队列,直接可以使用LinkedList类,这个类实现了Queue接口。

当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了那种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。

Queue<Customer> express = new CircularArrayQueue<Customer>(100);
express.add(new Customer("Harry"));

利用这种方式,一旦改变了想法,可以轻松地使用另外一种不同的实现。只需对程序的一个地方做出修改,即调用构造器的地方。比如觉得LinkedListQueue是个更好的选择,就可将代码修改为:

Queue<Customer> express = new LinkedListQueue<Customer>(100);
express.add(new Customer("Harry"));


1.2 Java类库中的集合接口与迭代器接口

 

在Java类库中,集合的基本接口是Collection接口,这个接口有两个基本方法:

boolean add(E e);
Iterator<E> iterator();

除了这两个方法外,还有几个方法,将在后面说道。add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。例如,试图向集合中添加一个对象,而这个对象在集中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象。

iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。

①.迭代器

Iterator接口包含了三个方法:

public interface Iterator<E> {
    boolean hasNext();
    E next();
	void remove();
}

 通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果达到了集合的末尾,next方法将会抛出一个NoSuchElementException(没有可迭代元素)。因此,需要在调用next方法之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就会返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true反复地调用next方法。如:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while(iter.hasNext()){
	String element = iter.next();
	//do something with element
}

JDK5.0起,这个循环可以采用一种更优雅的方式,用“foreach”循环可以更加简练地表示同样的操作:

Collection<String> c = ...;
for(String element : c){
	//do something with element
}

for each”循环可以与任何实现了Iterable接口的对象一起工作,这个接口只有一个方法,

public interface Iterable<E>
{
	Iterator<E> iterator();
}

Collection接口扩展了Iterable接口。因此,对于标准类库中的任何集合都可以使用“for each”循环。

集合元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,没迭代一个,索引值加1。如果是访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能遍历到集合中的所有元素,但却无法预知元素被访问的次序。

Java集合类库中迭代器与其他类库中的迭代器在概念上有着重要的区别。在传统的集合类库中,例如,C++的标准模板库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引i就可以查看数组元素a[i]一样。不需要查找元素,就可以将迭代器向前移动一个位置。这与不需要执行查找操作就可以通过i++将数组索引向前移动一样。但是,Java的迭代器并不是这样操作的,查找操作与位置变得更紧密相连了。查找一个元素的唯一方法就是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动。因此,应该将Java的迭代器认为是位于两个元素之间。当调用next方法时,迭代器就会越过下一个元素,并返回刚刚越过的那个元素的引用。如下图:



 

 

 

②.删除元素

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是否具有实际意义。如果想要删除指定位置上的元素,仍然需要越过这个元素,下面是如何删除字符串集合中的第一个元素的方法:

Iterator<String> iter = c.iterator();
iter.next();
iter.remove();

更重要的是,对next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next方法是不合法的,将会抛出一个IllegalStateException异常。

iter.remove();
iter.remove();//error!!

所以必须先调用next越过将要删除的元素。

iter.remove();
iter.next();
iter.remove();//ok!!

③.泛型实用方法

由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。例如,下面是一个检测任意集合中是否包含指定元素的泛型方法:

public static <E> boolean contains(Collection<E> c, Object obj){
	for(E elemenet : c){
		if(element.equals(obj)){
			return true;
		}
	}
	return false;
}

Java类库提供了很多实用的方法,我们不必自己重新构建这些方法,使用起来非常方便。实际上Collection接口声明很多有用的方法,所有实现类都必须提供这些方法。

public abstract boolean java.util.Collection.add(java.lang.Object)
public abstract boolean java.util.Collection.equals(java.lang.Object)
public abstract int java.util.Collection.hashCode()
public abstract void java.util.Collection.clear()
public abstract boolean java.util.Collection.contains(java.lang.Object)
public abstract boolean java.util.Collection.isEmpty()
public abstract boolean java.util.Collection.addAll(java.util.Collection)
public abstract java.util.Iterator java.util.Collection.iterator()
public abstract int java.util.Collection.size()
public abstract java.lang.Object[] java.util.Collection.toArray()
public abstract java.lang.Object[] java.util.Collection.toArray(java.lang.Object[])
public abstract boolean java.util.Collection.remove(java.lang.Object)
public abstract boolean java.util.Collection.containsAll(java.util.Collection)
public abstract boolean java.util.Collection.removeAll(java.util.Collection)
public abstract boolean java.util.Collection.retainAll(java.util.Collection)

在这些方法中,许多方法的功能非常明确,不需要过多的解释。当然,如果实现Collection接口的每一个类都要提供如此多的方法是一件很烦人的事情。为了能够让我们更加容易实现这个接口,Java类库提供了一个类AbstractCollection,它将基础方法size和iterator抽象化了

public boolean java.util.AbstractCollection.add(java.lang.Object)
public java.lang.String java.util.AbstractCollection.toString()
public void java.util.AbstractCollection.clear()
public boolean java.util.AbstractCollection.contains(java.lang.Object)
public boolean java.util.AbstractCollection.isEmpty()
public boolean java.util.AbstractCollection.addAll(java.util.Collection)
public abstract java.util.Iterator java.util.AbstractCollection.iterator()
public abstract int java.util.AbstractCollection.size()
public java.lang.Object[] java.util.AbstractCollection.toArray()
public java.lang.Object[] java.util.AbstractCollection.toArray(java.lang.Object[])
public boolean java.util.AbstractCollection.remove(java.lang.Object)
public boolean java.util.AbstractCollection.containsAll(java.util.Collection)
public boolean java.util.AbstractCollection.removeAll(java.util.Collection)
public boolean java.util.AbstractCollection.retainAll(java.util.Collection)
private static java.lang.Object[] java.util.AbstractCollection.finishToArray(java.lang.Object[],java.util.Iterator)

此时,一个具体的集合类可以扩展AbstractCollection类了。现在要由具体的集合类提供iterator方法,而contains方法已由AbstractCollection超类提供了。然而,如果子类有更好的方式实现contains方法,也可由子类提供,就这点而言,没有什么限制。



2. 具体的集合

 

Java类库中的集合,并简要描述了每个集合类的用途:

除了以Map结尾的类之外,其他类都实现类Collection接口。而以Map结尾的类实现类Map接口。


2.1 链表

在很多示例中已经使用了数组及动态的ArrayList类。然而,数组和数组列表都有一个重大的缺陷。就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。同样在数组中间的位置插入一个元素也是如此。


为了解决这个问题,我们可以使用数据结构——链表(Linked List)。尽管数组在连续的存储位置上存放对象的引用,但链表却将每个对象存放在独立的结点中,每个结点还存放着序列中下一个结点的引用。在Java中,所有的链表实际上都是双向链表(doubly linked)——即每个结点还存放指向前驱结点的引用。要从链表中间删除一个元素是很轻松的操作,集需要对被删除元素附近的结点更新一下即可。


 

在LinkedList中操作元素是非常简单的,如下面的代码示例:

List<String> staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Car");
Iterator<String> iter = staff.iterator();
String first = iter.next();
String second = iter.next();
iter.remove();//删除第2个元素

但是,链表与泛型集合之间有一个重要的区别。链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表中间。由于迭代器是描述集合中位置的,所有这种依赖于位置的add方法将由迭代器负责。只有对有自然有序的集合使用迭代器添加元素才有实际意义。例如,在后面将要说到的集(Set)类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含了add方法。

public abstract void java.util.ListIterator.add(java.lang.Object)
void add(E e);

Collectionadd方法不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表。另外,ListIterator接口有两个方法,可以用来反向遍历链表。

public abstract boolean java.util.ListIterator.hasPrevious()
public abstract java.lang.Object java.util.ListIterator.previous()

与next方法一样,previous方法返回越过的对象。LinkedList类的listIterator方法返回一个实现类ListIterator接口的迭代器对象。

ListIterator<String>  iter = staff.listIterator();

add方法在迭代器位置之前添加一个新对象。例如,下面的代码将越过链表中的第一个元素,并在第二个元素之前添加“Tom”;

List<String> staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Car");
ListIterator<String>  iter = staff.listIterator();
iter.next();
iter.add("Tom");

如果多次调用add方法,将按照提供的次序把元素添加到链表中。它们被依次添加到迭代器当前位置之前。

注意:在用“光标”类比时要格外小心。Remove操作与Backspace键的工作方式不一样。在调用next之后,remove方法确实与Backspace键一样删除了迭代器左侧的元素。但是,如果调用previous就会将右侧的元素删除掉,并且不能在同一行中调用两次remove方法。add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态。

最后需要说明,set方法用一个新的元素取代调用next或perivous方法返回的上一个元素。例如,下面用一个新值取代链表的第一个元素:

ListIterator<String>  iter = staff.listIterator();
String oldValue = iter.next();//返回第一个元素
iter.set(newValue);//第一个元素置为newValue

如果有两个迭代器,在一个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。

Iterator<String> iter = staff.iterator();
Iterator<String> iter2 = staff.iterator();
iter.next();
iter.remove();
iter2.next(); //java.util.ConcurrentModificationException

由于iter2检测到这个链表被外部修改了,所以对iter2的调用抛出一个ConcurrentModificationException异常。为了避免发生并发修改的异常,一般遵循下面简单的规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个即能读又能写的迭代器。

在Java类库中,还提供了许多在理论上存在一定争议的方法。链表不支持快速地随机访问。如果要查看链表中第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个因素,在程序需要采用整数索引访问元素,就不选用链表,而采用数组。尽管如此,LinkedList类还是提供了一个用来访问某个特定元素的get方法,当然,这个方法的效率并不太高。

List<String> staff = new LinkedList<String>();
String obj = staff.get(n);

绝不应该使用这种让人误解的随机访问方法来遍历链表。下面这段代码的效率极低:

for(int i= 0; i < staff.size(); i++){
	do something with staff.get(i);
}

每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。

列表迭代器接口(ListIterator)还有一个方法,可以告知当前位置的索引。实际上,从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以同时产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex方法返回下次调用previous方法时返回元素的整数索引。当然,这个索引只比nextIndex返回的索引值小1.这两个方法的效率非常高,这是因为迭代器保持着当前位置的计数值。最后需要说一下,如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。也就是说,调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。

如果链表中只有很少几个元素,就完全没有必要为get方法和set方法的开销而烦恼。但是,为什么要优先使用链表呢?使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价。如果列表只有几个元素,完全可以使用ArrayList。如果需要对集合进行随机访问,就是用数组或ArrayList,而不要使用链表。

 

2.2 数组列表

 

上面说到了List接口和实现了这个接口的LinkedList类。List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是迭代器,另一种是用get和set方法随机地访问每个元素。后一种对数组非常有用。集合类库提供了一种我们比较熟悉的ArrayList类,这个类也实现类List接口。ArrayList封装了一个动态再分配的对象数组。

注意:在需要使用动态数组时,有时可能会使用到Vector类。Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象。但是,如果由一个线程访问Vector,要在同步操作上耗费大量的时间。这个情况是非常常见的。而ArrayList的方法不是同步的。因此,建议在不需要同步时使用ArrayList,而不要使用Vector。

 

2.3 散列集

 

 

链表和数组可以按照我们的意愿排列元素的次序。但是,如果想要查看某个指定的元素,却又忘记了它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多,将会消耗很多时间,如果不在意元素的顺序,可以有几种能够快速查找元素的数据结构。其缺点是无法控制元素出现的次序。它们将按照有利于其操作目的的原则组织数据。

有一种我们所熟悉的数据结构,可以快速地查找所需要的对象,就是散列表(hash table)。散列表为每一个对象计算一个整数,称为哈希码(hash code)。哈希码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的哈希码。

String str1 = "Lee";
String str2 = "lee";
String str3 = "eel";
System.out.println(str1.hashCode());//76268
System.out.println(str2.hashCode());//107020
System.out.println(str3.hashCode());//100300

它们都是由String类的hashCode方法产生的。

如果是自定义类,就要负责实现这个类的hashCode方法。实现的hashCode方法要与equals方法兼容,即如果a.equals(b)为true,a与b必须具有相同的哈希码。

在Java中,哈希表(散列表)用链表数组实现。每个列表(结点)被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的哈希码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的哈希码为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)。或许会很幸运,在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果哈希码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。

如果想更多地控制散列表的运行性能,就要指定一个初始的桶数(结点)。桶数是用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。如果大致知道最终会有多少个元素插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是2的幂,默认值为16。当然,并不是总能够知道需要存储多少个元素的,也有可能初始的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填如元素,这个表就会用双倍的桶数自动进行再散列。对于大多数应用程序来说,装填因子为75%是比较合理的。

散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。Set是没有重复元素的元素集合。set的add方法首先在集中查找添加的对象,如果不存在,就将这个对象添加进去。Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素,contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。

散列集迭代器将依次访问所有的桶(结点)。由于散列将元素分散咋爱表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用HashSet。

示例程序将从System.in读取单词,然后将它们添加到集中,最后,再打印出集中的所有单词。通过命令行运行:java SetTest < words.txt

public class SetTest
{
   public static void main(String[] args)
   {
      Set<String> words = new HashSet<String>(); // HashSet implements Set
      long totalTime = 0;

      Scanner in = new Scanner(System.in);
      while (in.hasNext())
      {
         String word = in.next();
         long callTime = System.currentTimeMillis();
         words.add(word);
         callTime = System.currentTimeMillis() - callTime;
         totalTime += callTime;
      }

      Iterator<String> iter = words.iterator();
      for (int i = 1; i <= words.size(); i++){
         System.out.print(iter.next()+"\t");
	  }
	  System.out.println();	
      System.out.println(". . .");
      System.out.println(words.size() + " distinct words. " + totalTime + " milliseconds.");
   }
}

这个程序将读取输入的所有单词,并且将它们添加到散列集中。然后遍历散列集中的不同单词。单词以随机的顺序出现。

words.txt
This program uses a set to print all unique words in System.in

输出结果:

C:\code>java SetTest < words.txt
to unique  words set  program a  uses  System.in  in   This  all   print

 HashSet<E> API

 

public HashSet()

构造一个新的空 set,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。

public HashSet(Collection<? extends E> c)

构造一个包含指定 collection 中的元素的新 set。使用默认的加载因子 0.75 和足以包含指定 collection 中所有元素的初始容量来创建 HashMap。

public HashSet(int initialCapacity, float loadFactor)

构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和指定的加载因子。

int hashCode()

返回这个对象的哈希码。哈希码可以是任何整数,包括正数或负数。equals和hashCode的定义必须兼容,即如果x.equals(y)为true,x.hashCode 必须等于y.hashCode()。

 

 

2.4 树集 

 

TreeSet与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。例如,假设插入3个字符串,然后访问添加的所有元素。

Set<String> set = new TreeSet<String>();
set.add("Bob");
set.add("Amy");
set.add("Carl");
for(String str : set){
	System.out.println(str);
}

每个值将按照顺序打印出来:Amy Bob Carl。正如TreeSet类名一样,排序是用树结构完成的。每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问元素。

相比将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相对快很多。如果树中包含n个元素,查找新元素的正确位置平均需要log2n次比较。例如,如果一棵树包含了1000个元素,添加一个新元素大约需要比较10次。

将一个元素添加到TreeSet中要比添加到HashSet中慢,如下表所示:

TreeSet<E> API

public TreeSet()

构造一个新的空 set,该 set 根据其元素的自然顺序进行排序。插入该 set 的所有元素都必须实现 Comparable 接口。另外,所有这些元素都必须是可互相比较的:对于 set 中的任意两个元素 e1 和 e2,执行 e1.compareTo(e2) 都不得抛出 ClassCastException。如果用户试图将违反此约束的元素添加到 set(例如,用户试图将字符串元素添加到其元素为整数的 set 中),则 add 调用将抛出 ClassCastException。

public TreeSet(Comparator<? super E> comparator)

构造一个新的空 TreeSet,它根据指定比较器进行排序。插入到该 set 的所有元素都必须能够由指定比较器进行相互比较:对于 set 中的任意两个元素 e1 和 e2,执行 comparator.compare(e1, e2) 都不得抛出 ClassCastException。如果用户试图将违反此约束的元素添加到 set 中,则 add 调用将抛出 ClassCastException。

参数:comparator - 将用来对此 set 进行排序的比较器。如果该参数为 null,则使用元素的自然顺序。

public TreeSet(Collection<? extends E> c)

构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。插入该 set 的所有元素都必须实现 Comparable 接口。另外,所有这些元素都必须是可互相比较的:对于 set 中的任意两个元素 e1 和 e2,执行 e1.compareTo(e2) 都不得抛出 ClassCastException。

public TreeSet(SortedSet<E> s)

构造一个与指定有序 set 具有相同映射关系和相同排序的新 TreeSet。

 

 

2.5 比较对象

 

TreeSet在默认情况下,集中插入的元素实现了Comparable接口。这个接口定义了一个方法:

public interface Comparable<T>
{
	int compareTo(T other);
}

比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

例如,String类就是实现了Comparable接口,这个类的compareTo方法依据字典序对字符串进行比较。如果要插入自定义的对象,就必须通过实现Comparable 接口自定义排列顺序。在Object类中,没有提供任何compareTo接口的默认实现。例如,下面的代码展示了如何用部件编号对Item对象进行排序。

class Item implements Comparable<Item>
{ 
	public int compareTo(Item other){  
		return partNumber - other.partNumber;
	}
	...
}

如果对两个正整数进行比较,就像上面示例中的部件编号,就可以直接地返回他们的差。如果第一项位于第二项的前面,就返回负值,如果部件编号相同就返回0,否则返回正值。

然而,使用Comparable接口定义排列排序显然有局限性。对于一个给定的类,只能够实现这个接口一次。如果在一个集合中需要按照部件编号进行排序,在另一个集合中却要按照描述的信息进行排序,该怎么办呢?另外,如果需要对一个类的对象进行排序,而这个类的创建者又没有实现Comparable,又该怎么办呢?在这种情况下将Comparator对象传递给TreeSet构造器告诉树集使用不同的比较方法。Comparator接口声明了一个带有两个显式参数的compare方法:

public interface Comparator<T>
{
	int compare(T o1,T o2);
}

比较用来排序的两个参数。根据第一个参数小于、等于或大于第二个参数分别返回负整数、零或正整数。

参数:

    o1 - 要比较的第一个对象。

    o2 - 要比较的第二个对象。

与compareTo方法一样。

如果按照描述信息进行比较排序,就直接定义一个实现Comparator接口的类:

 

class ItemComparator implements Comparator<Item>
{
	public int compare(Item a, Item b){
		String descrA = a.getDescription();
		String descrB = b.getDescription();
	    return descrA.compareTo(descrB);
	}
}

然后将这个类传递给树集的构造方法:

ItemComparator comp = new ItemComparator();
Set<Item> sortByDescription = new TreeSet<Item>(comp);

注意:实际上,Comparator<T>接口声明两个方法:compare和equals。当然,每一个类都有一个equals方法;因此,为这个接口声明再添加一个equals方法似乎没有太必要了。API文档解释说,不需要覆盖equas方法,但是这样做可能会在某些情况下提高性能。

现在集中的元素可以散列也可以排序,是否总是应该用树集取代散列集呢?毕竟,添加一个元素所花费的时间看上去并不是很长,而且元素是自动排序的。到底应该怎样做将取决于所要收集的数据。如果不需要对数据进行排序,就没有必要付出排序的开销。更重要的是,对某些数据来说,对其排序要比散列更加困难。散列只是将对象适当地打乱存放,而比较却要精确地判别每个对象。

注意:TreeSet类实现了NavigableSet接口。这个接口增加了几个便于定位元素以及反向遍历的方法。

下面程序中创建了两个Item对象树集,第一个按照部件编号排序,这是Item默认排序。第二个通过使用一个定制的比较器来按照描述信息排序。

import java.util.*;

/**
   This program sorts a set of item by comparing  their descriptions.
*/
public class TreeSetTest
{  
   public static void main(String[] args)
   {  
      SortedSet<Item> parts = new TreeSet<Item>();
      parts.add(new Item("Toaster", 1234));
      parts.add(new Item("Widget", 4562));
      parts.add(new Item("Modem", 9912));
      System.out.println(parts);

      SortedSet<Item> sortByDescription = new TreeSet<Item>(new
         Comparator<Item>()
         {  
            public int compare(Item a, Item b)
            {  
               String descrA = a.getDescription();
               String descrB = b.getDescription();
               return descrA.compareTo(descrB);
            }
         });

      sortByDescription.addAll(parts);
      System.out.println(sortByDescription);
   }
}

/**
   An item with a description and a part number.
*/
class Item implements Comparable<Item>
{ 
	 private String description;
	 private int partNumber;
   /**
      Constructs an item.
      @param aDescription the item's description
      @param aPartNumber the item's part number
   */
   public Item(String aDescription, int aPartNumber)
   {  
      description = aDescription;
      partNumber = aPartNumber;
   }

   /**
      Gets the description of this item.
      @return the description
   */
   public String getDescription()
   {  
      return description;
   }

   public String toString()
   {  
      return "[descripion=" + description + ", partNumber=" + partNumber + "]";
   }

   public boolean equals(Object otherObject)
   {  
      if (this == otherObject) return true;
      if (otherObject == null) return false;
      if (getClass() != otherObject.getClass()) return false;
      Item other = (Item) otherObject;
      return description.equals(other.description)&& partNumber == other.partNumber;
   }

   public int hashCode()
   {  
      return 13 * description.hashCode() + 17 * partNumber;
   }

   public int compareTo(Item other)
   {  
      return partNumber - other.partNumber;
   }
}

 

 

2.6 队列与双端队列

 

队列可以让我们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,叫双端队列,可以让我们有效的在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在JDK 6中引入了Deque接口,并由ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

API interface Queue<E> 5.0

boolean add(E e)

将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。

boolean offer(E e)

将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。

E remove()

获取并移除此队列的头。此方法与 poll 唯一的不同在于:此队列为空时将抛出一个NoSuchElementException异常。

E poll()

获取并移除此队列的头,如果此队列为空,则返回 null。

E element()

获取,但是不移除此队列的头。此方法与 peek 唯一的不同在于:此队列为空时将抛出一个NoSuchElementException异常。

E peek()

获取但不移除此队列的头;如果此队列为空,则返回 null。

 

Deque<E>

一个线性 collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。

此接口扩展了 Queue 接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue 接口继承的方法完全等效于 Deque 方法,如下表所示:

双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法,如下表所示:

 

注意,在将双端队列用作队列或堆栈时,peek方法同样正常工作;无论哪种情况下,都从双端队列的开头抽取元素。

此接口提供了两种移除内部元素的方法:removeFirstOccurrence removeLastOccurrence

List 接口不同,此接口不支持通过索引访问元素。

虽然Deque实现没有严格要求禁止插入null元素,但建议最好这样做。建议任何事实上允许null元素的Deque实现用户最好不要利用插入null的功能。这是因为各种方法会将null用作特殊的返回值来指示双端队列为空。

Deque实现通常不定义基于元素的equalshashCode方法,而是从 Object类继承基于身份的 equals hashCode方法。

 

 注意,在将双端队列用作队列或堆栈时,peek 方法同样正常工作;无论哪种情况下,都从双端队列的开头抽取元素。此接口提供了两种移除内部元素的方法:removeFirstOccurrence 和 removeLastOccurrence。与 List 接口不同,此接口不支持通过索引访问元素。虽然Deque实现没有严格要求禁止插入null元素,但建议最好这样做。建议任何事实上允许null元素的Deque实现用户最好不要利用插入null的功能。这是因为各种方法会将null用作特殊的返回值来指示双端队列为空。

Deque实现通常不定义基于元素的equals和hashCode方法,而是从 Object 类继承基于身份的 equals 和 hashCode 方法。 

 

2.7 优先级队列

 

优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet一样,一个优先级队列既可以保存了实现了Comparable接口的类对象,也可以保存在构造器中提供比较器的对象。

使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务随机顺序添加到队列中。当启动一个新的任务时,都将优先级最高的任务从队列中删除。

下面显示了一个正在运行优先级队列的示例。与TreeSet中迭代不同,这里的迭代并不是按照元素的排列顺序访问的。而删除却是删除掉剩余元素中优先级数最小的那个元素。

public class PriorityQueueTest
{
   public static void main(String[] args)
   {
      PriorityQueue<GregorianCalendar> pq = new PriorityQueue<GregorianCalendar>();
      pq.add(new GregorianCalendar(1906, Calendar.DECEMBER, 9)); // G. Hopper
      pq.add(new GregorianCalendar(1815, Calendar.DECEMBER, 10)); // A. Lovelace
      pq.add(new GregorianCalendar(1903, Calendar.DECEMBER, 3)); // J. von Neumann
      pq.add(new GregorianCalendar(1910, Calendar.JUNE, 22)); // K. Zuse

      System.out.println("Iterating over elements...");
      for (GregorianCalendar date : pq)
         System.out.println(date.get(Calendar.YEAR));
      System.out.println("Removing elements...");
      while (!pq.isEmpty())
         System.out.println(pq.remove().get(Calendar.YEAR));
   }
}

 

 

2.8 映射表

 

集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有查找元素的精确副本。这不是一种非常通用的查找方式,通常,我们知道某些键的信息,并想要查找与之对应的元素。映射表(map)数据结构就是为此设计的。映射表用来存放键/值对。如果提供了键,就能快速找到值。例如,有一张关于员工信息的记录表,键为员工ID,值为Employee对象。Java类库为映射表提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。

散列映射表(HashMap)对键进行散列,树映射表(TreeMap)的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较只能作用与键。与键关联的值不能进行散列或比较。选择使用HashMap还是TreeMap?与集一样,散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择HashMap。下面将为存储的员工信息建立一个散列映射表:

Map<String, Employee> staff = new HashMap<String, Employee>();
staff.put("144-25-5464", new Employee("Amy Lee"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));

每当往映射表中添加对象时,必须同时提供一个键。在这里,键是一个字符串,对应的值是Employee对象。想要检索一个对象,必须要提供一个键:

String s = “157-62-7935”;
Employee e = staff.get(s);

如果在映射表中没有与给定键对应的信息,get将返回null。

在映射表中,键必须是唯一的。不能对同一个键存放两个或多个值。如果对同一个键两次调用put方法,第二次的值就会取代第一次的值。实际上,put将返回用这个键上被覆盖的那个值。

集合框架并没有将映射表本身视为一个集合(其他的数据结构框架则将映射表视为对(pair)的集合,或者视为用键最为索引的值的集合)。然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或者它的子接口视图。

有3个视图,他们分别是:键集、值集合(不是集)和键/值对集。键与键/值对形成了一个集,这是因为在映射表中一个键只能有一个副本。下面方法将返回这3个视图:

Set<K> keySet(),
Collection<K> values(),
Set<Map.Entry<K,V>> entrySet()

(entrySet的元素是静态内部类Map.Entry的对象)

注意,keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个其他类的对象。Set接口扩展了Collection接口。因此,可以与使用任何集合一样使用keySet。例如,可以枚举映射表中的所有键:

Set<String> keys = map.keySet();
For(String key : keys){
	//do something with key 
}

如果想要同时查看键与值,就可以通过枚举个条目(entries)查看,以避免对值进行查找。可以使用下面代码框架:

for(Map.Entry<String,Employee> entry : staff.entrySet()){
	String key = entry.getKey();
	Employee value = entry.getValue();
	//do something with key,value
}

如果调用迭代器的remove方法,实际上就从映射表中删除了键以及对应的值。但是,不能将元素添加到键集的视图中。如果只添加键而不添加值是毫无意义的。如果视图调用add方法,将会抛出一个UnsupportedOperationException异常- 如果此 set 不支持 add 操作。条目集视图(entries)也同样有限制,不过,添加新的键/值对是有意义的。

下面程序示例显示了映射表的操作过程。

import java.util.*;

/**
 * This program demonstrates the use of a map with key type String and value type Employee.
 */
public class MapTest
{
   public static void main(String[] args)
   {
      Map<String, Employee> staff = new HashMap<String, Employee>();
      staff.put("144-25-5464", new Employee("Amy Lee"));
      staff.put("567-24-2546", new Employee("Harry Hacker"));
      staff.put("157-62-7935", new Employee("Gary Cooper"));
      staff.put("456-62-5527", new Employee("Francesca Cruz"));
      
      System.out.println(staff.put("144-25-5464", new Employee("Amy"))); //返回被覆盖的值

      // print all entries
     
      System.out.println(staff);

      // remove an entry

      staff.remove("567-24-2546");

      // replace an entry

      staff.put("456-62-5527", new Employee("Francesca Miller"));

      // look up a value

      System.out.println(staff.get("157-62-7935"));

      // iterate through all entries

      for (Map.Entry<String, Employee> entry : staff.entrySet())
      {
         String key = entry.getKey();
         Employee value = entry.getValue();
         System.out.println("key=" + key + ", value=" + value);
      }
   }
}

/**
 * A minimalist employee class for testing purposes.
 */
class Employee
{
   /**
    * Constructs an employee with $0 salary.
    * @param n the employee name
    */
   public Employee(String n)
   {
      name = n;
      salary = 0;
   }

   public String toString()
   {
      return "[name=" + name + ", salary=" + salary + "]";
   }

   private String name;
   private double salary;
}

 

API  interfaceMap<K,V>

Vget(Object key)

返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。

Vput(K key,V value)

将指定的值与此映射中的指定键关联。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值(当且仅当m.containsKey(k) 返回 true 时,才能说映射 m 包含键 k 的映射关系)。如果该实现支持 null 值,则返回 null 也可能表示此映射以前将 null 与 key 关联。

void putAll(Map<? extends K,? extends V> m)

从指定映射中将所有映射关系复制到此映射中。

boolean containsKey(Object key)

如果此映射包含指定键的映射关系,则返回 true。更确切地讲,当且仅当此映射包含针对满足 (key==null ? k==null : key.equals(k)) 的键 k 的映射关系时,返回 true。

boolean containsValue(Object value)

如果此映射将一个或多个键映射到指定值,则返回 true。更确切地讲,当且仅当此映射至少包含一个对满足 (value==null ? v==null : value.equals(v)) 的值 v 的映射关系时,返回 true。

Set<Map.Entry<K,V>>entrySet()

返回此映射中包含的映射关系的 Set 视图。该 set 受映射支持,所以对映射的更改可在此 set 中反映出来,反之亦然。如果对该 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作,或者通过对迭代器返回的映射项执行 setValue 操作除外),则迭代结果是不确定的。set 支持元素移除,通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作。

Set<K>keySet()

返回此映射中包含的键的 Set 视图。该 set 受映射支持,所以对映射的更改可在此 set 中反映出来,反之亦然。如果对该 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。set 支持元素移除,通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作。

Collection<V>values()

返回此映射中包含的值的 Collection 视图。该 collection 受映射支持,所以对映射的更改可在此 collection 中反映出来,反之亦然。如果对该 collection 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),则迭代结果是不确定的。collection 支持元素移除,通过 Iterator.remove、Collection.remove、removeAll、retainAll 和 clear 操作可从映射中移除相应的映射关系。它不支持 add 或 addAll 操作。

Map.Entry<K,V>

映射项(键-值对)。Map.entrySet 方法返回映射的 collection 视图,其中的元素属于此类。获得映射项引用的唯一方法是通过此 collection 视图的迭代器来实现。这些 Map.Entry 对象在迭代期间有效;更确切地讲,如果在迭代器返回项之后修改了底层映射,则某些映射项的行为是不确定的,除了通过 setValue 在映射项上执行操作之外。

KgetKey()

返回与此项对应的键。

VgetValue()

返回与此项对应的值。如果已经从底层映射中移除了映射关系(通过迭代器的 remove 操作),则此调用的结果是不确定的。

VsetValue(V value)

用指定的值替换与此项对应的值(可选操作)。(写入该映射。)

HashMap、TreeMap

public HashMap()

构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

public HashMap(Map<? extends K,? extends V> m)

构造一个映射关系与指定 Map 相同的新 HashMap。所创建的 HashMap 具有默认加载因子 (0.75) 和足以容纳指定 Map 中映射关系的初始容量。

public HashMap(int initialCapacity, float loadFactor)

构造一个带指定初始容量和加载因子的空 HashMap。

public HashMap(int initialCapacity)

构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

public TreeMap()

使用键的自然顺序构造一个新的、空的树映射。插入该映射的所有键都必须实现Comparable 接口。另外,所有这些键都必须是可互相比较的:对于映射中的任意两个键 k1 和 k2,执行 k1.compareTo(k2) 都不得抛出 ClassCastException。

public TreeMap(Comparator<? super K> comparator)

构造一个新的、空的树映射,该映射根据给定比较器进行排序。插入该映射的所有键都必须由给定比较器进行相互比较:对于映射中的任意两个键 k1 和 k2,执行 comparator.compare(k1, k2) 都不得抛出 ClassCastException。

public TreeMap(Map<? extends K,? extends V> m)

构造一个与给定映射具有相同映射关系的新的树映射,该映射根据其键的自然顺序进行排序。插入此新映射的所有键都必须实现Comparable 接口。另外,所有这些键都必须是可互相比较的:对于映射中的任意两个键 k1 和 k2,执行 k1.compareTo(k2) 都不得抛出 ClassCastException。此方法的运行时间为 n*log(n)。

public TreeMap(SortedMap<K,? extends V> m)

构造一个与指定有序映射具有相同映射关系和相同排序顺序的新的树映射。此方法是以线性时间运行的。

SortedMap

Comparator<? super K>comparator()

返回对此映射中的键进行排序的比较器;如果此映射使用键的自然顺序,则返回 null。

KfirstKey()

返回此映射中当前第一个(最低)键。

KlastKey()

返回映射中当前最后一个(最高)键。

 

 

2.9 专用集与映射表类

 

在集合类库中,有几个专用的映射表类,下面做一些简单的介绍:

①弱散列映射表(WeakHashMap

如果有一个值,对应的键已经不再使用了。假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所有,这个键/值对无法从映射表中删除。遗憾的是,垃圾回收器不能够删除它。垃圾回收器跟踪活动的对象。只要映射表对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此,需要由程序负责长期存活的映射表中删除那些无用的值。或者使用WeakHashMap完成这件事情。当键的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。

弱散列映射表的内部运行机制,WeakHashMap使用弱引用(weak reference)保存键。WeakReference对象将引用保存到另一个对象中,在这里,就是散列表键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有其他引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但是将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入列队意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应的条目。

public class WeakHashMapTest {

	public static void main(String[] args) {
		testWeakHashMapAPIs();
	}
	
	private static void testWeakHashMapAPIs(){
		//初始化3个弱键
		String s1 = new String("one");
		String s2 = new String("two");
		String s3 = new String("three");
		
		Map wmap = new WeakHashMap();
		//add
		wmap.put(s1, "s1");
		wmap.put(s2, "s2");
		wmap.put(s3, "s3");
		
		System.out.printf("\nwmap:%s\n",wmap);
		System.out.println("contains key two :"+wmap.containsKey("two"));
		System.out.println("contains key four :"+wmap.containsKey("s4"));
		System.out.println("contains value 0 :"+wmap.containsValue(new Integer(0)));
		//wmap.remove("three");
		System.out.printf("\nwmap:%s\n",wmap);
		/**测试WeakHashMap 的自动回收特性*/
		//将s1设置null,这意味着"弱键"s1再没有被其他对象所引用
		s1 = null;
		System.gc();
		
		//遍历WeakHashMap
		Iterator iter = wmap.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry)iter.next();
			System.out.printf("next : %s - %s\n", entry.getKey(),entry.getValue());
		}
		System.out.println("after gc WeakHashMap size :"+wmap.size());
	}
}

②链接散列集(LinkedHashSet)和链接映射表(LinkedHashMap)

LinkedHashSet和LinkedHashMap用来记住插入元素的顺序。这样可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。

链接映射表插入的处理:

Map<String, Employee> staff = new HashMap<String, Employee>();
staff.put("144-25-5464", new Employee("Amy Lee"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
staff.put(null, new Employee("Francesca Cruz"));

然后,staff.keySet().iterator()枚举键和staff.values().iterator()枚举值:

key=157-62-7935, value=[name=Gary Cooper, salary=0.0]
key=567-24-2546, value=[name=Harry Hacker, salary=0.0]
key=144-25-5464, value=[name=Harry Hacker, salary=0.0]
key=456-62-5527, value=[name=Francesca Cruz, salary=0.0]

链接散列映射表将用访问顺序,而不是插入顺序,对映射表条目进行迭代。每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置受到影响,而散列表中的桶不会受到影响。一个条目总位于与键散列码对应的桶中)。要想构造这样一个散列映射表,使用构造一个带指定初始容量和加载因子的空插入顺序 LinkedHashMap 实例。

public LinkedHashMap(int initialCapacity, float loadFactor)

访问顺序对于实现高缓存的“最近最少使用”原则十分重要。例如,可能希望将访问访问频率高的元素放在内存中,而访问频率低的元素从数据库中读取。当在表找不到元素项且表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉,这些是近期最少使用的几个元素。甚至可以让这一过程自动化。即构造一个LinkedHashMap的子类,然后覆盖下面这个方法:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest)

此重写允许映射增加到 100 个条目,然后每次添加新条目时删除最旧的条目,始终维持 100 个条目的稳定状态。

private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
}

另外,还可以对eldest条目进行评估,以此决定是否应该将它删除。例如,可以检查与这个条目一起存在的时间戳。

③枚举集(EnumSet)和枚举映射表(EnumMap

EnumSet是一个枚举型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1

EnumSet类没有公共的构造器。可以使用静态工厂方法构造这个集:

//构造EnumSet
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY,Weekday.FRIDAY);//可以使用Set接口的常用方法来修改
for (Iterator<Weekday> iterator = always.iterator(); iterator.hasNext();) {
	Weekday weekday =  iterator.next();
	System.out.println(weekday);
}

 

 

EnumMap 是一个键类型为枚举类型的映射表。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:

public EnumMap(Class<K> keyType)

创建一个具有指定键类型的空枚举映射。

参数:

keyType - 此枚举映射的键类型的 class 对象

EnumMap<Weekday, Employee> personInChang = new EnumMap<Weekday,Employee>(Weekday.class);

注意:在EnumMap的API文档中,将会看到E extends Enum<E>这样的类型参数。简单地说,它的意思是“E是一个枚举类型”。所有的枚举类型都扩展与泛型Enum类。例如,Weekday扩展与Enum<Weekday>。

④标志散列映射表(IdentityHashMap)
在这个IdentityHashMap类中,键的散列值不是用hashCode计算的,而是用System. identityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码时使用的方式。而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不是equals。也就是说,不同的键对象,即使内容相同,也被视为不同的对象。在实现对象遍历算法时,这个类非常有用,可以用来跟踪每个对象的遍历状况。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 






------- android培训java培训、期待与您交流! ----------


相关文章

    暂无相关文章
相关栏目:

用户点评