Map:
对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用==而不是equals()方法来判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。
Collections
public class SynchronizedTest{
public static void main(String[] args){
//下面程序创建了4个同步的集合对象
Collection c=Collections.synchronizedCollection(new ArrayList());
List list=Collections.synchronizedList(new ArrayList());
Set s=Collections.synchronizedSet(new HashSet());
Map m=Collections.synchronizedMap(new HashMap());
}
}
实现一个Map 这个Map是说明性的,缺乏效率,并且具有固定的尺寸不灵活:
//: containers/AssociativeArray.java
// Associates keys with values.
import static net.mindview.util.Print.*;
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 };
}
@SuppressWarnings("unchecked")
public V get(K key) {
for(int i = 0; i < index; i++)
if(key.equals(pairs[i][0]))
return (V)pairs[i][1];
return null; // Did not find key
}
public String toString() {
StringBuilder result = new StringBuilder();
for(int i = 0; i < index; 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"); // Past the end
} catch(ArrayIndexOutOfBoundsException e) {
print("Too many objects!");
}
print(map);
print(map.get("ocean"));
}
} /* Output:
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing
*///:~
Map之间的比较
| Map 实现 | 描述 |
|---|---|
| HashMap* | 基于哈希表的实现。(使用此类来代替 Hashtable 。)为插入和定位键值对提供了常数时间性能。可以通过构造方法调整性能,这些构造方法允许你设置哈希表的容量和装填因子。 |
| LinkedHashMap | 与 HashMap 类似,但是当遍历时,可以按插入顺序或最近最少使用(LRU)顺序获取键值对。只比 HashMap 略慢,一个例外是在迭代时,由于其使用链表维护内部顺序,所以会更快些。 |
| TreeMap | 基于红黑树的实现。当查看键或键值对时,它们按排序顺序(由 Comparable 或 Comparator 确定)。 TreeMap 的侧重点是按排序顺序获得结果。 TreeMap 是唯一使用 subMap() 方法的 Map ,它返回红黑树的一部分。 |
| WeakHashMap | 一种具有 弱键(weak keys) 的 Map ,为了解决某些类型的问题,它允许释放 Map 所引用的对象。如果在 Map 外没有对特定键的引用,则可以对该键进行垃圾回收。 |
| ConcurrentHashMap | 不使用同步锁定的线程安全 Map。这在并发的一章中讨论。 |
| IdentityHashMap | 使用 == 而不是 equals() 来比较键。仅用于解决特殊问题,不适用于一般用途。 |
//: containers/Maps.java
// Things you can do with Maps.
import java.util.concurrent.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class Maps {
public static void printKeys(Map<Integer,String> map) {
printnb("Size = " + map.size() + ", ");
printnb("Keys: ");
print(map.keySet()); // Produce a Set of the keys
}
public static void test(Map<Integer,String> map) {
print(map.getClass().getSimpleName());
map.putAll(new CountingMapData(25));
// Map has 'Set' behavior for keys:
map.putAll(new CountingMapData(25));
printKeys(map);
// Producing a Collection of the values:
printnb("Values: ");
print(map.values());
print(map);
print("map.containsKey(11): " + map.containsKey(11));
print("map.get(11): " + map.get(11));
print("map.containsValue(\"F0\"): "
+ map.containsValue("F0"));
Integer key = map.keySet().iterator().next();
print("First key in map: " + key);
map.remove(key);
printKeys(map);
map.clear();
print("map.isEmpty(): " + map.isEmpty());
map.putAll(new CountingMapData(25));
// Operations on the Set change the Map:
map.keySet().removeAll(map.keySet());
print("map.isEmpty(): " + map.isEmpty());
}
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>());
}
} /* Output:
HashMap
Size = 25, Keys: [15, 8, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, 0]
Values: [P0, I0, X0, Q0, H0, W0, J0, V0, G0, B0, O0, Y0, E0, T0, L0, S0, D0, M0, R0, C0, N0, U0, K0, F0, A0]
{15=P0, 8=I0, 23=X0, 16=Q0, 7=H0, 22=W0, 9=J0, 21=V0, 6=G0, 1=B0, 14=O0, 24=Y0, 4=E0, 19=T0, 11=L0, 18=S0, 3=D0, 12=M0, 17=R0, 2=C0, 13=N0, 20=U0, 10=K0, 5=F0, 0=A0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 15
Size = 24, Keys: [8, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, 0]
map.isEmpty(): true
map.isEmpty(): true
...
*///:~
HashMap:
jdk 1.7时是数组+链表组成,
jdk1.8时是 数组+ 链表 + 红黑树组成。 链表元素大于等于8时会把链表转为树结构,若桶中链的元素个数小于等于6时,树结构还原成链表。当链表的个数为8左右徘徊时就会生成树转链表,链表转树,效率低下。hasMap的负载因子默认为0.75,2^n是为了散列更加均匀。
SortedMap
使用 SortedMap (由 TreeMap 或 ConcurrentSkipListMap 实现),键保证按排序顺序,这允许在 SortedMap 接口中使用这些方法来提供其他功能:
Comparator comparator():生成用于此 Map 的比较器, null 表示自然排序。T firstKey():返回第一个键。T lastKey():返回最后一个键。SortedMap subMap(fromKey,toKey):生成此 Map 的视图,其中键从 fromKey(包括),到 toKey (不包括)。SortedMap headMap(toKey):使用小于 toKey 的键生成此 Map 的视图。SortedMap tailMap(fromKey):使用大于或等于 fromKey 的键生成此 Map 的视图。
这是一个类似于 SortedSetDemo.java 的示例,显示了 TreeMap 的这种额外行为:
// collectiontopics/SortedMapDemo.java
// What you can do with a TreeMap
import java.util.*;
import onjava.*;
public class SortedMapDemo {
public static void main(String[] args) {
TreeMap<Integer,String> sortedMap =
new TreeMap<>(new CountMap(10));
System.out.println(sortedMap);
Integer low = sortedMap.firstKey();
Integer high = sortedMap.lastKey();
System.out.println(low);
System.out.println(high);
Iterator<Integer> it =
sortedMap.keySet().iterator();
for(int i = 0; i <= 6; i++) {
if(i == 3) low = it.next();
if(i == 6) high = it.next();
else it.next();
}
System.out.println(low);
System.out.println(high);
System.out.println(sortedMap.subMap(low, high));
System.out.println(sortedMap.headMap(high));
System.out.println(sortedMap.tailMap(low));
}
}
/* Output:
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0,
9=J0}
0
9
3
7
{3=D0, 4=E0, 5=F0, 6=G0}
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0}
{3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0}
*/复制ErrorOK!
这里,键值对按照键的排序顺序进行排序。因为 TreeMap 中存在顺序感,所以“位置”的概念很有意义,因此可以拥有第一个、最后一个元素或子图。
LinkedHashMap
LinkedHashMap 针对速度进行哈希处理,但在遍历期间也会按插入顺序生成键值对( System.out.println() 可以遍历它,因此可以看到遍历的结果)。 此外,可以在构造方法中配置 LinkedHashMap 以使用基于访问的 最近最少使用(LRU) 算法,因此未访问的元素(因此是删除的候选者)会出现在列表的前面。 这样可以轻松创建一个能够定期清理以节省空间的程序。下面是一个显示这两个功能的简单示例:
// collectiontopics/LinkedHashMapDemo.java
// What you can do with a LinkedHashMap
import java.util.*;
import onjava.*;
public class LinkedHashMapDemo {
public static void main(String[] args) {
LinkedHashMap<Integer,String> linkedMap =
new LinkedHashMap<>(new CountMap(9));
System.out.println(linkedMap);
// Least-recently-used order:
linkedMap =
new LinkedHashMap<>(16, 0.75f, true);
linkedMap.putAll(new CountMap(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);
}
}
/* Output:
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{6=G0, 7=H0, 8=I0, 0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0}
{6=G0, 7=H0, 8=I0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 0=A0}
*/!
这些键值对确实是按照插入顺序进行遍历,即使对于LRU版本也是如此。 但是,在LRU版本中访问前六项(仅限)后,最后三项将移至列表的前面。然后,当再次访问“ 0 ”后,它移动到了列表的后面。
创建不可修改的 Collection 或 Map
通常,创建 Collection 或 Map 的只读版本会很方便。 Collections 类通过将原始集合传递给一个方法然后返回一个只读版本的集合。 对于 Collection (如果不能将 Collection 视为更具体的类型), List , Set 和 Map ,这类方法有许多变体。这个示例展示了针对每种类型,正确构建只读版本集合的方法:
// collectiontopics/ReadOnly.java
// Using the Collections.unmodifiable methods
import java.util.*;
import onjava.*;
public class ReadOnly {
static Collection<String> data = new ArrayList<>(Countries.names(6));
public static void main(String[] args) {
Collection<String> c = Collections.unmodifiableCollection(new ArrayList<>(data));
System.out.println(c); // Reading is OK
//- c.add("one"); // Can't change it
List<String> a = Collections.unmodifiableList(new ArrayList<>(data));
ListIterator<String> lit = a.listIterator();
System.out.println(lit.next()); // Reading is OK
//- lit.add("one"); // Can't change it
Set<String> s = Collections.unmodifiableSet(new HashSet<>(data));
System.out.println(s); // Reading is OK
//- s.add("one"); // Can't change it
// For a SortedSet:
Set<String> ss = Collections.unmodifiableSortedSet(new TreeSet<>(data));
Map<String,String> m =Collections.unmodifiableMap(new HashMap<(Countries.capitals(6)));
System.out.println(m); // Reading is OK
//- m.put("Ralph", "Howdy!");
// For a SortedMap:
Map<String,String> sm =
Collections.unmodifiableSortedMap(
new TreeMap<>(Countries.capitals(6)));
}
}
/* Output:
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO,
BURUNDI]
ALGERIA
[BENIN, BOTSWANA, ANGOLA, BURKINA FASO, ALGERIA,
BURUNDI]
{BENIN=Porto-Novo, BOTSWANA=Gaberone, ANGOLA=Luanda,
BURKINA FASO=Ouagadougou, ALGERIA=Algiers,
BURUNDI=Bujumbura}
*/!
为特定类型调用 “unmodifiable” 方法不会导致编译时检查,但是一旦发生转换,对修改特定集合内容的任何方法调用都将产生 UnsupportedOperationException 异常。
在每种情况下,在将集合设置为只读之前,必须使用有意义的数据填充集合。填充完成后,最好的方法是用 “unmodifiable” 方法调用生成的引用替换现有引用。这样,一旦使得内容无法修改,那么就不会冒有意外更改内容的风险。另一方面,此工具还允许将可修改的集合保留为类中的私有集合,并从方法调用处返回对该集合的只读引用。所以,你可以在类内修改它,但其他人只能读它。
同步 Collection 或 Map
synchronized 关键字是多线程主题的重要组成部分,更复杂的内容在并发中介绍。在这里,只需要注意到 Collections 类包含一种自动同步整个集合的方法。 语法类似于 “unmodifiable” 方法:
// collectiontopics/Synchronization.java
// Using the Collections.synchronized methods
import java.util.*;
public class Synchronization {
public static void main(String[] args) {
Collection<String> c =
Collections.synchronizedCollection(
new ArrayList<>());
List<String> list = Collections
.synchronizedList(new ArrayList<>());
Set<String> s = Collections
.synchronizedSet(new HashSet<>());
Set<String> ss = Collections
.synchronizedSortedSet(new TreeSet<>());
Map<String,String> m = Collections
.synchronizedMap(new HashMap<>());
Map<String,String> sm = Collections
.synchronizedSortedMap(new TreeMap<>());
}
}
最好立即通过适当的 “synchronized” 方法传递新集合,如上所示。这样,就不会意外地暴露出非同步版本。
Fail Fast
Java 集合还具有防止多个进程修改集合内容的机制。如果当前正在迭代集合,然后有其他一些进程介入并插入,删除或更改该集合中的对象,则会出现此问题。也许在集合中已经遍历过了那个元素,也许还没有遍历到,也许在调用 size() 之后集合的大小会缩小...有许多灾难情景。 Java 集合库使用一种 fail-fast 的机制,该机制可以检测到除了当前进程引起的更改之外,其它任何对集合的更改操作。如果它检测到其他人正在修改集合,则会立即生成 ConcurrentModificationException 异常。这就是“fail-fast”的含义——它不会在以后使用更复杂的算法尝试检测问题(快速失败)。
通过创建迭代器并向迭代器指向的集合中添加元素,可以很容易地看到操作中的 fail-fast 机制,如下所示:
// collectiontopics/FailFast.java
// Demonstrates the "fail-fast" behavior
import java.util.*;
public class FailFast {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
Iterator<String> it = c.iterator();
c.add("An object");
try {
String s = it.next();
} catch(ConcurrentModificationException e) {
System.out.println(e);
}
}
}
/* Output:
java.util.ConcurrentModificationException
*/
异常来自于在从集合中获得迭代器之后,又尝试在集合中添加元素。程序的两个部分可能会修改同一个集合,这种可能性的存在会产生不确定状态,因此异常会通知你更改代码。在这种情况下,应先将所有元素添加到集合,然后再获取迭代器。
ConcurrentHashMap , CopyOnWriteArrayList 和 CopyOnWriteArraySet 使用了特定的技术来避免产生 ConcurrentModificationException 异常。
归纳起来简单地说HashMap:
-
HashMap在底层将key-value对当成一个整体进行处理,这个整体就是一个Entry对象。
-
HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。由此可见,HashMap之所以能快速存、取它所包含的Entry,完全类似于现实生活中的:不同的东西要放在不同的位置,需要时才能快速找到它。
-
当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75。这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap的get()与put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间
-
HashMap时根据实际需要适当地调整load factor的值。如果程序比较关心空间开销,内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕,则可以适当减少负载因子。通常情况下,程序员无需改变负载因子的值。
-
如果开始就知道HashMap会保存多个key-value对,可以在创建时就使用较大的初始化容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无需调用resize()方法重新分配table数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap时初始化容量设置也需要小心对待
Map与Set
Set与Map之间的关系非常密切:
Set集合和Map集合的对应关系如下。
■ Set <-> Map
■ EnumSet <-> EnumMap
■ SortedSet <-> SortedMap
■ TreeSet <-> TreeMap
■ NavigableSet <-> NavigableMap
■ HashSet <-> HashMap
■ LinkedHashSet <-> LinkedHashMap
虽然Map中放的元素是key-value对,Set集合中放的元素是单个对象,但如果我们把key-value对中的value当成key的附庸:key在哪里,value就跟在哪里。这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,而计算Entry存储时则只考虑Entry封装的key。从Java源码来看, Java是先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。
如果把Map里的所有value放在一起来看,它们又非常类似于一个List:元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象作为索引。如果需要从List集合中取出元素,则需要提供该元素的数字索引;如果需要从Map中取出元素,则需要提供该元素的key索引。因此,Map有时也被称为字典,或关联数组
遍历map用keyset()方法:
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap scores = new LinkedHashMap();
scores.put("语文", 80);
scores.put("英文", 82);
scores.put("数学", 76);
//遍历scores里的所有key-value对
for (Object key : scores.keySet()) {
System.out.println(key + "------>" + scores.get(key));
}
}
}
HashSet与HashMap的性能:
对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。
hash表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能:hash算法可以根据hashCode值计算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态为open:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如图8.8所示是hash表保存各元素,且发生“hash冲突”的示意图
因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性:
- 容量(capacity) :hash中桶的数量
- 初始化容量(initial capacity) :创建hash表时桶的数量,HashMap和HashSet都允许在构造器中指定初始化容量
- 尺寸(size) :当前hash表中记录的数量。
- 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢
hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。HashSet和HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销。程序员可以根据实际情况来调整HashSet和HashMap的“负载极限”值。
如果开始就知道HashSet和HashMap、Hashtable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet和HashMap、Hashtable所包含的最大记录数除以负载极限,就不会发生rehashing。使用足够大的初始化容量创建HashSet和HashMap、Hashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得太高