- Map的内部结构Entry
- Set与Map的关系
- Map的内部类Entry
- Map的通用方法及Map的简单用法
- HashMap和HashTable的区别
- HashMap和HashTable判断元素相等的标准
- 可变对象对HashMap的影响
- LinkedHashMap的特征
- Properties 的特征
- SortedMap接口和TreeMap类的特征
- WeekHashMap
- IdentifyHashMap与HashMap的区别
- EnumMap的特征
- 各种Map实现类的性能分析
Map的内部结构
Set和Map的关系
Map由key-value对组成,key不可以重复(因为无序),Map接口定义了一个Set keySet();方法用来返回所有key.即所有key组成了一个Set集合。
Map中key集合与集合容器Set非常相似,因此Map和Set的子集也非常相似,从名字上看,Set集合有HashSet, LinkeHashSet, SortedSet, TreeSet, EnumSet;而Map有HashMap, LinkedHashMap, SortedMap, TreeMap, EnumMap等子接口和实现类。Set和Map中的key集合的存储方式完全相同,从JDK源码来看,JAVA是先实现了Map, 然后包装一个value集为null的Map就实现了Set.
Map的内部类Entry
Map的内部有一个内部类Entry,用来封装key-value对。Entry中的部分方法,Object getKey(); Object getValue(); Object setValue(V value);下面是Map接口JDK源代码中的定义,
1 public interface Map 2 { 3 public static interface Entry 4 { 5 6 public abstract Object getKey(); 7 8 public abstract Object getValue(); 9 10 public abstract Object setValue(Object obj); 11 12 public abstract boolean equals(Object obj); 13 14 public abstract int hashCode(); 15 } 16 17 ....
下面是Map接口的一个实现类HashMap, 可以看到HashMap的源码中,在内部类Entry中增加了几个私有成员,
1 public class HashMap extends AbstractMap 2 implements Map, Cloneable, Serializable 3 { 4 static class Entry 5 implements Map.Entry 6 { 7 8 ...... 9 10 final Object key; 11 Object value; 12 Entry next; 13 int hash; 14 15 //初始化内部类 16 Entry(int i, Object obj, Object obj1, Entry entry) 17 { 18 value = obj1; 19 next = entry; 20 key = obj; 21 hash = i; 22 } 23 } 24 25 ......
当HashMap要put进新元素时,会初始化一个Entry类来保存key-value对。如下面的JDK源代码,
1 public Object put(Object obj, Object obj1) 2 { 3 if(table == EMPTY_TABLE) 4 inflateTable(threshold); 5 if(obj == null) 6 return putForNullKey(obj1); 7 int i = hash(obj); 8 int j = indexFor(i, table.length); 9 for(Entry entry = table[j]; entry != null; entry = entry.next) 10 { 11 Object obj2; 12 if(entry.hash == i && ((obj2 = entry.key) == obj || obj.equals(obj2))) 13 { 14 Object obj3 = entry.value; 15 entry.value = obj1; 16 entry.recordAccess(this); 17 return obj3; 18 } 19 } 20 21 modCount++; 22 addEntry(i, obj, obj1, j); 23 return null; 24 }
第22行会初始化一个Entry对象,
1 ...... 2 void addEntry(int i, Object obj, Object obj1, int j) 3 { 4 if(size >= threshold && null != table[j]) 5 { 6 resize(2 * table.length); 7 i = null == obj ? 0 : hash(obj); 8 j = indexFor(i, table.length); 9 } 10 createEntry(i, obj, obj1, j); 11 } 12 ...... 13 void createEntry(int i, Object obj, Object obj1, int j) 14 { 15 Entry entry = table[j]; 16 table[j] = new Entry(i, obj, obj1, entry); 17 size++; 18 } 19 ......
Map接口中定义的方法及Map的简单用法
基本方法有get(Object obj), put(V value), remove(Object obj), putAll(Map map)...
其他方法,故名思议,
containsKey(Object obj);containsValue(Object obj);clear();equals(Object obj);
另外一些方法,解释如下,
public abstract Set keySet(); 返回该Map中所有key组成的Set集合
public abstract Collection values();返回该Map中所有value组成的collection集合
public abstract Set entrySet();返回Map中的所有Entry对象组成的Set集合,一个Entry对象是由key-value对组成。
下面是Map的一个简单用法,
1 package collection.map; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 public class Maps { 7 public static void main(String[] args) { 8 Map map = new HashMap(); 9 map.put("a",1); 10 map.put("b",2); 11 map.put("c",3); 12 //key不同时,value可以重复 13 map.put("d",4); 14 System.out.println(map); 15 //key重复时,新的value会覆盖旧的value,同时返回旧的value 16 System.out.println(map.put("d", 5)); 17 System.out.println(map); 18 System.out.println(map.containsKey("b")); 19 System.out.println(map.containsValue(3)); 20 21 for(Object key : map.keySet()) { 22 //map.get(key)方法获取value 23 System.out.print(map.get(key)+","); 24 } 25 System.out.print(" "); 26 map.remove("c"); 27 System.out.println(map); 28 } 29 }
执行结果,
1 {d=4, b=2, c=3, a=1} 2 4 3 {d=5, b=2, c=3, a=1} 4 true 5 true 6 5,2,3,1, 7 {d=5, b=2, a=1}
HashMap和Hashtable
HashMap和Hashtable是Map的两个典型的实现类,它们之间的关系与HashSet和Vector完全类似。Hashtable是一个古老的Map实现类,虽然支持安全线程,但现在已经可以通过Collections工具类来同步容器的线程了,所以Hashtable用得非常少。
HashMap和HashTable判断元素相等的标准
与HashSet一样,HashMap和Hashtable判断元素是否相等的标准,也是需要同时满足两个key通过equals方法对比返回true,两个key的hashCode值要相等。在Map接口中已经定义了equals和hashCode方法,所以在HashMap和Hashtable中,必须重写这两个方法,且必须满足上面的规则。下面是一个例子,
1 package collection.map; 2 3 import java.util.HashMap; 4 import java.util.Hashtable; 5 6 class A { 7 int count; 8 public A(int count) { 9 this.count = count; 10 } 11 //根据count的值来判断两个对象是否相等 12 public boolean equals(Object obj) { 13 if (this == obj) return true; 14 if (obj != null && obj.getClass() == A.class) { 15 A a = (A)obj; 16 return this.count == a.count; 17 } 18 return false; 19 } 20 21 //根据count值来计算hashCode 22 public int hashCode() { 23 return this.count; 24 } 25 } 26 27 class B { 28 //重写equals, B与任何对象比较都返回true 29 public boolean equals(Object obj) { 30 return true; 31 } 32 } 33 34 public class HashTables { 35 public static void main(String[] args) { 36 Hashtable ht = new Hashtable(); 37 ht.put(new A(100), "a"); 38 ht.put(new A(200), "b"); 39 ht.put(new A(300), new B()); 40 System.out.println(ht); 41 //只要两个对象通过equal比较返回true,Hashtable就认为它们的value相等 42 //由于此处Hashtable会调用value为B对象的equals做比较,所以总返回true,在HashMap中则返回false 43 //这也是Hashtable和HashMap底层containsValue方法的区别 44 System.out.println(ht.containsValue("test")); 45 //比较key则需要equal相等且hashCode相等 46 System.out.println(ht.containsKey(new A(300))); 47 System.out.println(ht.containsKey(new A(305))); 48 ht.remove(new A(200)); 49 System.out.println(ht); 50 } 51 52 }
执行结果,
1 {collection.map.A@12c=collection.map.B@1b5998f, collection.map.A@c8=b, collection.map.A@64=a} 2 true 3 true 4 false 5 {collection.map.A@12c=collection.map.B@1b5998f, collection.map.A@64=a}
上面这个例子中可见,Hashtable判断两个元素是否相等(即判断Key是否相等)的标准是,即要equals返回true,又要hashCode相等,而Hashtable判断两个元素的value是否相等,只需要通过equals比较两个value是否返回true,并且这个equals是可以重载的。
可变对象对HashMap的影响
与HashSet类似的是,如果使用可变对象作为HashMap,Hashtable的key,如果程序修改了组成key的可变变量,则也可能会破坏HashMap和Hashtable,导致无法正常访问。
比如下面的例子,
1 package collection.map; 2 3 import java.util.HashMap; 4 import java.util.Iterator; 5 6 class C { 7 int count; 8 public C(int count) { 9 this.count = count; 10 } 11 //根据count的值来判断两个对象是否相等 12 public boolean equals(Object obj) { 13 if (this == obj) return true; 14 if (obj != null && obj.getClass() == C.class) { 15 C a = (C)obj; 16 return this.count == a.count; 17 } 18 return false; 19 } 20 21 //根据count值来计算hashCode 22 public int hashCode() { 23 return this.count; 24 } 25 } 26 27 public class HashMapErr { 28 public static void main(String[] args) { 29 HashMap hm = new HashMap(); 30 hm.put(new C(100),1); 31 hm.put(new C(200), 2); 32 hm.put(new C(300), 3); 33 Iterator it = hm.keySet().iterator(); 34 C first = (C)it.next(); 35 System.out.println(first.count); 36 first.count = 500; 37 System.out.println(hm); 38 //key被修改后,对应的元素无法删除,元素也无法取出 39 hm.remove(new C(100)); 40 System.out.println(hm); 41 System.out.println(hm.get(new C(1))); 42 43 //未被修改的key对应的元素可以被删除 44 hm.remove(new C(200)); 45 System.out.println(hm); 46 47 } 48 }
执行结果,
1 100 2 {collection.map.C@1f4=1, collection.map.C@c8=2, collection.map.C@12c=3} 3 {collection.map.C@1f4=1, collection.map.C@c8=2, collection.map.C@12c=3} 4 null 5 {collection.map.C@1f4=1, collection.map.C@12c=3}
LinkedHashMap的特征
HashSet有一个子类LinkedHashSet,与此类似的是HashMap有一个子类LinkedHashMap。 LinkedHashMap也是一个有序集合,其内部由一个双向链表来维护元素顺序(只需要维护key的顺序),默认顺序为插入元素的顺序,Map的迭代顺序也是根据此双向链表决定的。
LinkedHashMap因为要维护元素顺序,因此插入和删除的性能比起HashMap略低,但是迭代遍历的性能会更高。 下面是一个用LinkedHashMap迭代遍历的例子,
1 package collection.map; 2 3 import java.util.LinkedHashMap; 4 import java.util.Map; 5 6 public class LinkedHashMaps { 7 public static void main(String[] args) { 8 Map lhm= new LinkedHashMap(); 9 lhm.put("a", 1); 10 lhm.put("b", 2); 11 lhm.put("c", 3); 12 System.out.println(lhm); 13 lhm.remove("b"); 14 lhm.put("b", 2); 15 System.out.println(lhm); 16 } 17 }
执行结果,
1 {a=1, b=2, c=3} 2 {a=1, c=3, b=2}
Properties 的特征
Properties是Hashtable的一个子类,它的key和value都必须是String类型,它可以将一个Map对象和文件IO (inputStream, outputStream)关联起来,通过store(...)方法将Map对象的数据写入文件或者通过load(...)方法从文件读取数据写入Map. Properties还提供了以下方法,getProperty(...), setProperty(...)。下面是它的典型用法,
1 package collection.map; 2 3 import java.io.FileInputStream; 4 import java.io.FileNotFoundException; 5 import java.io.FileOutputStream; 6 import java.io.IOException; 7 import java.util.Properties; 8 9 public class Propertieses { 10 public static void main(String[] args) throws FileNotFoundException, IOException { 11 Properties pos = new Properties(); 12 pos.setProperty("a", "1"); 13 pos.setProperty("b", "2"); 14 //write pos to output.txt 15 pos.store(new FileOutputStream("output.txt"), "here is a comment line"); 16 17 Properties pos2 = new Properties(); 18 pos2.setProperty("c", "3"); 19 // read from output.txt and set to pos2 20 pos2.load(new FileInputStream("output.txt")); 21 System.out.println(pos2); 22 } 23 }
执行结果,
{b=2, a=1, c=3}
同时,output.txt中写入了pos的内容如下,
#here is a comment line #Tue Nov 01 14:01:09 CST 2016 b=2 a=1
SortedMap接口和TreeMap类的特征
Set接口派生了一个SortedSet子接口,SortedSet 有一个实现类TreeSet, 与此类似的是,Map接口派生了一个SortedMap子接口,SortedMap子接口有一个实现类TreeMap. TreeMap底层是一个红黑树数据结构,每个节点就是一个key-value对,节点按照key进行排序,TreeMap中和TreeSet一样,也有两种排序方式,即自然排序(由元素实现Comparable控制排序逻辑)和定制排序(在TreeMap集合中传入一个排序逻辑Comparator), 由于前面已经多次提到这两种排序方式,下面直接给出例子,
1 package collection.map; 2 3 import java.util.TreeMap; 4 5 class T implements Comparable { 6 int count; 7 public T(int count) { 8 this.count = count; 9 } 10 public String toString() { 11 return "T[count:"+count+"]"; 12 } 13 public boolean equals(Object obj) { 14 if(this==obj) return true; 15 if(obj != null && obj.getClass() == T.class) { 16 T t = (T)obj; 17 return this.count == t.count; 18 } 19 return false; 20 } 21 @Override 22 public int compareTo(Object obj) { 23 T t = (T)obj; 24 return this.count - t.count; 25 } 26 } 27 28 public class TreeMaps { 29 public static void main(String[] args) { 30 TreeMap tm = new TreeMap(); 31 tm.put(new T(5), "a"); 32 tm.put(new T(-3), "b"); 33 tm.put(new T(9), "c"); 34 System.out.println(tm); 35 System.out.println(tm.firstEntry()); 36 System.out.println(tm.lastKey()); 37 System.out.println(tm.higherKey(new T(-3))); 38 System.out.println(tm.lowerKey(new T(9))); 39 //return sub map 40 System.out.println(tm.subMap(new T(-3), new T(9))); 41 } 42 }
执行结果
1 {T[count:-3]=b, T[count:5]=a, T[count:9]=c} 2 T[count:-3]=b 3 T[count:9] 4 T[count:5] 5 T[count:5] 6 {T[count:-3]=b, T[count:5]=a}
定制排序和HashSet中的定制排序一样,即在TreeMap初始化的时候,传入一个Comparator对象作为参数, 排序逻辑在Comparator中写。
从上面的例子中还可以看到,TreeMap判断两个元素相等的标准是,通过key进行equals比较返回true, 同时通过CompareTo比较key也要返回true。 因此在重写了key中的equals之后,一定要保证CompareTo方法也要返回跟equals一致的结果。
WeakHashMap
WeakHashMap是一种弱引用Map,意思是集合中的key对实际对象只有弱引用, 因此当垃圾回收了该key所对应的实际对象后, WeakHashMap会自动删除该key对应的key-value。 而HashMap是一种强引用,只要HashMap对象没被销毁,key所引用的对象是不会被垃圾回收器回收的,因此HashMap也不会自动删除key-value对。 下面的例子演示了WeakHashMap弱引用,
1 package collection.map; 2 3 import java.util.WeakHashMap; 4 5 public class WeekHashMaps { 6 public static void main(String[] args) { 7 WeakHashMap wm = new WeakHashMap(); 8 //new String("aa")是匿名对象, WeakHashMap只保留它的弱引用 9 wm.put(new String("aa"), 1); 10 wm.put(new String("bb"), 2); 11 //"cc"是字符串直接量,WeakHashMap保留它的强引用 12 wm.put("cc", 3); 13 System.out.println(wm); 14 //通知系统回收垃圾 15 System.gc(); 16 System.runFinalization(); 17 //前两个key-value元素将被删除,只能看到最后一个元素 18 System.out.println(wm); 19 } 20 }
执行结果,
1 {cc=3, bb=2, aa=1} 2 {cc=3}
IdentifyHashMap
IdentifyHashMap与HashMap的实现机制基本相同,不同点是IdentifyHashMap判断两个元素(即两个key)相等的标准不同。HashMap要求两个元素的key通过equals对比返回true,通过hashCode返回的值也相等,则认为这两个元素相等; 而IdentifyHashMap判断两个元素相等的标准是, 当且仅当两个元素的key严格相等(key1 == key2)时,才认为两个元素相等。下面演示IdentifyHashMap的用法,
1 package collection.map; 2 3 import java.util.IdentityHashMap; 4 5 public class IdentityHashMaps { 6 public static void main(String[] args) { 7 IdentityHashMap im = new IdentityHashMap(); 8 im.put(new String("aa"),1); 9 im.put(new String("aa"),2); 10 System.out.println(im); 11 im.put("bb",3); 12 im.put("bb",4); 13 System.out.println(im); 14 } 15 }
可以看到,对于两个匿名对象new String("aa")的key,key1 != key2 ,因此IdentityHashMap会将它们看作不同的key,都添加进集合中。只有下面的两个常量字符串"bb",由于满足了key1==k2,所以IdentityHashMap只能添加其中一个(第二个覆盖了第一个),执行结果如下,
1 {aa=2, aa=1} 2 {aa=2, aa=1, bb=4}
EnumMap的特征
EnumMap必须与枚举对象一起使用,EnumMap中所有key必须是单个枚举类的枚举值,EnumMap根据key的自然顺序维护集合顺序。EnumMap内部以数组形式保存,所以性能好。下面示范用法,
1 package collection.map; 2 3 import java.util.EnumMap; 4 5 enum Season 6 { 7 SPRING, SUMMER, FALL, WINTER 8 } 9 10 public class EnumMaps { 11 public static void main(String[] args) { 12 EnumMap em = new EnumMap(Season.class); 13 em.put(Season.SPRING, 1); 14 em.put(Season.SUMMER, 2); 15 System.out.println(em); 16 } 17 }
执行结果,
{SPRING=1, SUMMER=2}
各种Map实现类的性能分析
对于Map的两个常用实现类HashMap和Hashtable, 其实现机制几乎一样,但是Hashtable是一个古老,保证线程安全的集合,所以要比HashMap性能差。
TreeMap通常要比HashMap和Hashtable性能差,因为TreeMap底层要用红黑树来保持元素的顺序。
TreeMap的一个典型用法是,由于TreeMap中的key总是处于排序状态,可以用keySet()方法获取所有key, 接着用toArray()方法生成一个数组,再用Arrays工具类中的binaryArray()方法对已经排序的key数组进行快速查询对象。
对于一般情况,推荐使用HashMap,HashMap的查询速度也比较快(底层由object[]数组保存Entry对象),只是HashMap没有像TreeMap已经排好序的属性。
LinkedHashMap比HashMap要慢,因为它需要链表来保存元素的添加顺序(用来做集合迭代)。
IdentityHashMap与HashMap底层实现方式相似,性能上没有什么特别之处,只是两者判断元素是否相等的标准不同。
EnumMap的性能最好,但它只能使用同一个枚举类的值作为key.