本文最后更新于:2024年3月18日 凌晨
Java Map
Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap,Hashtable,LinkedHashMap和TreeMap,类继承关系如下图所示。
HashMap :它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的,HashMap最多只允许一条记录的键为null,允许多条记录的值为null,HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致,如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
Hashtable :Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁,Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
LinkedHashMap :LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
TreeMap :TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的,如果使用排序的映射,建议使用TreeMap,在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
Properties :Hashtable还有个子类Properties,其关键字和值只能是String类型,经常被用来存储和访问配置信息。
Map是包括了关键字,值以及它们的映射关系的集合,可分别使用如下方法得到。
public Set<K> keySet():关键字的集合。
public Collection<V> values():值的集合。
public Set<Map.Entry<K,V>> entrySet():
Map中还定义了对Map数据集合的操作方法,如下所示:
public void clear():清空整个数据集合。
public V get(K key):根据关键字得到对应值。
public V put(K key,V value):加入新的"关键字-值”,如果该映射关系在map中已存在,则修改映射的值,返回原来的值,如果该映射关系在map中不存在,则返回null
public V remove(Object key):删除Map中关键字所对应的映射关系,返回结果同put()方法。
public boolean equals(Object obj):判断Map对象与参数对象是否等价,两个 Map相等,当且仅当其entrySet()得到的集合是一致的。
public boolean containsKey(Object key):判断在Map中是否存在与键值匹配的映射关系。
public boolean contains Values(Object value):判断在Map中是否存在与键值匹配的映射关系。
HashMap
HashMap以Hash表数据结构实现,查找对象时通过Hash函数计算其位置,它是为快速查询而设计的,其内部定义了一个 Hash表数组Entry[] table,元素会通过Hash函数将元素的Hash值转换成数组中存放的索引,如果有冲突,则使用链表的形式将所有相同Hash值的元素串起来,可以通过查看HashMap.Entry的源码它是一个单链表结构。
HashMap 是线程不安全的,不是同步的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Map 接口的使用 { public static void main (String[] args) { Map<String,String> m = new HashMap<String, String>(); m.put("张三" ,"2003011" ); m.put("李四" ,"2003012" ); m.put("王五" ,"2003013" ); m.put("张三" ,"2003001" ); Set<String> keys = m.keySet(); for (Iterator<String> i = keys.iterator();i.hasNext();){ System.out.print(i.next()+"," ); } System.out.println(m.values()); } } 李四,张三,王五。 [2003012 , 2003001 , 2003013 ]
LinkedHashMap
LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用 LinkedHashMap
**LinkedHashMap 是 Map 接口的Hash表和链接列表实现,具有可预知的迭代顺序,**此实现提供所有可选的映射操作,并允许使用 null 值和 null 键,此类不保证映射的顺序,特别是它不保证该顺序不变。
LinkedHashMap 实现与 HashMap 的不同之处在于,前者维护着一个运行于所有条目的双重链接列表,此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表,默认是按插入顺序排序,如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)
注意 :
此实现不是同步的,如果多个线程同时访问链接的Hash映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
由于 LinkedHashMap 需要维护元素的插入顺序,因此性能略低于 HashMap 的性能,但在迭代访问 Map 里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Map 接口的使用 { public static void main (String[] args) { Map<String,String> m = new LinkedHashMap<String, String>(); m.put("张三" ,"2003011" ); m.put("李四" ,"2003012" ); m.put("王五" ,"2003013" ); Set<String> keys = m.keySet(); for (Iterator<String> i = keys.iterator();i.hasNext();) System.out.print(i.next()+"," ); System.out.println(m.values()); } } 张三,李四,王五。 [2003012 , 2003001 , 2003013 ]
定义
LinkedHashMap继承了HashMap,所以它们有很多相似的地方。
1 2 public class LinkedHashMap <K ,V > extends HashMap <K ,V > implements Map <K ,V > { }
构造方法
LinkedHashMap提供了多个构造方法,我们先看空参的构造方法。
1 2 3 4 5 6 public LinkedHashMap () { super (); accessOrder = false ; }
首先使用super调用了父类HashMap的构造方法,其实就是根据初始容量,负载因子去初始化Entry[] table
然后把accessOrder设置为false,这就跟存储的顺序有关了,LinkedHashMap存储数据是有序的,而且分为两种:插入顺序和访问顺序。
这里accessOrder设置为false,表示不是访问顺序而是插入顺序存储的,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的,LinkedHashMap也提供了可以设置accessOrder的构造方法,我们来看看这种模式下,它的顺序有什么特点?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Map <String , String > linkedHashMap = new LinkedHashMap<>(16 , 0.75 f, true ); linkedHashMap.put("name1" , "josan1" ); linkedHashMap.put("name2" , "josan2" ); linkedHashMap.put("name3" , "josan3" ); System.out.println("开始时顺序:" );Set <Entry<String , String >> set = linkedHashMap.entrySet();Iterator <Entry<String , String >> iterator = set .iterator();while (iterator.hasNext()) { Entry entry = iterator.next(); String key = (String ) entry.getKey(); String value = (String ) entry.getValue(); System.out.println("key:" + key + ",value:" + value); } System.out.println("通过get方法,导致key为name1对应的Entry到表尾" ); linkedHashMap.get ("name1" );Set <Entry<String , String >> set2 = linkedHashMap.entrySet();Iterator <Entry<String , String >> iterator2 = set2.iterator();while (iterator2.hasNext()) { Entry entry = iterator2.next(); String key = (String ) entry.getKey(); String value = (String ) entry.getValue(); System.out.println("key:" + key + ",value:" + value); }
因为调用了get(“name1”)导致了name1对应的Entry移动到了最后,这里只要知道LinkedHashMap有插入顺序和访问顺序两种就可以,后面会详细讲原理。
还记得,上一篇HashMap解析中提到,在HashMap的构造函数中,调用了init方法,而在HashMap中init方法是空实现,但LinkedHashMap重写了该方法,所以在LinkedHashMap的构造方法里,调用了自身的init方法,init的重写实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 readObject) before any entries are inserted into the map. Initializes the chain. / @Override void init() { header = new Entry<>(-1 , null , null , null ); header.before = header.after = header; }
这好像跟我们上一篇HashMap提到的Entry有些不一样,HashMap中静态内部类Entry是这样定义的:
1 2 3 4 5 6 static class Entry <K ,V > implements Map .Entry <K ,V > { final K key; V value; Entry<K,V> next; int hash; }
没有before和after属性啊!原来,LinkedHashMap有自己的静态内部类Entry,它继承了HashMap.Entry,定义如下:
1 2 3 4 5 6 7 8 9 10 private static class Entry < K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry (int hash, K key, V value, HashMap.Entry<K,V> next) { super (hash, key, value, next); }
所以LinkedHashMap构造函数,主要就是调用HashMap构造函数初始化了一个Entry[] table,然后调用自身的init初始化了一个只有头结点的双向链表,完成了如下操作:
put方法
LinkedHashMap没有重写put方法,所以还是调用HashMap得到put方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public V put (K key, V value ) { if (key == null ) return putForNullKey(value ); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals (k))) { V oldValue = e.value ; e.value = value ; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value , i); return null ; }
我们看看LinkedHashMap的addEntry方法:
1 2 3 4 5 6 7 8 9 10 void addEntry (int hash, K key, V value , int bucketIndex ) { super.addEntry(hash, key, value , bucketIndex); Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } }
这里调用了父类HashMap的addEntry方法,如下:
1 2 3 4 5 6 7 8 9 10 void addEntry (int hash, K key, V value , int bucketIndex ) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0 ; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value , bucketIndex); }
前面是扩容相关的代码,在上一篇HashMap解析中已经讲过了,这里主要看createEntry方法,LinkedHashMap进行了重写。
1 2 3 4 5 6 7 8 9 void createEntry (int hash, K key, V value , int bucketIndex ) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value , old); table[bucketIndex] = e; e.addBefore(header); size++; }
我们来看看LinkedHashMap.Entry的addBefore方法:
1 2 3 4 5 6 private void addBefore (Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this ; after.before = this ; }
从这里就可以看出,当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中,所以可以看出LinkedHashMap就是HashMap+双向链表,下面用图来表示逐步往LinkedHashMap中添加数据的过程,红色部分是双向链表,黑色部分是HashMap结构,header是一个Entry类型的双向链表表头,本身不存储数据。
首先是只加入一个元素Entry1,假设index为0:
当再加入一个元素Entry2,假设index为15:
当再加入一个元素Entry3,假设index也是0:
以上,就是LinkedHashMap的put的所有过程了,总体来看,跟HashMap的put类似,只不过多了把新增的Entry加入到双向列表中。
扩容
在HashMap的put方法中,如果发现前元素个数超过了扩容阀值时,会调用resize方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void resize (int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return ; } Entry[] newTable = new Entry[newCapacity]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash); table = newTable; threshold = (int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 ); }
LinkedHashMap重写了transfer方法,数据的迁移,它的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 void transfer (HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e = header.after; e != header; e = e.after) { if (rehash) e.hash = (e.key == null ) ? 0 : hash(e.key); int index = indexFor(e.hash, newCapacity); e.next = newTable[index]; newTable[index] = e; } }
可以看出,LinkedHashMap扩容时,数据的再散列和HashMap是不一样的。
HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置。
LinkedHashMap是遍历的双向链表,取得每一个Entry,然后重新计算hash值,然后存放到新table的对应位置。
从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数),而遍历table是N+table的空余个数(N为元素个数)
双向链表的重排序
前面分析的,主要是当前LinkedHashMap中不存在当前key时,新增Entry的情况,当key如果已经存在时,则进行更新Entry的value,就是HashMap的put方法中的如下代码:
1 2 3 4 5 6 7 8 9 10 for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals (k))) { V oldValue = e.value ; e.value = value ; e.recordAccess(this ); return oldValue; } }
主要看e.recordAccess(this),这个方法跟访问顺序有关,而HashMap是无序的,所以在HashMap.Entry的recordAccess方法是空实现,但是LinkedHashMap是有序的,LinkedHashMap.Entry对recordAccess方法进行了重写。
1 2 3 4 5 6 7 8 9 10 11 12 void recordAccess (HashMap<K,V> m ) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove (); addBefore(lm.header); } }
在LinkedHashMap中,只有accessOrder为true,即是访问顺序模式,才会put时对更新的Entry进行重新排序,而如果是插入顺序模式时,不会重新排序,这里的排序跟在HashMap中存储没有关系,只是指在双向链表中的顺序。
举个栗子:开始时,HashMap中有Entry1,Entry2,Entry3,并设置LinkedHashMap为访问顺序,则更新Entry1时,会先把Entry1从双向链表中删除,然后再把Entry1加入到双向链表的表尾,而Entry1在HashMap结构中的存储位置没有变化,对比图如下所示:
get方法
LinkedHashMap有对get方法进行了重写,如下:
1 2 3 4 5 6 7 8 9 public V get (Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null ) return null ; e.recordAccess(this ); return e.value; }
先是调用了getEntry方法,通过key得到Entry,而LinkedHashMap并没有重写getEntry方法,所以调用的是HashMap的getEntry方法,在上一篇文章中我们分析过HashMap的getEntry方法:首先通过key算出hash值,然后根据hash值算出在table中存储的index,然后遍历table[index]的单向链表去对比key,如果找到了就返回Entry
后面调用了LinkedHashMap.Entry的recordAccess方法,上面分析过put过程中这个方法,其实就是在访问顺序的LinkedHashMap进行了get操作以后,重新排序,把get的Entry移动到双向链表的表尾。
遍历方式取数据
我们先来看看HashMap使用遍历方式取数据的过程:
很明显,这样取出来的Entry顺序肯定跟插入顺序不同了,既然LinkedHashMap是有序的,那么它是怎么实现的呢?先看看LinkedHashMap取遍历方式获取数据的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 Map <String , String > linkedHashMap = new LinkedHashMap<>(); linkedHashMap.put("name1" , "josan1" ); linkedHashMap.put("name2" , "josan2" ); linkedHashMap.put("name3" , "josan3" );Set <Entry<String , String >> set = linkedHashMap.entrySet();Iterator <Entry<String , String >> iterator = set .iterator();while (iterator.hasNext()) { Entry entry = iterator.next(); String key = (String ) entry.getKey(); String value = (String ) entry.getValue(); System.out.println("key:" + key + ",value:" + value); }
LinkedHashMap没有重写entrySet方法,我们先来看HashMap中的entrySet,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public Set <Map .Entry<K,V>> entrySet() { return entrySet0(); } private Set <Map .Entry<K,V>> entrySet0() { Set <Map .Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } private final class EntrySet extends AbstractSet <Map .Entry <K ,V >> { public Iterator <Map .Entry<K,V>> iterator() { return newEntryIterator(); } ...... }
可以看到,HashMap的entrySet方法,其实就是返回了一个EntrySet对象。
我们得到EntrySet会调用它的iterator方法去得到迭代器Iterator,从上面的代码也可以看到,iterator方法中直接调用了newEntryIterator方法并返回,而LinkedHashMap重写了该方法。
1 2 3 Iterator <Map .Entry<K,V>> newEntryIterator() { return new EntryIterator(); }
这里直接返回了EntryIterator对象,这个和上一篇HashMap中的newEntryIterator方法中一模一样,都是返回了EntryIterator对象,其实他们返回的是各自的内部类,我们来看看LinkedHashMap中EntryIterator的定义:
1 2 3 4 5 private class EntryIterator extends LinkedHashIterator <Map .Entry <K ,V >> { public Map.Entry<K,V> next () { return nextEntry(); } }
该类是继承LinkedHashIterator,并重写了next方法,而HashMap中是继承HashIterator
我们再来看看LinkedHashIterator的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private abstract class LinkedHashIterator <T > implements Iterator <T > { Entry<K,V> nextEntry = header.after; Entry<K,V> lastReturned = null ; public boolean hasNext () { return nextEntry != header; } Entry<K,V> nextEntry () { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == header) throw new NoSuchElementException(); Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; } ...... }
我们先不看整个类的实现,只要知道在LinkedHashMap中,Iterator<Entry<String, String>> iterator = set.iterator(),这段代码会返回一个继承LinkedHashIterator的Iterator,它有着跟HashIterator不一样的遍历规则。
接着,我们会用while(iterator.hasNext())去循环判断是否有下一个元素,LinkedHashMap中的EntryIterator没有重写该方法,所以还是调用LinkedHashIterator中的hasNext方法,如下:
1 2 3 4 5 public boolean hasNext () { return nextEntry != header; }
nextEntry表示下一个应该返回的Entry,默认值是header.after,即双向链表表头的下一个元素,而上面介绍到,LinkedHashMap在初始化时,会调用init方法去初始化一个before和after都指向自身的Entry,但是put过程会把新增加的Entry加入到双向链表的表尾,所以只要LinkedHashMap中有元素,第一次调用hasNext肯定不会为false
然后我们会调用next方法去取出Entry,LinkedHashMap中的EntryIterator重写了该方法,如下:
1 2 3 public Map.Entry<K,V> next ( ) { return nextEntry(); }
而它自身又没有重写nextEntry方法,所以还是调用的LinkedHashIterator中的nextEntry方法:
1 2 3 4 5 6 7 8 Entry<K,V> nextEntry ( ) { Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; }
这里其实遍历的是双向链表,所以不会存在HashMap中需要寻找下一条单向链表的情况,从头结点Entry header的下一个节点开始,只要把当前返回的Entry的after作为下一个应该返回的节点即可,直到到达双向链表的尾部时,after为双向链表的表头节点Entry header,这时候hasNext就会返回false,表示没有下一个元素了,LinkedHashMap的遍历取值如下图所示:
易知,遍历出来的结果为Entry1,Entry2…Entry6
可得,LinkedHashMap是有序的,且是通过双向链表来保证顺序的。
remove方法
LinkedHashMap没有提供remove方法,所以调用的是HashMap的remove方法,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public V remove (Object key ) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value ); }final Entry<K,V> removeEntryForKey (Object key ) { int hash = (key == null ) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null ) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals (k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this ); return e; } prev = e; e = next; } return e; }
在上一篇HashMap中就分析了remove过程,其实就是断开其他对象对自己的引用,比如被删除Entry是在单向链表的表头,则让它的next放到表头,这样它就没有被引用了,如果不是在表头,它是被别的Entry的next引用着,这时候就让上一个Entry的next指向它自己的next,这样,它也就没被引用了。
在HashMap.Entry中recordRemoval方法是空实现,但是LinkedHashMap.Entry对其进行了重写,如下:
1 2 3 4 5 6 7 8 void recordRemoval (HashMap<K,V> m ) { remove (); }private void remove ( ) { before.after = after; after.before = before; }
易知,这是要把双向链表中的Entry删除,也就是要断开当前要被删除的Entry被其他对象通过after和before的方式引用。
所以,LinkedHashMap的remove操作,首先把它从table中删除,即断开table或者其他对象通过next对其引用,然后也要把它从双向链表中删除,断开其他对应通过after和before对其引用。
HashMap与LinkedHashMap的结构对比
再来看看HashMap和LinkedHashMap的结构图,是不是秒懂了,LinkedHashMap其实就是可以看成HashMap的基础上,多了一个双向链表来维持顺序。
Hashtable
Hashtable是原始的java.util的一部分,是一个Dictionary具体的实现,然而,Java 2 重构的Hashtable实现了Map接口,因此,Hashtable现在集成到了集合框架中,它和HashMap类很相似,但是它支持同步,所有的读写等操作都进行了锁(synchronized)保护,在多线程环境下没有安全问题,但是锁保护也是有代价的,会对读写的效率产生较大影响。
像HashMap一样,Hashtable在Hash表中存储键/值对,当使用一个Hash表,要指定用作键的对象,以及要链接到该键的值,然后,该键经过Hash处理,所得到的Hash值被用作存储在该表中值的索引,但是HashTable 的 key,value 都不可为 null
HashTable类中,保存实际数据的,依然是Entry对象,其数据结构与HashMap是相同的。
Hashtable定义了四个构造方法
创建一个指定大小的Hashtable,并且通过fillRatio指定填充比例。
1 Hashtable(int size,float fillRatio)
Hashtable中除了从Map接口中定义的方法外,还定义了以下方法:
序号
方法描述
1
void clear( ) 将此Hashtable清空,使其不包含任何键
2
Object clone( ) 创建此Hashtable的浅表副本
3
boolean contains(Object value) 测试此Hashtable中是否存在与指定值关联的键
4
boolean containsKey(Object key) 测试指定对象是否为此Hashtable中的键
5
boolean containsValue(Object value) 如果此 Hashtable 将一个或多个键映射到此值,则返回 true
6
Enumeration elements( ) 返回此Hashtable中的值的枚举
7
Object get(Object key) 返回指定键所映射到的值,如果此映射不包含此键的映射,则返回 null. 更确切地讲,如果此映射包含满足(key.equals(k))的从键 k 到值 v 的映射,则此方法返回 v,否则,返回 null
8
boolean isEmpty( ) 测试此Hashtable是否没有键映射到值
9
Enumeration keys( ) 返回此Hashtable中的键的枚举
10
Object put(Object key, Object value) 将指定 key 映射到此Hashtable中的指定 value
11
void rehash( ) 增加此Hashtable的容量并在内部对其进行重组,以便更有效地容纳和访问其元素
12
Object remove(Object key) 从Hashtable中移除该键及其相应的值
13
int size( ) 返回此Hashtable中的键的数量
14
String toString( ) 返回此 Hashtable 对象的字符串表示形式,其形式为 ASCII 字符 ", "(逗号加空格)分隔开的,括在括号中的一组条目
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.util.*;public class HashTableDemo { public static void main (String args[]) { Hashtable balance = new Hashtable(); Enumeration names; String str; double bal; balance.put("Zara" , new Double(3434.34 )); balance.put("Mahnaz" , new Double(123.22 )); balance.put("Ayan" , new Double(1378.00 )); balance.put("Daisy" , new Double(99.22 )); balance.put("Qadir" , new Double(-19.08 )); names = balance.keys(); while (names.hasMoreElements()) { str = (String) names.nextElement(); System.out.println(str + ": " + balance.get(str)); } System.out.println(); bal = ((Double)balance.get("Zara" )).doubleValue(); balance.put("Zara" , new Double(bal+1000 )); System.out.println("Zara's new balance: " + balance.get("Zara" )); } }
1 2 3 4 5 6 7 Qadir : -19 .08 Zara : 3434 .34 Mahnaz : 123 .22 Daisy : 99 .22 Ayan : 1378 .0 Zara 's new balance: 4434 .34
TreeMap
TreeMap 是一个有序的 key-value 集合,非同步,基于红黑树(Red-Black tree)实现,每一个 key-value 节点作为红黑树的一个节点。
放入TreeMap的元素,必须实现Comparable接口,如果没有实现Comparable接口,则必须在创建 TreeMap 时传入自定义的 Comparator对象,TreeMap 会自动对元素的进行排序。
TreeMap 中判断相等的标准是:两个 key 通过equals()方法返回为 true,并且通过compare()方法比较应该返回为 0
要严格按照compare()规范实现比较逻辑,否则,TreeMap将不能正常工作,如果使用自定义的类来作为 TreeMap 中的 key 值,且想让 TreeMap 能够良好的工作,则必须重写自定义类中的equals()方法。
key排序
TreeMap默认是升序的,如果我们需要改变排序方式,则需要使用比较器:Comparator,Comparator可以对集合对象或者数组进行排序的比较器接口,实现该接口的public compare(T o1,To2)方法即可实现排序,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TreeMapTest { public static void main(String [] args) { Map <String , String > map = new TreeMap<String , String >( new Comparator<String >() { public int compare(String obj1, String obj2) { return obj2.compareTo(obj1); } }); map.put("b" , "ccccc" ); map.put("d" , "aaaaa" ); map.put("c" , "bbbbb" ); map.put("a" , "ddddd" ); Set <String > keySet = map.keySet(); Iterator <String > iter = keySet.iterator(); while (iter.hasNext()) { String key = iter.next(); System.out.println(key + ":" + map.get (key)); } } }
1 2 3 4 d:aaaaa c:bbbbb b:ccccc a:ddddd
value排序
上面例子是对根据TreeMap的key值来进行排序的,但是有时我们需要根据TreeMap的value来进行排序,对value排序我们就需要借助于Collections的sort(List<T> list, Comparator<? super T> c)方法,该方法根据指定比较器产生的顺序对指定列表进行排序,但是有一个前提条件,那就是所有的元素都必须能够根据所提供的比较器来进行比较,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class TreeMapTest { public static void main(String [] args) { Map <String , String > map = new TreeMap<String , String >(); map.put("a" , "ddddd" ); map.put("c" , "bbbbb" ); map.put("d" , "aaaaa" ); map.put("b" , "ccccc" ); List <Map .Entry<String ,String >> list = new ArrayList<Map .Entry<String ,String >>(map.entrySet()); Collections.sort(list,new Comparator<Map .Entry<String ,String >>() { public int compare(Entry<String , String > o1, Entry<String , String > o2) { return o1.getValue().compareTo(o2.getValue()); } }); for (Map .Entry<String ,String > mapping:list){ System.out.println(mapping.getKey()+":" +mapping.getValue()); } } }
1 2 3 4 d:aaaaa c:bbbbb b:ccccc a:ddddd
EnumMap
如果Map作为key的对象是enum类型,那么,还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
我们以DayOfWeek这个枚举类型为例,为它做一个"翻译”功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Main { public static void main (String[] args) { Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class); map.put(DayOfWeek.MONDAY, "星期一" ); map.put(DayOfWeek.TUESDAY, "星期二" ); map.put(DayOfWeek.WEDNESDAY, "星期三" ); map.put(DayOfWeek.THURSDAY, "星期四" ); map.put(DayOfWeek.FRIDAY, "星期五" ); map.put(DayOfWeek.SATURDAY, "星期六" ); map.put(DayOfWeek.SUNDAY, "星期日" ); System.out.println(map); System.out.println(map.get(DayOfWeek.MONDAY)); } }
使用EnumMap的时候,总是用Map接口来引用它,因此,实际上把HashMap和EnumMap互换,在客户端看来没有任何区别。
Properties
Java集合库提供了一个Properties来表示一组"配置”,由于历史遗留原因,Properties内部本质上是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口。
读取配置文件
用Properties读取配置文件非常简单,Java默认配置文件以.properties为扩展名,每行以key=value表示,以#课开头的是注释,以下是一个典型的配置文件:
1 2 3 4 last_open_file =/data/hello.txt auto_save_interval =60
可以从文件系统读取这个.properties文件:
1 2 3 4 5 6 String f = "setting.properties" ; Properties props = new Properties(); props.load(new java.io.FileInputStream(f)); String filepath = props.getProperty("last_open_file" ); String interval = props.getProperty("auto_save_interval" , "120" );
可见,用Properties读取配置文件,一共有三步:
创建Properties实例。
调用load()读取文件。
调用getProperty()获取配置。
调用getProperty()获取配置时,如果key不存在,将返回null,我们还可以提供一个默认值,这样,当key不存在的时候,就返回默认值。
也可以从classpath读取.properties文件,因为load(InputStream)方法接收一个InputStream实例,表示一个字节流,它不一定是文件流,也可以是从jar包中读取的资源流:
1 2 Properties props = new Properties(); props.load(getClass().getResourceAsStream("/common/setting.properties" ));
如果有多个.properties文件,可以反复调用load()读取,后读取的key-value会覆盖已读取的key-value:
1 2 3 Properties props = new Properties(); props.load(getClass().getResourceAsStream("/common/setting.properties" )); props.load(new FileInputStream("C:\\conf\\setting.properties" ));
上面的代码演示了Properties的一个常用用法:可以把默认配置文件放到classpath中,然后,根据机器的环境编写另一个配置文件,覆盖某些默认的配置。
注意 :Properties设计的目的是存储String类型的key-value,但Properties实际上是从Hashtable派生的,它的设计实际上是有问题的,但是为了保持兼容性,现在已经没法修改了,除了getProperty()和setProperty()方法外,还有从Hashtable继承下来的get()和put()方法,这些方法的参数签名是Object,我们在使用Properties的时候,不要去调用这些从Hashtable继承下来的方法。
写入配置文件
如果通过setProperty()修改了Properties实例,可以把配置写入文件,以便下次启动时获得最新配置,写入配置文件使用store()方法:
1 2 3 4 Properties props = new Properties(); props.setProperty("url" , "http://www.liaoxuefeng.com" ); props.setProperty("language" , "Java" ); props.store(new FileOutputStream("C:\\conf\\setting.properties" ), "这是写入的properties注释" );
编码
早期版本的Java规定.properties文件编码是ASCII编码(ISO8859-1),如果涉及到中文就必须用name=\u4e2d\u6587来表示,从JDK9开始,Java的.properties文件可以使用UTF-8编码了。
不过,需要注意的是,由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码,我们需要用另一个重载方法load(Reader)读取:
1 2 Properties props = new Properties(); props.load(new FileReader("settings.properties" , StandardCharsets.UTF_8));
InputStream和Reader的区别是一个是字节流,一个是字符流,字符流在内存中已经以char类型表示了,不涉及编码问题。
重写 equals 和 hashCode
正确使用Map必须保证。
作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true
作为key的对象还必须正确覆写hashCode()方法,因为通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数,HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value,且hashCode()方法要严格遵循以下规范。
如果两个对象相等,则两个对象的hashCode()必须相等。
如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
即对应两个实例a和b
如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode()
如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。
注意 :
上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。
而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。
实例 :以Person类为例:
1 2 3 4 5 public class Person { String firstName; String lastName; int age; }
把需要比较的字段找出来,然后,引用类型使用Objects.equals()比较,基本类型使用==比较。
在正确实现equals()的基础上,我们还需要正确实现hashCode(),即上述3个字段分别相同的实例,hashCode()返回的int必须相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Person { String firstName; String lastName; int age; @Override int hashCode () { int h = 0 ; h = 31 * h + firstName.hashCode(); h = 31 * h + lastName.hashCode(); h = 31 * h + age; return h; } @Override public boolean equals (Object obj) { if (obj instanceof Person) { Person p = (Person) obj; return this .firstName == p.firstName && this .lastName == p.lastName && this .age == p.age; } return false ; } }
String类已经正确实现了hashCode()方法,我们在计算Person的hashCode()时,反复使用31*h,这样做的目的是为了尽量把不同的Person实例的hashCode()均匀分布到整个int范围。
和实现equals()方法遇到的问题类似,如果firstName或lastName为null,上述代码工作起来就会抛NullPointerException,为了解决这个问题,我们在计算hashCode()的时候,经常借助Objects.hash()来计算:
1 2 3 int hashCode () { return Objects.hash(firstName, lastName, age); }
编写equals()和hashCode()遵循的原则是:equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算,equals()中没有使用到的字段,绝不可放在hashCode()中计算。
另外注意,对于放入HashMap的value对象,没有任何要求。
补充 :关于equals和hashCode方法,很多Java程序都知道,但很多人也就是仅仅知道而已,在Joshua Bloch的大作《Effective Java》(很多软件公司,《Effective Java》,《Java编程思想》以及《重构:改善既有代码质量》)中是这样介绍equals方法的:
首先equals方法必须满足自反性(x.equals(x)必须返回true),对称性(x.equals(y)返回true时,y.equals(x)也必须返回true),传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false
实现高质量的equals方法的诀窍包括:
使用==操作符检查"参数是否为这个对象的引用"
使用instanceof操作符检查"参数是否为正确的类型"
对于类中的关键属性,检查参数传入对象的属性是否与之相匹配。
编写完equals方法后,问自己它是否满足对称性,传递性,一致性。
重写equals时总是要重写hashCode
不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。
HashMap/Hashtable/HashSet/LinkedHashMap/TreeMap 比较
Hashmap 是一个最常用的 Map,它根据键的 HashCode 值存储数据,HashMap 最多只允许一条记录的键为Null,允许多条记录的值为 Null
Hashtable 与 HashMap 类似,不同的是:它不允许记录的键或者值为空,是线程安全的,因此也导致了 Hashtale 的效率偏低。
LinkedHashMap 是 HashMap 的一个子类,如果需要输出的顺序和输入的相同,那么用 LinkedHashMap 可以实现。
TreeMap 实现 SortMap 接口,内部实现是红黑树,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的,TreeMap 不允许 key 的值为 null