在上一讲中介绍了散列映射表HashMap和树映射表TreeMap,知道了HashMap的底层实现机制。这一讲将介绍Set接口和实现类:HashSet和TreeSet。由于HashSet的实现是基于HashMap的,TreeSet的实现是基于TreeMap的,所以这里不做过多底层的讨论,毕竟这部分已经在Java集合(四):Map映射中讨论过了。
1 散列表与Set接口
链表和数组可以按照人们的意愿排列元素的顺序。但是,如果想要查看某个指定的元素,但却忘了它的位置,就需要访问所有的元素,直到找到为止。如果集合中的元素很多,将会消耗很长时间。如果不在意元素的顺序,可以有几种能够快速查找元素的数据结构。但是缺点是不能控制元素的顺序。它们将按照有利于其操作目的的原则组织数据。
有一种常见的数据结构,就是散列表(hash table)。散列表可以根据每个对象计算一个整数,称为散列码(hash code)。不同的对象产生不同的散列码。
在Java中,散列表用链表数组实现。每个列表称为桶,这个已经在Map映射中介绍了。
散列表的特点就是:元素没有顺序,元素不能重复。
Set接口的定义如下:
public interface java.util.Set<E> extends java.util.Collection<E> { public abstract int size(); public abstract boolean isEmpty(); public abstract boolean contains(java.lang.Object); public abstract java.util.Iterator<E> iterator(); public abstract java.lang.Object[] toArray(); public abstract <T> T[] toArray(T[]); public abstract boolean add(E); public abstract boolean remove(java.lang.Object); public abstract boolean containsAll(java.util.Collection<?>); public abstract boolean addAll(java.util.Collection<? extends E>); public abstract boolean retainAll(java.util.Collection<?>); public abstract boolean removeAll(java.util.Collection<?>); public abstract void clear(); public abstract boolean equals(java.lang.Object); public abstract int hashCode(); public java.util.Spliterator<E> spliterator(); }这些方法的含义也很简单。
2 HashSet类
Java集合类库中提供了HashSet类实现了Set接口。这个类的底层是使用HashMap实现的:
private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
我们知道,HashMap是一个键值对,而HashSet存储的不是键值对,因此为了使用HashMap存储HashSet的元素,就需要构造一个键值对。可以使用需要存储在HashSet中的元素作为键,而上面的PRESENT作为值,就构成了一个键值对,这样就可以存在HashMap中了。
也就是说,HashSet与HashMap的原理一样,不同的是HashSet的值都一样,都是PRESENT。
由于在前一节中已经详细介绍了HashMap的原理,这里不再叙述了。只说一下HashSet的使用。
下面的代码从System.in中读取单词,然后将它们添加到HashSet中,再打印出所有的单词。由于HashSet中不存储相同的元素,所以打印出来的单词是不重复的。运行这个程序时使用下面的命令行:
java SetTest < alice.txt
这样就把alice.txt作为输入,程序就会读取所有的单词。代码如下:
import java.util.*; public class SetTest { public static void main(String[] args) { Set<String> words = new HashSet<>(); // 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 <= 20 && iter.hasNext(); i++) System.out.println(iter.next()); System.out.println(". . ."); System.out.println(words.size() + " distinct words. " + totalTime + " milliseconds."); } }
结果如下:
可以看见,一共有5392个不同的单词。
3 TreeSet类
TreeSet和HashSet类似,不过,它比HashSet有所改进。TreeSet是一个有序集合,可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动按照排序后的顺序呈现。例如,假设插入三个字符串,然后访问添加的所有元素:
SortedSet<String> sorter=new TreeSet<>(); sorter.add("B"); sorter.add("A"); sorter.add("C"); for(String s:sorter)System.out.println(s);
结果是:A B C
TreeSet的底层是使用TreeMap实现的,是一个红黑树。每次添加一个元素到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
将一个元素添加到树中要比添加到一个散列表中要慢,但是,与将元素添加到数组中或链表中要快。如果树中一共有n个元素,将元素插入到正确位置的时间为logn。
与TreeMap一样,构造一个TreeSet也需要一个比较器,可以使用默认的比较器,也可以使用自己的比较器。使用自己的比较器时,需要给TreeSet的构造器传递一个Comparator对象。
下面的程序创建了两个Item对象的树集。第一个按照部件编号排序,这是Item对象的默认顺序。第二个通过使用一个定制的比较器来按照描述信息排序:
import java.util.*; public class TreeSetTest { public static void main(String[] args) { SortedSet<Item> parts = new TreeSet<>(); 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<>(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); } }
import java.util.*; /** * An item with a description and a part number. */ public 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 "[\n\tdescripion=" + description + ",\n\tpartNumber=" + partNumber +"\n]\n"; } 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 Objects.equals(description, other.description) && partNumber == other.partNumber; } public int hashCode() { return Objects.hash(description, partNumber); } public int compareTo(Item other) { return Integer.compare(partNumber, other.partNumber); } }
结果如下: