第17章 容器深入研究
第11章介绍了Java容器类库的概念和基本功能,本章将更深入地探索这个重要的类库。
在对容器有了更加完备的了解后,我们将学习散列机制是如何工作的,以及在使用散列容器时怎样编写hashCode()和equals()方法。还会学习为什么某些容器会有不同版本的实现,以及怎样在它们之间进行选择。本章最后将以对通用便利工具和特殊类的探索作为结束。
17.1 完整的容器分类法
Java SE5新添加了:
- Queue接口及其实现PriorityQueue和各种风格的BlockingQueue。
- ConsurrentMap接口及其实现ConsurrentHashMap,用于多线程机制。
- CopyOnWriteArrayList和CopyOnWriteArraySet,用于多线程机制。
- EnumSet和EnumMap,为使用Enum而设计的Set和Map的特殊实现。
- 在Collections类中的多个便利方法。
17.2 填充容器
与Arrays类似,Collections类也存在一些实用的静态方法:
class StringAddress {
private String s;
public StringAddress(String s) { this.s = s; }
public String toString() { return super.toString() + " " + s; }
}
public class FillingLists {
public static void main(String[] args) {
List<StringAddress> list = new ArrayList<StringAddress>(Collections.nCopies(4, new StringAddress("Hello")));
System.out.println(list);
Collections.fill(list, new StringAddress("World!"));
System.out.println(list);
}
}
上例使用了Collections的两个方法:
- Collections.nCopies(int n,T o):返回一个使用特定类型对象填充的指定大小的List。
- Collections.fill(List list, T obj):使用一个对象替换List中的所有元素。
在StringAddress.toString()中,调用了Object.toString()方法,产生该对象的散列码的无符号十六进制。从输出可以看出,所有的引用都指向了相同的对象。
17.2.1 一种Generator解决方案
所有的Collection子类型都有一个接收Collection对象的构造器,用所接收的Collection对象中的元素来填充新的容器。为了更加容易地创建测试数据,我们将Generator应用于CollectionData的构造器上:
public class CollectionData <T> extends ArrayList<T> {
public CollectionData(Generator<T> gen,int quantity) {
for (int i = 0; i < quantity; i++)
add(gen.next());
}
public static <T> CollectionData<T> list(Generator<T> gen,int quantity) {
return new CollectionData<T>(gen, quantity);
}
}
下面是初始化LinkedHashSet的一个示例:
class Goverment implements Generator<String> {
String[] foundation = ("strange women lying in ponds " +
"distrbuting swords is no basis for a system of " +
"government").split(" ");
private int index;
public String next() { return foundation[index++]; }
}
public class CollectionDataTest {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>(new CollectionData<String>(new Goverment(), 15));
System.out.println(set);
Set<String> set2 = new LinkedHashSet<String>();
set2.addAll(CollectionData.list(new Goverment(), 15));
System.out.println(set2);
}
}
可以发现,该Set中元素的打印顺序与它们的插入顺序相同,因为LinkedHashSet保持了插入顺序的链接列表。
下面是将第16章定义的随机生成器RandomGenerator应用到CollectionData的示例:
public class CollectionDataGeneration {
public static void main(String[] args) {
System.out.println(new ArrayList<String>(
new CollectionData<String>(new RandomGenerator.String(9), 10)));
System.out.println(new HashSet<Integer>(
new CollectionData<Integer>(new RandomGenerator.Integer(), 10)));
}
}
17.2.2 Map生成器
我们可以对Map使用相同的方式,但首先需要一个Pair类:
public class Pair<K,V> {
public final K key;
public final V value;
public Pair(K k,V v) {
key = k;
value = v;
}
}
这样,我们就可以调用Generator的next()方法产生一个键值对对象了。
现在,我们可以使用各种不同的Generator、Iterator和常量值的组合来填充Map初始化对象:
public class MapData<K,V> extends LinkedHashMap<K, V> {
public MapData(Generator<Pair<K, V>> gen, int quantity) {
for (int i = 0; i < quantity; i++) {
Pair<K, V> p = gen.next();
put(p.key, p.value);
}
}
public MapData(Generator<K> genK, Generator<V> genV,int quantity) {
for (int i = 0; i < quantity; i++)
put(genK.next(), genV.next());
}
public MapData(Generator<K> genK, V value,int quantity) {
for (int i = 0; i < quantity; i++)
put(genK.next(), value);
}
public MapData(Iterable<K> genK, Generator<V> genV) {
for (K key : genK)
put(key, genV.next());
}
public MapData(Iterable<K> genK, V value) {
for (K key : genK)
put(key, value);
}
public static <K,V> MapData<K,V> map(Generator<Pair<K, V>> gen , int quantity) {
return new MapData<K, V>(gen, quantity);
}
public static <K,V> MapData<K,V> map(Generator<K> genK, Generator<V> genV,int quantity) {
return new MapData<K, V>(genK, genV, quantity);
}
public static <K,V> MapData<K,V> map(Generator<K> genK, V value,int quantity) {
return new MapData<K, V>(genK, value, quantity);
}
public static <K,V> MapData<K,V> map(Iterable<K> genK, Generator<V> genV) {
return new MapData<K, V>(genK, genV);
}
public static <K,V> MapData<K,V> map(Iterable<K> genK, V value) {
return new MapData<K, V>(genK, value);
}
}
上例使用了多个重载的方法对Map进行填充:
- 使用单一的Generator< Pair< K,V >>产生一个键值对对象。
- 使用两个分离的Generator分别产生键、值。
- 使用一个Generator产生键,一个常量作为值。
- 使用一个Iterable中的元素作为键,并用Generator产生值。
- 使用一个Iterable中的元素作为键,并用常量作为值。
下面是一个使用MapData的示例:
class Letters implements Generator<Pair<Integer,String>>,Iterable<Integer> {
private int size = 9;
private int number = 1;
private char letter = 'A';
public Pair<Integer,String> next() {
return new Pair<Integer,String>(number++, ""+letter++);
}
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
public Integer next() { return number++; }
public boolean hasNext() { return number < size; }
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
public class MapDataTest {
public static void main(String[] args) {
System.out.println(MapData.map(new Letters(), 11));
System.out.println(MapData.map(new CountingGenerator.Character(),new RandomGenerator.String(3), 8));
System.out.println(MapData.map(new CountingGenerator.Character(), "Value", 6));
System.out.println(MapData.map(new Letters(), new RandomGenerator.String(3)));
System.out.println(MapData.map(new Letters(), "Pop"));
}
}
17.2.3 使用Abstract类
下面的示例通过继承java.uti.AbstractMap来定制只读型的Map:
public class Countries {
public static final String[][] DATA = {
{"CHINA","Beijing"},{"EGYPT","Cairo"},
{"TOGO","Lome"},{"TUNISIA","Tunis"},
{"LIBYA","Tripoli"},{"GHANA","Accra"}
};
private static class FlyweightMap extends AbstractMap<String, String> {
private static class Entry implements Map.Entry<String,String> {
int index;
Entry(int index) { this.index = index;}
public boolean equals(Object o) {
return DATA[index][0].equals(o);
}
public String getKey() { return DATA[index][0]; }
public String getValue() { return DATA[index][1]; }
public String setValue(String value) {
throw new UnsupportedOperationException();
}
public int hashCode() { return DATA[index][0].hashCode(); }
}
static class EntrySet extends AbstractSet<Map.Entry<String, String>> {
private int size ;
EntrySet(int size) {
if(size < 0)
this.size = 0;
else if(size > DATA.length)
this.size = DATA.length;
else
this.size = size;
}
public int size() { return this.size; }
private class Iter implements Iterator<Map.Entry<String, String>> {
private Entry entry = new Entry(-1);
public boolean hasNext() { return entry.index < size-1; }
public Map.Entry<String, String> next() {
entry.index++;
return entry;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<Map.Entry<String, String>> iterator() {
return new Iter();
}
}
private static Set<Map.Entry<String, String>> entries = new EntrySet(DATA.length);
public Set<Map.Entry<String, String>> entrySet() { return entries; }
}
static Map<String,String> select(final int size) {
return new FlyweightMap() {
public Set<Map.Entry<String, String>> entrySet() {
return new EntrySet(size);
}
};
}
static Map<String,String> map = new FlyweightMap();
public static Map<String,String> capitals() { return map; }
public static Map<String,String> capitals(int size) { return select(size); }
static List<String> names = new ArrayList<String>(map.keySet());
public static List<String> names() { return names; }
public static List<String> names(int size) { return new ArrayList<String>(select(size).keySet()); }
public static void main(String[] args) {
System.out.println(capitals(5));
System.out.println(names(5));
System.out.println(new HashMap<String,String>(capitals(3)));
}
}
下面是通过继承java.util.AbstractList来快速自定义一个List:
public class CountingIntegerList extends AbstractList<Integer>{
private int size;
public CountingIntegerList(int size) {
this.size = size < 0 ? 0 : size;
}
public Integer get(int index) { return Integer.valueOf(index); }
public int size() { return size; }
public static void main(String[] args) {
System.out.println(new CountingIntegerList(30));
}
}
下面是通过继承AbstractMap定制的具有任意尺寸的Map:
public class CountingMapData extends AbstractMap<Integer, String>{
private int size;
private static String[] chars = "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z".split(" ");
public CountingMapData(int size) {
if(size < 0) this.size = 0;
this.size = size;
}
private static class Entry implements Map.Entry<Integer, String> {
int index;
Entry(int index) { this.index = index; }
public boolean equals(Object o) {
return Integer.valueOf(index).equals(o);
}
public Integer getKey() { return index; }
public String getValue() { return chars[index % chars.length] + Integer.toString(index/chars.length); }
public String setValue(String value) {
throw new UnsupportedOperationException();
}
public int hashCode() { return Integer.valueOf(index).hashCode(); }
}
public Set<Map.Entry<Integer, String>> entrySet() {
Set<Map.Entry<Integer, String>> entries = new LinkedHashSet<Map.Entry<Integer,String>>();
for (int i = 0; i < size; i++)
entries.add(new Entry(i));
return entries;
}
public static void main(String[] args) {
System.out.println(new CountingMapData(60));
}
}
与之前不同的是,这里使用的是LinkedHashSet,而不是定制的Set。
17.3 Collection的功能方法
下面列出了可以通过Collection执行的所有操作:
- boolean add(T):将参数添加进容器。
- boolean addAll(Collection):将参数中的所有元素添加进容器。
- void clear():移除容器中的所有元素。
- boolean contains(T):如果容器持有该参数,返回true。
- Boolean containsAll(Collection):如果容器持有该参数中的所有元素,返回true。
- boolean isEmpty():容器中没有元素时,返回true。
- Iterator iterator():返回一个Iterator< T >,可用来遍历容器中的元素。
- Boolean remove(Object):如果参数在容器中,则移除此元素的一个实例。
- boolean removeAll(Collection):移除参数中的所有元素。
- boolean retainAll(Collection):容器中的元素与参数中的元素求交集。
- int size():返回容器中元素的数目。
- Object[] toArray():返回一个数组,该数组包含容器中的所有元素。
- T[] toArray(T[] a):返回一个与参数类型一致的数组,该数组包含容器中的所有元素。
17.4 可选操作
执行各种不同的添加和移除的方法在Collection接口中都是可选操作。这意味着实现类并不需要为这些方法提供功能定义。
这种方式可以将具体功能延迟到需要时才实现,为了让这种方式能够工作,Java提供了未获支持的操作(UnsupportedOperationException):该方式必须是一种罕见事件,即在大多数情况下,所有操作都应该能工作。并且,未获支持的操作只有在运行时才能探测到。
17.4.1 未获支持的操作
最常见的未获支持的操作,是来源于由固定尺寸的数据结构支持的容器。
public class Unsupported {
static void test(String msg,List<String> list) {
System.out.println("--- " + msg + " ---");
Collection<String> c = list;
Collection<String> subList = list.subList(1, 8);
Collection<String> c2 = new ArrayList<String>(subList);
try { c.retainAll(c2); } catch(Exception e) {
System.out.println("retainAll(): " + e);
}
try { c.removeAll(c2); } catch(Exception e) {
System.out.println("removeAll(): " + e);
}
try { c.clear(); } catch(Exception e) {
System.out.println("clear(): " + e);
}
try { c.add("X"); } catch(Exception e) {
System.out.println("add(): " + e);
}
try { c.remove("C"); } catch(Exception e) {
System.out.println("remove(): " + e);
}
try { list.set(0, "X"); } catch(Exception e) {
System.out.println("list.set(): " + e);
}
}
public static void main(String[] args) {
List<String> list = Arrays.asList("A B C D E F G H I J K L".split(" "));
test("Modifiable Copy", new ArrayList<String>(list));
test("Arrays.asList()", list);
test("unmodifiableList()", Collections.unmodifiableList(new ArrayList<String>(list)));
}
}
在main()方法中演示了三种情况:
-
Arrays.asList()生成的List是基于固定大小的数组,仅支持那些不会改变数组大小的操作。而那些试图改变底层数据结构尺寸的操作,都会产生一个未获支持的操作异常。
-
通过将上述list通过构造器传递给任意的Collection类型,都会产生新的尺寸可调的底层数据结构。
-
Collections.unmodifiableList()产生的是一个不可修改的列表。
由此可见,未获支持的操作主要用于支持各种容器的特性。
17.5 List的功能方法
通常情况下,我们对List的使用仅仅是添加、获取和遍历。
下面是List的一些常用方法:
- add(T):添加一个元素。
- add(int,T):在指定索引添加元素。
- addAll(Collection):添加集合中的所有元素。
- addAll(int,Collection):在指定索引处开始添加集合中的元素。
- contains(Object):是否持有指定元素。
- containsAll(Collection):是否持有参数集合中的所有元素。
- get(int):获取指定索引处的元素。
- remove(int):移除指定索引处的元素。
- remove(Object):移除指定元素。
- set(int,T):将指定索引的元素设置为参数中的元素。
- retainAll(Collection):与参数集合中的元素求交集。
- removeAll(Collection):移除集合中的所有元素。
17.6 Set和存储顺序
Set与Collection有完全一样的接口,该接口具有以下特点:
- 存入Set的每个元素都必须是唯一的。
- 加入Set的元素必须定义equals()方法以确保对象的唯一性。
- Set接口不维护元素的次序。
Set的不同实现之间,存储顺序都有所变化。并且,不同的Set实现不仅具有不同的行为,而且它们对于可以在特定的Set中放置的元素的类型也有不同的要求:
-
HashSet:为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()方法。
-
TreeSet:保持次序的Set,底层为树结构。使用它可以从Set中提取有序的次序。元素必须实现Comparable接口。
-
LinkedHashSet:具有HashSet的查询速度,且内部使用链表维护元素的顺序。在使用迭代器遍历时,会按照元素插入的次序显示。元素也必须定义hashCode()方法。
在大多数情况下,我们对Set进行的操作就是访问,因此HashSet是我们的首选。
下面的示例演示了几种Set的实现所持有元素必须定义的方法:
class SetType {
int i;
public SetType(int n) { i = n; }
public boolean equals(Object o) {
return o instanceof SetType && (i == ((SetType)o).i);
}
public String toString() { return Integer.toString(i); }
}
class HashType extends SetType {
public HashType(int n) { super(n); }
public int hashCode() { return i; }
}
class TreeType extends SetType implements Comparable<TreeType>{
public TreeType(int n) { super(n); }
public int compareTo(TreeType o) {
return (o.i < i ? -1 : (o.i == i ? 0 : 1));
}
}
public class TypesForSets {
static <T> Set<T> fill(Set<T> set, Class<T> type) {
try {
for (int i = 0; i < 10; i++)
set.add(type.getConstructor(int.class).newInstance(i));
} catch (Exception e) {
throw new RuntimeException(e);
}
return set;
}
static <T> void test(Set<T> set, Class<T> type) {
fill(set, type);
fill(set, type);
fill(set, type);
System.out.println(set);
}
public static void main(String[] args) {
test(new HashSet<HashType>(), HashType.class);
test(new LinkedHashSet<HashType>(), HashType.class);
test(new TreeSet<TreeType>(), TreeType.class);
}
}
17.6.1 SortedSet
SortedSet可以保证所持有元素的排序状态,其具有以下附加方法:
- Comparator comparator():返回当前Set使用的Comparator
- Object first():返回容器中的第一个元素
- Object last():返回容器中的最后一个元素
- SortedSet subSet(T,T):生成此Set的子集,范围从第一个参数到第二个参数。
- SortedSet headSet(T):生成此Set的子集,由小于参数的元素组成。
- SortedSet tail(T):生成此Set的子集,由大于或等于参数的元素组成。
下面是一个简单的示例:
public class SortedSetDemo {
public static void main(String[] args) {
SortedSet<String> sortedSet = new TreeSet<String>();
Collections.addAll(sortedSet, "one two three four five six seven eight".split(" "));
System.out.println(sortedSet);
String low = sortedSet.first();
String high = sortedSet.last();
System.out.println("low = " + low + " ,high = " + high);
Iterator<String> iterator = sortedSet.iterator();
for (int i = 0; i <= 6; i++) {
if(i == 3) low = iterator.next();
if(i == 6) high = iterator.next();
else iterator.next();
}
System.out.println("low = " + low + " ,high = " + high);
System.out.println(sortedSet.subSet(low, high));
System.out.println(sortedSet.headSet(high));
System.out.println(sortedSet.tailSet(low));
}
}
可以发现,SortedSet是按对象的比较函数对元素排序。
17.7 队列
除了并发应用,Queue在Java SE5中仅有两个实现:LinkedList和PriorityQueue,它们之间的差异在于排序行为。下面是涉及Queue实现的大部分操作的基本示例:
public class QueueBehavior {
private static int count = 10;
static <T> void test(Queue<T> queue,Generator<T> gen) {
for (int i = 0; i < count; i++)
queue.offer(gen.next());
while(queue.peek() != null)
System.out.print(queue.remove() + " ");
System.out.println();
}
static class Gen implements Generator<String> {
String[] s = ("one two three four five six seven eight nine ten".split(" "));
int i ;
public String next() { return s[i++]; }
}
public static void main(String[] args) {
test(new LinkedList<String>(), new Gen());
test(new PriorityQueue<String>(), new Gen());
test(new ArrayBlockingQueue<String>(count), new Gen());
test(new ConcurrentLinkedQueue<String>(), new Gen());
test(new LinkedBlockingQueue<String>(), new Gen());
test(new PriorityBlockingQueue<String>(), new Gen());
}
}
可以看到:除了优先级队列,Queue按照先进先出的规则对元素进行排列。
17.7.1 优先级队列
在第11章时,我们介绍过优先级队列PriorityQueue:默认的排序将使用对象在队列中的自然顺序,但是可以通过提供Comparator(比较器)来修改这个顺序。
下面的示例是通过将对象实现Comparable接口,根据自定义规则,进行排序:
public class ToDoList extends PriorityQueue<ToDoList.ToDoItem>{
static class ToDoItem implements Comparable<ToDoItem> {
private char primary;
private int secondary;
private String item;
public ToDoItem(char primary, int secondary, String item) {
this.primary = primary;
this.secondary = secondary;
this.item = item;
}
public int compareTo(ToDoItem arg) {
if(this.primary > arg.primary)
return 1;
if(this.primary == arg.primary)
if(this.secondary > arg.secondary)
return 1;
else if(this.secondary == arg.secondary)
return 0;
return -1;
}
public String toString() {
return Character.toString(primary) + secondary + ": " + item;
}
}
public void add(char primary, int secondary, String item) {
super.add(new ToDoItem(primary, secondary, item));
}
public static void main(String[] args) {
ToDoList toDoList = new ToDoList();
toDoList.add('C', 4, "Empty trash");
toDoList.add('A', 2, "Feed dog");
toDoList.add('B', 7, "Feed bird");
toDoList.add('C', 3, "Mow lawn");
toDoList.add('A', 1, "Water lawn");
toDoList.add('B', 1, "Feed cat");
while(!toDoList.isEmpty())
System.out.println(toDoList.remove());
}
}
17.7.2 双向队列
双向队列:可以在任何一端添加或移除元素。在LinkedList中包含支持双向队列的方法,但在Java标准类型中没有任何显式的用于双向队列的接口。因此我们无法像Queue那样将LinkedList向上转型,但可以使用代理的方式创建Deque类:
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() { return deque.getFirst(); }
public T getLast() { 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 methods as necessary
}
下面是对该类的简单测试:
public class DequeTest {
static void fillTest(Deque<Integer> deque) {
for (int i = 20; i < 27; i++)
deque.addFirst(i);
for (int i = 50; i < 55; i++)
deque.addLast(i);
}
public static void main(String[] args) {
Deque<Integer> di = new Deque<Integer>();
fillTest(di);
System.out.println(di);
while(di.size() != 0)
System.out.print(di.removeFirst() + " ");
System.out.println();
fillTest(di);
while(di.size() != 0)
System.out.print(di.removeLast() + " ");
}
}
在大多数情况下,我们不会同时对队列两段进行操作,因此双向队列并不常用。
17.8 理解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 key,V value) {
if(index >= pairs.length)
throw new ArrayIndexOutOfBoundsException();
pairs[index++] = new Object[]{ key, value };
}
public V get(K key) {
for (int i = 0; i < pairs.length; i++)
if(pairs[i][0].equals(key))
return (V) pairs[i][1];
return null;
}
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < pairs.length; i++) {
result.append(pairs[i][0].toString());
result.append(" : ");
result.append(pairs[i][1].toString());
if(i < index -1)
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
AssociativeArray<String, String> map = new AssociativeArray<String, String>(6);
map.put("sky", "blue");
map.put("grass", "green");
map.put("ocean", "dancing");
map.put("tree", "tall");
map.put("earth", "brown");
map.put("sun", "warm");
try {
map.put("extra", "object");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Too many objects!");
}
System.out.println(map);
System.out.println(map.get("ocean"));
}
}
关联数组中的基本方法是put()和get()。上例中的做法只具有说明性,效率是极差的。
17.8.1 性能
性能是映射表中的一个重要问题,当在get()中使用线性搜索时,执行速度会相当地慢。而HashMap则能提高速度,它通过使用散列码来取代对键的缓慢搜索。散列码:相对唯一的,用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。hashCode()是根类Object中的方法,即所有Java对象都能产生散列码。HashMap就是使用对象的hashCode()方法进行快速查询的,此方法能够显著提高性能。
下面是Map的基本实现:
-
HashMap:基于散列表的实现。插入和查询键值对的开销是固定的。
-
LinkedHashMap:类似HashMap,但其使用链表维护了内部次序。因此在迭代遍历时,键值对顺序保持为插入次序或最近最少使用的次序,因此迭代遍历速度优于HashMap。但在随机访问时,比HashMap稍慢一点。
-
TreeMap:基于红黑树的实现。查看键或键值对时,它们会被排序,次序由Comparable或Comparator决定。并且TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
-
WeakHashMap:弱键映射,允许释放映射所指向的对象。如果映射之外没有引用指向某个键,则此键可以被垃圾收集器回收。
-
ConcurrentHashMap:线程安全的Map,不涉及同步加锁。
-
IdentityHashMap:使用==代替equals()对键进行比较的散列映射。
对Map中使用的键具有以下要求:
- 任何键都必须具有一个equals()方法。
- 如果键被用于散列Map,那么它必须具有恰当的hashCode()方法。
- 如果键被用于TreeMap,那么它必须实现Comparable。
下面的示例展示了Map接口的基本操作:
public class Maps {
public static void printKeys(Map<Integer,String> map) {
System.out.print("Size = " + map.size() + ", ");
System.out.print("Keys: ");
System.out.println(map.keySet());
}
public static void test(Map<Integer,String> map) {
System.out.println(map.getClass().getSimpleName());
map.putAll(new CountingMapData(25));
map.putAll(new CountingMapData(25));
printKeys(map);
System.out.print("Values: ");
System.out.println(map.values());
}
public static void main(String[] args) {
test(new HashMap<Integer, String>());
test(new TreeMap<Integer, String>());
test(new LinkedHashMap<Integer, String>());
test(new IdentityHashMap<Integer, String>());
test(new ConcurrentHashMap<Integer, String>());
test(new WeakHashMap<Integer, String>());
}
}
17.8.2 SortedMap
SortedMap可以确保键处于排序状态,并且它具有一下额外的方法:
- Comparator comparator():返回当前Map使用Comparator,返回null表示自然排序。
- T firstKey():返回Map中的第一个键。
- T lastKey():返回Map中最后一个键。
- SortedMap headMap(K):生成键小于参数的Map子集。
- SortedMap tailMap(K):生成键大于或等于参数的Map子集。
- SortedMap subMap(K,K):生成范围由第一个参数(包含)到第二个参数(不包含)的Map子集。
下面的例子与SortedSetDemo相似,演示了TreeMap新增的功能:
public class SortedMapDemo {
public static void main(String[] args) {
TreeMap<Integer, String> sortedMap = new TreeMap<Integer, String>(new CountingMapData(10));
System.out.println(sortedMap);
Integer low = sortedMap.firstKey();
Integer high = sortedMap.lastKey();
System.out.println("low = " + low + " ,high = " + high);
Iterator<Integer> iterator = sortedMap.keySet().iterator();
for (int i = 0; i <= 6; i++) {
if(i == 3) low = iterator.next();
if(i == 6) high = iterator.next();
else iterator.next();
}
System.out.println("low = " + low + " ,high = " + high);
System.out.println(sortedMap.subMap(low, high));
System.out.println(sortedMap.headMap(high));
System.out.println(sortedMap.tailMap(low));
}
}
此处,键值对是按键的次序排列的。TreeMap中的次序是有意义的,从而才可以获取第一个和最后一个元素,并且可以提取Map的子集。
17.8.3 LinkedHashMap
为了提高速度,LinkedHashMap散列化所有元素,但在遍历键值对时,却又以元素的插入顺序返回键值对。此外,在其构造器中可以使之采用最近最少使用算法(LRU)。
下面的简单示例演示了LinkedHashMap的这两种特点:
public class LinkedHashMaoDemo {
public static void main(String[] args) {
LinkedHashMap<Integer, String> linkedMap = new LinkedHashMap<Integer, String>(new CountingMapData(9));
System.out.println(linkedMap);
linkedMap = new LinkedHashMap<Integer, String>(16, 0.75f, true);
linkedMap.putAll(new CountingMapData(9));
System.out.println(linkedMap);
for (int i = 0; i < 6; i++)
linkedMap.get(i);
System.out.println(linkedMap);
linkedMap.get(0);
System.out.println(linkedMap);
}
}
可以看到:键值对是以插入的顺序进行遍历的,甚至LRU算法的版本初始情况下,也是如此。但被访问过后,未访问的元素就会被移到队首,最近访问的元素就会被移到队尾。
17.9 散列与散列码
下面的HashMap通过自定义的类作为键:
public class Grounding {
protected int number;
public Grounding(int n) { this.number = n; }
public String toString() { return "Grounding #" + number; }
}
public class Prediction {
private static Random rand = new Random(66);
private boolean shadow = rand.nextDouble() > 0.5;
public String toString() {
if(shadow)
return "Six more weeks of Winter!";
return "Early Spring!";
}
}
public class SpringDetector {
public static <T extends Grounding> void detectSpring(Class<T> type) throws Exception {
Constructor<T> ghog = type.getConstructor(int.class);
Map<Grounding,Prediction> map = new HashMap<Grounding, Prediction>();
for (int i = 0; i < 10; i++)
map.put(ghog.newInstance(i), new Prediction());
System.out.println("map = " + map);
Grounding gh = ghog.newInstance(3);
System.out.println("Looking up prediction for " + gh);
if(map.containsKey(gh))
System.out.println(map.get(gh));
else
System.out.println("Key not found: " + gh);
}
public static void main(String[] args) throws Exception {
detectSpring(Grounding.class);
}
}
可以发现,即使我们没有对作为键的类进行特殊处理,HashMap仍能正常工作。其原因是该类会继承根基类Object的hashCode()方法,使用对象的地址计算散列码。并且,当HashMap查询时,会使用equlas()来判断键值是否相等,此处也是继承了Object的equals()方法,比较对象的地址值是否相等。
正确的equals()方法必须满足下列5个条件:
- 自反性:对任意x,x.equals(x)一定返回true。
- 对称性:对任意x和y,x.equals(y)和y.equals(x)返回结果一致。
- 传递性:对任意x、y和z,如果x.equals(y)、y.equals(z)的结果都为true,则x.equals(z)的结果也为true。
- 一致性:对任意x和y,如果对象中等价比较信息没有改变,则无论调用多少次,其结果应该保持一致。
- 对任何不是null的x,x.equals(null)的结果一定是false。
下面,我们将上例中的类通过重写hashCode()和equals()方法进行重构:
public class Grounding2 extends Grounding{
public Grounding2(int n) { super(n); }
public int hashCode() { return number; }
public boolean equals(Object o) {
return o instanceof Grounding2 &&
(number == ((Grounding2)o).number);
}
}
public class SpringDetector2 {
public static void main(String[] args) throws Exception {
SpringDetector.detectSpring(Grounding2.class);
}
}
此时,我们将该类的标识数字作为散列码,并将该数字作为对象是否一致的标志。当判断键相同时,则会发生覆盖效果。
17.9.1 理解hashCode()
下面的例子用一对ArrayList实现了一个Map:
public class MapEntry<K,V> implements Map.Entry<K, V>{
private K key;
private V value;
public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
public V setValue(V v) {
V result = value;
value = v;
return result;
}
public int hashCode() {
return (key == null ? 0 : key.hashCode())^
(value == null ? 0 : value.hashCode());
}
public boolean equals(Object o) {
if(!(o instanceof MapEntry)) return false;
MapEntry me = (MapEntry) o;
return (key == null ? me.getKey() == null : key.equals(me.getKey())) &&
(value == null ? me.getValue() == null : value.equals(me.getValue()));
}
public String toString() { return key + "=" + value; }
}
public class SlowMap <K,V> extends AbstractMap<K, V> {
private List<K> keys = new ArrayList<K>();
private List<V> values = new ArrayList<V>();
public V put(K key , V value) {
V oldValue = get(key);
if(!keys.contains(key)){
keys.add(key);
values.add(value);
}else
values.set(keys.indexOf(key), value);
return oldValue;
}
public V get(Object key) {
if(!keys.contains(key))
return null;
return values.get(keys.indexOf(key));
}
public Set<Map.Entry<K, V>> entrySet() {
Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K,V>>();
Iterator<K> ki = keys.iterator();
Iterator<V> vi = values.iterator();
while(ki.hasNext())
set.add(new MapEntry<K, V>(ki.next(),vi.next()));
return set;
}
public static void main(String[] args) {
SlowMap<String,String> m = new SlowMap<String, String>();
m.putAll(Countries.capitals(5));
System.out.println(m);
System.out.println(m.get("CHINA"));
System.out.println(m.entrySet());
}
}
17.9.2 为速度而散列
SlowMap.java中键没有按照任何特定顺序保存,对键的差只能使用简单的线性查询,而线性查询是最慢的查询方式。
散列的价值则在于提高了查询速度:我们都知道存储元素最快的是数组,所以可以通过一个序列数组来负责存储Map.Entry对象。而数组的下标就是散列码,散列码一般是对键对象进行操作而得到的整型值,散列码相同的对象将被存储在同一个序列中。 下面的例子使用LinkedList作为存储Entry的序列:
public class SimpleHashMap <K,V> extends AbstractMap<K, V> {
static final int SIZE = 997;
LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE];
public V put(K key,V value) {
V oldValue = null;
int index = Math.abs(key.hashCode()) % SIZE;
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()) % SIZE;
if(buckets[index] == null) return null;
for (MapEntry<K, V> iPair : buckets[index])
if(iPair.getKey().equals(key))
return iPair.getValue();
return null;
}
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> mapEntry : bucket)
set.add(mapEntry);
}
return set;
}
public static void main(String[] args) {
SimpleHashMap<String, String> map = new SimpleHashMap<String, String>();
map.putAll(Countries.capitals(5));
System.out.println(map);
System.out.println(map.get("CHINA"));
}
}
可以发现,使用散列码使得我们很好地将对象分离开来,并且,散列码重复较少的情况下,我们在查询时将不必进行线性遍历,能够迅速查询到结果。
该实现只是简单地展示散列映射表执行的各种操作,java类库中的HashMap的实现,则是一个跳优后的实现。
17.9.3 覆盖hashCode()
设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。而直接使用具有唯一性的对象信息计算散列码,则会使得散列码变得唯一,从而使得对象过于松散。正确的做法是:使用对象内有意义的识别信息。并且好的hashCode()应该产生分布均匀的散列码。
下面演示了String的散列码计算情况:
public class StringHashCode {
public static void main(String[] args) {
String[] hellos = "Hello Hello".split(" ");
System.out.println(hellos[0].hashCode());
System.out.println(hellos[1].hashCode());
}
}
可以看出,String类中的hashCode()的计算是基于String内容的。
下面是一个散列码计算的示例:
public class CountedString {
private static List<String> created = new ArrayList<String>();
private String s;
private int id = 0;
public CountedString(String str) {
s = str;
created.add(s);;
for (String s2 : created)
if(s2.equals(s))
id++;
}
public String toString() {
return "String: " + s + " id: " + id + " hashCode(): " + hashCode();
}
public int hashCode() {
int result = 17;
result = 37 * result + s.hashCode();
result = 37 * result + id;
return result;
}
public boolean equals(Object o) {
return o instanceof CountedString &&
s.equals(((CountedString)o).s) &&
id == ((CountedString)o).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));
}
}
}
下面是在第14章中所定义的typeinfo.pet类库中的基类Individual类的定义:
public class Individual implements Comparable<Individual> {
private static long counter = 0;
private final long id = counter++;
private String name;
public Individual(String name) { this.name = name; }
public Individual() {}
public String toString() {
return getClass().getSimpleName() + (name == null ? "" : " " + name);
}
public long id() { return id; }
public boolean equals(Object o) {
return o instanceof Individual && id == ((Individual) o).id;
}
public int hashCode() {
int result = 17;
if (name != null) {
result = 37 * result + name.hashCode();
}
result = 37 * result + (int) id;
return result;
}
public int compareTo(Individual arg) {
String first = getClass().getSimpleName();
String argFirst = arg.getClass().getSimpleName();
int firstCompare = first.compareTo(argFirst);
if (firstCompare != 0) {
return firstCompare;
}
if (name != null && arg.name != null) {
int secondCompare = name.compareTo(arg.name);
if (secondCompare != 0) {
return secondCompare;
}
}
return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
}
}
17.10 选择接口的不同实现
在Java中,实际上只有四种容器:Map、List、Set和Queue。但每种不同的实现都有各自的特征、优点和缺点。
在Java类库中:
- Hashtable、Vector和Stack是过去遗留下来的类,存在的目的只是为了支持老的程序。
- 不同类型的Queue只在它们接收和产生数值的方式上有所差异。
各种接口之间的实现的区别通常归结于底层数据结构的不同。
对于List接口的实现ArrayList和LinkedList,基本的List操作都相同:
- ArrayList:底层由数组支持,擅长于随机访问元素,但是在List的中间插入和移除元素时较慢。
- LinkedList:由双向链表实现,插入和删除操作代价较低,并提供了优化的顺序访问。但在随机访问方面相对较慢。
对于Set接口的实现TreeSet、HashSet和LinkedHashSet:
- TreeSet:将元素存储在红-黑树数据结构中。
- HashSet:输出顺序没有规律可寻,出于速度考虑,使用了散列函数。
- LinkedHashSet:因为查询速度的原因也使用了散列,但看起来它使用了链表来维护元素的插入顺序。
当我们需要使用某个特定容器的不同实现所共有的操作时,我们当然希望选择性能更高的容器。此时,我们可以通过性能测试对容器的各个实现之间进行对比。
17.10.1 性能测试框架
为了更方面地进行测试,我们将测试过程中的基本功能放置到了一个测试框架中。该测试框架主要由三个部分组成:
- 抽象类Test< C >:提供测试接口并存储该项测试的名字。
- 测试参数类TestParam:保存用于该项测试的参数。
- 测试核心功能类Tester:具有初始化容器、打印信息和计算测试时间的功能。
下面是一个抽象类Test< C >:
public abstract class Test<C> {
String name;
public Test(String name) { this.name = name; }
abstract int test(C container, TestParam p);
}
抽象方法test()中两个参数分别为:所测试的容器和各项参数。
TestParam用于存储测试的各项参数:
public class TestParam {
public final int size;
public final int loops;
public TestParam(int size,int loops) {
this.size = size;
this.loops = loops;
}
public static TestParam[] array(int...values) {
int size = values.length/2;
TestParam[] result = new TestParam[size];
int n = 0;
for (int i = 0; i < size; i++)
result[i] = new TestParam(values[n++], values[n++]);
return result;
}
public static TestParam[] array(String[] values) {
int[] vals = new int[values.length];
for (int i = 0; i < vals.length; i++)
vals[i] = Integer.decode(values[i]);
return array(vals);
}
}
该类主要包含两种参数:
- size:容器中元素数量。
- loops:该项测试迭代的次数。
下面是具有核心的测试功能的类Tester:
public class Tester<C> {
public static int fieldWidth = 0;
public static TestParam[] defaultParams = TestParam.array(
10, 5000, 100, 5000, 1000, 5000, 10000, 500 );
protected C initialize(int size) { return container; }
protected C container;
private String headline = "";
private List<Test<C>> tests;
private static String stringField() {
return "%" + fieldWidth + "s";
}
private static String numberField() {
return "%" + fieldWidth + "d";
}
private static int sizeWidth = 5;
private static String sizeField = "%" + sizeWidth + "s";
private TestParam[] paramList = defaultParams;
public Tester(C container, List<Test<C>> tests) {
this.container = container;
this.tests = tests;
if(container != null )
headline = container.getClass().getSimpleName();
}
public Tester(C container, List<Test<C>> tests,TestParam[] paramList) {
this(container, tests);
this.paramList = paramList;
}
public void setHeadline(String newHeadline) {
headline = newHeadline;
}
public static <C> void run(C cntnr, List<Test<C>> tests) {
new Tester<C>(cntnr, tests);
}
public static <C> void run(C cntnr, List<Test<C>> tests,TestParam[] paramList) {
new Tester<C>(cntnr, tests, paramList);
}
private void displayHeader() {
int width = fieldWidth * tests.size() + sizeWidth;
int dashLength = width - headline.length() -1;
StringBuilder head = new StringBuilder(width);
for (int i = 0; i < dashLength/2; i++)
head.append('-');
head.append(' ');
head.append(headline);
head.append(' ');
for (int i = 0; i < dashLength/2; i++)
head.append('-');
System.out.println(head);
System.out.format(sizeField, "size");
for (Test test : tests)
System.out.format(stringField(), test.name);
System.out.println();
}
public void timedTest() {
displayHeader();
for (TestParam param : paramList) {
System.out.format(sizeField, param.size);
for (Test<C> test : tests) {
C kontainer = initialize(param.size);
long start = System.nanoTime();
int reps = test.test(kontainer, param);
long duration = System.nanoTime() - start;
long timePerRep = duration;
System.out.format(numberField(), timePerRep);
}
System.out.println();
}
}
}
该类中有几个重要参数:
- C container:所要测试的容器。
- List< Test< C>> tests:对该容器所要测试项目的集合。如:add、get、set...
- TestParam[] paramList:测试参数类数组,测试时会遍历该数组对每一项都进行测试。
17.10.2 对List的选择
下面是对List操作中最本质的部分进行性能测试:
public class ListPerformance {
static Random rand = new Random();
static int reps = 1000;
static List<Test<List<Integer>>> tests = new ArrayList<Test<List<Integer>>>();
static {
tests.add(new Test<List<Integer>>("add") {
int test(List<Integer> list, TestParam p) {
int loops = p.loops;
int listSize = p.size;
for (int i = 0; i < loops; i++) {
list.clear();
for (int j = 0; j < listSize; j++)
list.add(j);
}
return loops * listSize;
}
});
tests.add(new Test<List<Integer>>("get") {
int test(List<Integer> list, TestParam p) {
int loops = p.loops * reps;
int listSize = p.size;
for (int i = 0; i < loops; i++)
list.get(rand.nextInt(listSize));
return loops;
}
});
tests.add(new Test<List<Integer>>("set") {
int test(List<Integer> list, TestParam p) {
int loops = p.loops * reps;
int listSize = p.size;
for (int i = 0; i < loops; i++)
list.set(rand.nextInt(listSize),66);
return loops;
}
});
}
static class ListTester extends Tester<List<Integer>> {
public ListTester(List<Integer> container, List<Test<List<Integer>>> tests) {
super(container, tests);
}
protected List<Integer> initialize(int size) {
container.clear();
container.addAll(new CountingIntegerList(size));
return container;
}
public static void run(List<Integer> list,List<Test<List<Integer>>> tests) {
new ListTester(list, tests).timedTest();
}
}
public static void main(String[] args) {
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
Tester<List<Integer>> arrayTest = new Tester<List<Integer>>(null,tests.subList(1, 3)){
protected List<Integer> initialize(int size) {
Integer[] ia = Generated.array(Integer.class, new CountingGenerator.Integer(),size);
return Arrays.asList(ia);
}
};
arrayTest.setHeadline("Array as List");
arrayTest.timedTest();
Tester.defaultParams = TestParam.array(
10, 5000, 100, 5000, 1000, 1000, 10000, 200);
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
ListTester.run(new ArrayList<Integer>(), tests);
ListTester.run(new LinkedList<Integer>(), tests);
}
}
17.10.3 对Set的选择
下面的测试说明了TreeSet、HashSet和LinkedHashSet的性能表现:
public class SetPerformance {
static List<Test<Set<Integer>>> tests = new ArrayList<Test<Set<Integer>>>();
static {
tests.add(new Test<Set<Integer>>("add") {
int test(Set<Integer> set, TestParam p) {
int loops = p.loops;
int size = p.size;
for (int i = 0; i < loops; i++) {
set.clear();
for (int j = 0; j < size; j++)
set.add(j);
}
return loops * size;
}
});
tests.add(new Test<Set<Integer>>("contains") {
int test(Set<Integer> set, TestParam p) {
int loops = p.loops;
int span = p.size * 2;
for (int i = 0; i < loops; i++)
for (int j = 0; j < span; j++)
set.contains(j);
return loops * span;
}
});
tests.add(new Test<Set<Integer>>("iterate") {
int test(Set<Integer> set, TestParam p) {
int loops = p.loops * 10;
for (int i = 0; i < loops; i++) {
Iterator<Integer> it = set.iterator();
while(it.hasNext())
it.next();
}
return loops * set.size();
}
});
}
public static void main(String[] args) {
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
Tester.fieldWidth = 10;
Tester.run(new TreeSet<Integer>(), tests);
Tester.run(new HashSet<Integer>(), tests);
Tester.run(new LinkedHashSet<Integer>(), tests);
}
}
HashSet在添加和查询元素时,性能比TreeSet好,TreeSet存在的唯一原因是它可以维持元素的排序状态,因此在迭代时速度较快。
17.10.4 对Map的选择
下面的程序展示了Map的不同实现在性能方面的开销:
public class MapPerformance {
static List<Test<Map<Integer,Integer>>> tests = new ArrayList<Test<Map<Integer,Integer>>>();
static {
tests.add(new Test<Map<Integer,Integer>>("put") {
int test(Map<Integer,Integer> map, TestParam p) {
int loops = p.loops;
int size = p.size;
for (int i = 0; i < loops; i++) {
map.clear();
for (int j = 0; j < size; j++)
map.put(j,j);
}
return loops * size;
}
});
tests.add(new Test<Map<Integer,Integer>>("get") {
int test(Map<Integer,Integer> map, TestParam p) {
int loops = p.loops;
int span = p.size * 2;
for (int i = 0; i < loops; i++)
for (int j = 0; j < span; j++)
map.get(j);
return loops * span;
}
});
tests.add(new Test<Map<Integer,Integer>>("iterate") {
int test(Map<Integer,Integer> map, TestParam p) {
int loops = p.loops * 10;
for (int i = 0; i < loops; i++) {
Iterator it = map.entrySet().iterator();
while(it.hasNext())
it.next();
}
return loops * map.size();
}
});
}
public static void main(String[] args) {
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
Tester.fieldWidth = 10;
Tester.run(new TreeMap<Integer,Integer>(), tests);
Tester.run(new HashMap<Integer,Integer>(), tests);
Tester.run(new LinkedHashMap<Integer,Integer>(), tests);
Tester.run(new IdentityHashMap<Integer,Integer>(), tests);
Tester.run(new WeakHashMap<Integer,Integer>(), tests);
Tester.run(new Hashtable<Integer,Integer>(), tests);
}
}
HashMap的性能因子
下面是关于Map的一些术语:
- 容量:表中的桶位数。
- 初始容量:表在创建时所拥有的桶位数。
- 尺寸:表中当前存储的项数。
- 负载因子:尺寸/容量。
HashMap使用的默认负载因子是0.75,即当表达到四分之三慢时,会进行再散列。
17.11 实用方法
Java中有大量用于容器的方法,它们被表示为java.util.Collections类内部的静态方法:
-
checkedCollection(Collection,Class):第一个参数是希望动态检查的容器,第二个参数是该容器强制要求的类型。受检查的容器会在试图插入类型不正确的对象时抛出ClassCastException。
-
max(Collection)、min(Collection):返回Collection中最大或最小的元素,采用Collection内置的自然比较法
-
max(Collection,Comparator)、min(Collection,Comparator):返回Collection中最大或最小的元素,采用Comparator进行比较
17.11.1 List的排序和查询
List的排序与查询所使用的方法与对象数组所使用的方法相似:
- Collections.shuffle(List):将List随机打乱。
- Collections.sort(List):按照List元素的默认排序方式排序。
- Collections.sort(List,Comparator):按照指定Comparator进行排序。
- Collections.binarySearch():对排序后的序列进行查询。
17.11.2 设定Collection或Map为不可修改
下例说明如何正确生成各种只读容器:
public class ReadOnly {
static Collection<String> data = new ArrayList<String>(Countries.names(5));
public static void main(String[] args) {
Collection<String> c = Collections.unmodifiableCollection(data);
System.out.println(c);
List<String> a = Collections.unmodifiableList(new ArrayList<String>(data));
ListIterator<String> lit = a.listIterator();
System.out.println(lit.next());
Map<String,String> map = Collections.unmodifiableMap(new HashMap<String, String>(Countries.capitals(5)));
System.out.println(map);
}
}
Collections.unmodifiableCollection()系列的方法,均可以将容器设定为可读,并且如果对可读容器进行修改操作,则会引起UnsupportedOperationException异常。
17.11.3 Collection或Map的同步控制
Collections.synchronizedCollection()系列方法能够自动同步整个容器,其语法与生成只读容器的语法相似:
public class Synchronization {
public static void main(String[] args) {
Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>());
Map<String,String> map = Collections.synchronizedMap(new HashMap<String,String>());
}
}
快速报错
Java容器有一种保护机制:快速报错。在迭代遍历某个容器的过程中,不允许对容器的任何修改,一旦发现容器被修改,则会抛出ConcurrentModificationException异常:
public class FailFast {
public static void main(String[] args) {
Collection<String> c = new ArrayList<String>();
Iterator<String> it = c.iterator();
c.add("An object");
try {
String s = it.next();
} catch (ConcurrentModificationException e) {
System.out.println(e);
}
}
}
17.12 持有引用
class VeryBig {
private static final int SIZE = 1000;
private long[] la = new long[SIZE];
private String ident;
public VeryBig(String id) { ident = id; }
public String toString() { return ident; }
protected void finalize() {
System.out.println("Finalizing " + ident);
}
}
public class References {
private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();
public static void checkQueue() {
Reference<? extends VeryBig> inq = rq.poll();
if(inq != null)
System.out.println("In queue: " + inq.get() );
}
public static void main(String[] args) {
int size = 10;
if(args.length > 0)
size = new Integer(args[0]);
LinkedList<SoftReference<VeryBig>> sa = new LinkedList<SoftReference<VeryBig>>();
for (int i = 0; i < size ; i++) {
sa.add(new SoftReference<VeryBig>(new VeryBig("Soft " + i),rq));
System.out.println("Just created: " + sa.getLast());
checkQueue();
}
LinkedList<WeakReference<VeryBig>> wa = new LinkedList<WeakReference<VeryBig>>();
for (int i = 0; i < size ; i++) {
wa.add(new WeakReference<VeryBig>(new VeryBig("Weak " + i),rq));
System.out.println("Just created: " + wa.getLast());
checkQueue();
}
SoftReference<VeryBig> s = new SoftReference<VeryBig>(new VeryBig("Soft"));
WeakReference<VeryBig> w = new WeakReference<VeryBig>(new VeryBig("Weak"));
System.gc();
LinkedList<PhantomReference<VeryBig>> pa = new LinkedList<PhantomReference<VeryBig>>();
for (int i = 0; i < size ; i++) {
pa.add(new PhantomReference<VeryBig>(new VeryBig("Phantom " + i),rq));
System.out.println("Just created: " + pa.getLast());
checkQueue();
}
}
}
17.12.1 WeakHashMap
17.13 Java1.0/1.1容器
17.13.1 Vector和Enumeration
17.13.2 Hashtable
17.13.3 Stack
17.13.4 BitSet
17.14 总结
容器类库对于面向对象语言来说是最重要的类库,本章更详细地介绍了容器的类库。