该类特点是:
1.只有全等的key值,该类才会认为两个key值相等。比如new String(“11”) 与new String(“11”),这两个对象就不是全等,而一般的HashMap则认为上面两个对象是相等的。
2.并且该类非常有意思的是,在key-value数据的存储上,类似于HashMap,采用map数组进行存储,但是key-value不是利用链表解决冲突,而是继续计算下一个索引,把数据计算在下一个有效索引的数组中,也就是数据全部存储map数组中,并且table[i]=key 则table[i+1]=value。key-value紧挨着存储在map数组中。
3.内部通过数组存储键值对,相邻元素存在键值对。比如:i 位置是key,i+1位置是value
4.当hashcode相等,出现冲突的时候,通过线性探索发解决冲突问题
5.比较的是引用相等
IdentityHashMap与常用的HashMap的区别是:
前者比较key时是“引用相等”而后者是“对象相等”,即对于k1和k2,当k1==k2时,IdentityHashMap认为两个key相等,而HashMap只有在k1.equals(k2) == true 时才会认为两个key相等。
默认的加载因子为2/3,在重新哈希后,加载因子变为1/3.当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 reszie 方法将容量翻倍,重新进行哈希。增加桶数,重新哈希,可能相当昂贵。
先来个测试
//在IdentityHashMap中,是判断key是否为同一个对象,而不是普通HashMap的equals方式判断
@Test
public void testIdentityHashMap() {
String s1 = new String("one");
String s2 = new String("one");
System.out.println( s1.equals(s2)); //true
System.out.println( s1==s2 ); //false
Map map =new IdentityHashMap();
map.put(s1,"first");
map.put(s2,"second");
for (Map.Entry entry: map.entrySet()) {
System.out.println(entry.getKey() +",--" +entry.getValue());
//one,--first
//one,--second
}
System.out.println("idenMap="+map.containsKey("one")); //idenMap=false
System.out.println("idenMap="+map.get("one")); //idenMap=null
System.out.println("idenMap="+map.containsKey(s1)); //idenMap=true
System.out.println("idenMap="+map.get(s1)); //idenMap=first
System.out.println("idenMap="+map.containsKey(s2)); //idenMap=true
System.out.println("idenMap="+map.get(s2)); //idenMap=second
map.clear();
//++++++++++++++++++++++++++++++++++++++++++++++++++++
System.out.println("+++++++++++++++++++++++++++++++++++++++++");
map.put(s1,"first");
map.put(s1,"second");
for (Map.Entry entry: map.entrySet()) {
System.out.println(entry.getKey()+","+entry.getValue()); //one,second
}
System.out.println("idenMap="+map.containsKey(s1)); //idenMap=true
System.out.println("idenMap="+map.get(s1)); //idenMap=second
}
/**
* 没有参数构造器的初始容量。必须是二的幂。预期最大容量为负载因子的2/3,也就是21
*/
private static final int DEFAULT_CAPACITY = 32;
/**
* 采用默认容量的构造器,最大21
*/
public IdentityHashMap() {
init(DEFAULT_CAPACITY);
}
/**
* 设定容量大小的构造器
* 参数期望的最大容量expectedMaxSize不能为负数,否则异常。
* 参数expectedMaxSize并不代表实际的容量大小。
* 通过capacity(expectedMaxSize)方法我们会发现,实际的容量要大一些。
*/
public IdentityHashMap(int expectedMaxSize) {
if (expectedMaxSize < 0)
throw new IllegalArgumentException("expectedMaxSize is negative: "
+ expectedMaxSize);
init(capacity(expectedMaxSize)); //将参数期望的容量大小expectedMaxSize扩大1.5倍再初始化
}
/**
* 将参数期望的容量大小expectedMaxSize扩大1.5倍
* 之后按照比改制大的最小2进制数设定容量大小。
* 返回的result值是实际的容量大小。
*/
private int capacity(int expectedMaxSize) {
int minCapacity = (3 * expectedMaxSize)/2;
int result;
if (minCapacity > MAXIMUM_CAPACITY || minCapacity < 0) {
result = MAXIMUM_CAPACITY;
} else {
result = MINIMUM_CAPACITY;
while (result < minCapacity)
result <<= 1;
}
return result;
}
/**
* 极限容量是initCapacity值的三分之二,而数组长度确实initCapacity的两倍!
* 由此可见IdentityHashMap比HashMap耗费内存空间。
* 等于说,当用到数组长度的三分之一的时候就要进行扩容操作。
* 可见内存空间消耗有多大。
*/
private void init(int initCapacity) {
threshold = (initCapacity * 2)/3;
table = new Object[2 * initCapacity];
}
/**
* @param m the map whose mappings are to be placed into this map
* @throws NullPointerException if the specified map is null
*/
public IdentityHashMap(Map extends K, ? extends V> m) {
// Allow for a bit of growth
this((int) ((1 + m.size()) * 1.1));
putAll(m);
}
通过构造器我们知道,IdentityHashMap在初始化的时候就已经构造比较大的map数组以解决可能的冲突问题,以便将数据都存储在数组中。同时为了提高查询效率,极限容量设置的比较小,只有数组长度的三分之一。但是问题也来了,
占用了太大的内存空间。也就是有效利用的空间不足数组总长度的三分之一。
1.put方法
该方法最为重要。通过该方法我们知道,IdentityHashMap类在存放key-value对时,不采用链表解决冲突,而是通过nextKeyIndex(i,len)方法找到下一个存放数据的索引值,如果该索引处没有值则存放数据,如果有值,继续nextKeyIndex(i,len)进行查找直到在数组中找到合适的存放位置。同时,如果找到全等的key值,说明已经存放过key,则用新value值替换旧value值。put方法最后,进行resize检查。如果存放下一个数据的长度>=极限容量,则进行扩容。通过put方法,我们可以了解到为什么在初始化table数组的时候,把数组长度定义为设计容量的两倍了。在init()方法中,我们知道,极限容量设计为数组长度的三分之一,说明,当存放的数组达到数组长度的三分之一的时候就要进行扩容。可见IdentityHashMap在内存空间中的消耗有多大。
public V put(K key, V value) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
Object item;
while ( (item = tab[i]) != null) {
if (item == k) {
V oldValue = (V) tab[i + 1];
tab[i + 1] = value;
return oldValue;
}
i = nextKeyIndex(i, len);
}
modCount++;
tab[i] = k;
tab[i + 1] = value;
if (++size >= threshold)
resize(len); // len == 2 * current capacity.
return null;
}
扩容方法:扩展为原来数组长度的两倍。扩容之后进行数据的转移,拷贝到新数组当中。循环内部手动进行了数据的清除,设置旧数组中的无用引用为null.个人理解原因是:数组长度较大,占用内存空间比较大,及时释放内存空间是王道!由于扩容之后空间一定够用,所以直接将原来数组中的数据存放到新数组对应的位置即可。并且数组存放不存在链表,只是数组中,所以管理起来比较方便。
private void resize(int newCapacity) {
// assert (newCapacity & -newCapacity) == newCapacity; // power of 2
int newLength = newCapacity * 2;
Object[] oldTable = table;
int oldLength = oldTable.length;
if (oldLength == 2*MAXIMUM_CAPACITY) { // can't expand any further
if (threshold == MAXIMUM_CAPACITY-1)
throw new IllegalStateException("Capacity exhausted.");
threshold = MAXIMUM_CAPACITY-1; // Gigantic map!
return;
}
if (oldLength >= newLength)
return;
Object[] newTable = new Object[newLength];
threshold = newLength / 3;
for (int j = 0; j < oldLength; j += 2) {
Object key = oldTable[j];
if (key != null) {
Object value = oldTable[j+1];
oldTable[j] = null;
oldTable[j+1] = null;
int i = hash(key, newLength);
while (newTable[i] != null)
i = nextKeyIndex(i, newLength);
newTable[i] = key;
newTable[i + 1] = value;
}
}
table = newTable;
}
其他方法
/**
* putAll方法,使用put方法进行数据的复制。
* 没什么说的。原理同上。
*/
public void putAll(Map extends K, ? extends V> m) {
int n = m.size();
if (n == 0)
return;
if (n > threshold) // conservatively pre-expand
resize(capacity(n));
for (Entry extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
/**
* Circularly traverses table of size len.
* 计算下一个key值出现在数组处的索引值
*/
private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}
通过上面的方法我们知道,该类在完成key-value对的存放时,是挨着存放key-value对到数组中。以步进2为间隔进行数据填充。
3、查询获取数据方法
/**
* 通过key值获取value对象。
* 通过程序会发现,item==k,说明只有当全等的时候
* 才会返回对象,否则找不到value值返回null。
* 同时,item==k的情况下,tab[i+1]为value值。
* 说明数组i处存放key值,i+1处存放value值。
* 这也解释了初始化数组时候2倍长度的原因了。
* @see #put(Object, Object)
*/
public V get(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k)
return (V) tab[i + 1];
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}
/**
* 是否包含指定的key值。与上面的get(key)方法类似
* 原理同上。
*/
public boolean containsKey(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k)
return true;
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
/**
* 是否包含指定的value值。
* 循环中i以2位步长进行循环遍历
* 说明数组偶数处存放value值。
* IdentityHashMap iden =
new IdentityHashMap<>();
iden.put(null, null);
iden.put(null, null);
System.out.println(iden);
System.out.println(iden.containsKey(null));
System.out.println(iden.containsValue(null));
上面的运行结果是:
{null=null}
true
true
* 说明存放的全等的key值会替换原来的value值。
* 存放的key==null的情况下,会通过maskKey(key)方法
* 将key=null的值,替换为NULL_KEY对象。
* 请结合put方法查看代码。
*/
public boolean containsValue(Object value) {
Object[] tab = table;
for (int i = 1; i < tab.length; i += 2)
if (tab[i] == value && tab[i - 1] != null)
return true;
return false;
}
/**
* 是否包含指定的key-value对。
* 原理和上面的containsKey containsValue类似
*/
private boolean containsMapping(Object key, Object value) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k)
return tab[i + 1] == value;
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
上面三个是查询map数组中数据的方法,上面的方法依次通过获取key在map数组中的索引进行查询,知道查询到结果为止。
4、删除数据
/**
* 使用全等的方式比较key值。
* 通过key值,得到hash值,然后得到索引值。
* 如果存在key值,则删除对应位置上的数据。同时size-1.
* 并且使用closeDeletion(i)将数组后面的数据重新remap
* 存放在数组的相应位置当中。
*/
public V remove(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k) {
modCount++;
size--;
V oldValue = (V) tab[i + 1];
tab[i + 1] = null;
tab[i] = null;
closeDeletion(i);
return oldValue;
}
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}
/**
* 删除指定的key-value对。
* 原理同上面的remove类似。
* 通过key值得到hash值,然后得到key值在数组中的索引值
* 有了索引值一一比较key值,只有和目标key值==时,才会删除
* 该索引处的数据。
* 删除之后,通过closeDeletion(i)方法将后面的数据重新rehash
* 重新在数组中进行存放。
* 如果没有找到==的key值,则返回false,说明删除不成功。
*/
private boolean removeMapping(Object key, Object value) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k) {
if (tab[i + 1] != value)
return false;
modCount++;
size--;
tab[i] = null;
tab[i + 1] = null;
closeDeletion(i);
return true;
}
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
/**
* 该方法是在删除数组中的数据的时候重新rehash数组中的元素
* 重新存放在数组中。
* 关于if语句中的判断有点小麻烦。
* 这个判断需要了解hash值的计算。
* 不过不影响我们大概知道是怎么回事。
* 同时我们也可以知道的是,删除元素并不会使数组的长度减小。
* 这一点比较重要。
*/
private void closeDeletion(int d) {
Object[] tab = table;
int len = tab.length;
Object item;
for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
i = nextKeyIndex(i, len) ) {
int r = hash(item, len);
if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
tab[d] = item;
tab[d + 1] = tab[i + 1];
tab[i] = null;
tab[i + 1] = null;
d = i;
}
}
}
由于IdentityHashMap采用在数组中保存key-value数据,并以加长的数组来解决可能引起的冲突,所以数据删除起来比较方便,只不过只有全等的情况下,才会删除key值所对应的value。同时,由于删除一对数据之后导致后面的数据遍历不到,所以当删除一对数据之后,需要对后面的数据重写在map数组上面定位。
5 相等方法和hashcode方法
/**
* 比较两个IdentityHashMap对象是否相等
* equals方法可以比较Map类对象,只要entrySet().equals(m.entrySet())返回
* true就可以了。
* 1、如果参数o属于IdentityHashMap类对象,直接使用containsMapping方法逐一
*比较每个key-value对数据是否相等即可。
* 2、如果参数o不属于containsMapping类对象,则使用entrySet()方法得到的
* EntrySet集合进行比较。
* 通过查找该类中的EntrySet内部类,没有equals方法,说明复用父类AbstractSet
* 中的equals方法,通过查找父类的equals方法,我们发现,
* AbstractSet类又调用了AbstractCollection类的
* containsAll(Collection> c)方法。经过一系列的调用
* 最终通过比较的是参数o集合的每个数据的(o.equals(it.next()))方法
* 进行判断的。
* 也就是说,如果参数o不是IdentityHashMap类对象,则使用参数o的equals方法
* 进行比较,不要求==全等。
*/
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof IdentityHashMap) {
IdentityHashMap m = (IdentityHashMap) o;
if (m.size() != size)
return false;
Object[] tab = m.table;
for (int i = 0; i < tab.length; i+=2) {
Object k = tab[i];
if (k != null && !containsMapping(k, tab[i + 1]))
return false;
}
return true;
} else if (o instanceof Map) {
Map m = (Map)o;
return entrySet().equals(m.entrySet());
} else {
return false; // o is not a Map
}
}
/**
* 计算hash值。
* 最终的hash值与key和value都有关
* 保证每个key-value对的hash值是唯一的。
*/
public int hashCode() {
int result = 0;
Object[] tab = table;
for (int i = 0; i < tab.length; i +=2) {
Object key = tab[i];
if (key != null) {
Object k = unmaskNull(key);
result += System.identityHashCode(k) ^
System.identityHashCode(tab[i + 1]);
}
}
return result;
}
这两个方法是该类的关键所在,hashcode的计算不仅仅和key值有关,而且和value值有关,这样就保证了key-value对具备唯一的hash值。同时通过重写equals方法,判定只有key值全等情况下才会判断key值相等。这就是IdentityHashMap与普通HashMap不同的关键所在。
1、介绍
WeakHashMap采用弱引用队列关联map数组中存储的数据,该类与普通HashMap类似,解决冲突一样采用链表解决。了解了HashMap再来了解WeakHashMap会很容易上手。之所以采用采用WeakHashMap该类,是因为通过该类可是实现缓存,在内存空间很紧张情况下,使用该类,避免强引用占用大量的内存,销毁掉不用或者过时的对象,较早的释放空间。
该类主要的特点就是使用引用队列,将Entry对象与引用队列关联起来,使得每个Entry对象都是弱引用,先看内部类Entry的定义
private static class Entry extends WeakReference
这是WeakHashMap类中内部类Entry的定义,这也就是它与普通HashMap不同的关键所在。Entry构造器中,将每个Entry与引用队列关联,而每个Entry对象就是一个弱引用!!!这个弱引用指向key值。
/**
* Expunges stale entries from the table.
* 该方法是WeakHashMap类的核心方法
* 每次在进行getSize() getTable()方法是都要调用该方法
* 该方法实现的功能是:
* 通过遍历引用队列当中保存的已经回收的弱引用对象
* 将原map 数组中的引用清除,map数组中只保留还没有回收的弱引用对象。
* queue.poll()弹出的是弱引用对象,该类中的Entry集成了WeakReference类
* 方法中利用两层循环:一层循环遍历引用队列中的值,另一层循环遍历
* map数组中的值,当在map数组中发现由于引用队列中相同的引用
* 则把应用变量从map数组中删除。更新map数组长度
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry e = (Entry) x;
int i = indexFor(e.hash, table.length);
Entry prev = table[i];
Entry p = prev;
while (p != null) {
Entry next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
EnumMap是与枚举类相结合的Map类。跟hash没有多大关系。EnumMap就是专门与枚举类结合形成Map的key-value对结构。值的注意的是,EnumMap中虽然也存储的是key-value对的数据,但是内存实现上却采用的是数组结构。key存储一个数组结构,value也用一个对应的数组结构。
private enum Season
{
SPRING,SUMMER,FALL,WINTER
}
@Test
public void testEnumMap() {
//创建一个EnumMap对象,该EnumMap的所有key必须是Season枚举类的枚举值
EnumMap enumMap = new EnumMap(Season.class);
enumMap.put(Season.SUMMER,"夏日炎炎");
enumMap.put(Season.SPRING,"春暧花开");
System.out.println(enumMap); //{SPRING=春暧花开, SUMMER=夏日炎炎}
}
构造器
public EnumMap(Class keyType) {
this.keyType = keyType;
//保存枚举类的所有枚举值到数组中
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
public EnumMap(EnumMap m) {
keyType = m.keyType;
keyUniverse = m.keyUniverse;
vals = m.vals.clone();
size = m.size;
}
public EnumMap(Map m) {
//如果m类是EnumMap类,与上面的构造器一样
if (m instanceof EnumMap) {
EnumMap em = (EnumMap) m;
keyType = em.keyType;
keyUniverse = em.keyUniverse;
vals = em.vals.clone();
size = em.size;
} else {
//如果不是EnumMap类,则不允许vals数组长度为空
if (m.isEmpty())
throw new IllegalArgumentException("Specified map is empty");
keyType = m.keySet().iterator().next().getDeclaringClass();
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
putAll(m);
}
}
上面是EnumMap的三个构造器,有构造器知道,该类存储的key-value对,key值类型必须是枚举类型。枚举类型的值通过该方法getKeyUniverse(keyType)获取枚举类数组,keyUniverse 数组存放key集合。vals数组存放value数据。
put方法
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
private void typeCheck(K key) {
Class keyClass = key.getClass();
if (keyClass != keyType && keyClass.getSuperclass() != keyType)
throw new ClassCastException(keyClass + " != " + keyType);
}
put方法存入key-value对。由于key值是固定的枚举类的常量值,并且在构造器初始化的过程中,已经获取到了,这里put方法中在此传入key值,是为了vals数组中保存value值。
public void putAll(Map extends K, ? extends V> m) {
if (m instanceof EnumMap) {
EnumMap extends K, ? extends V> em =
(EnumMap extends K, ? extends V>)m;
if (em.keyType != keyType) {
if (em.isEmpty())
return;
throw new ClassCastException(em.keyType + " != " + keyType);
}
for (int i = 0; i < keyUniverse.length; i++) {
Object emValue = em.vals[i];
if (emValue != null) {
if (vals[i] == null)
size++;
vals[i] = emValue;
}
}
} else {
/**
* 这里的putAll(m)会调用父类的方法
* 父类该方法的实现上,均采用一一放入的put方法进行
* 数据的存入。
* 此时会调用put(e.getKey(), e.getValue());
* 子类put方法会被调用,所以回到了该类中的put方法
* put方法中会检验key值的类型,如果不匹配就不能放入,
* 抛出异常
*/
super.putAll(m);
}
}
keyUniverse就是初始化时保存的枚举类常量值数组。通过上面的方法,非常简单保存集合中的数据。并且EnumMap采用数组保存key-value对,管理起来很方便,逻辑不复杂。
get方法
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum)key).ordinal()]) : null);
}
/**
* Returns true if key is of the proper type to be a key in this
* enum map.
* 检查key值是否是有效值
* key值不能为null
* 同时key的类型必须是初始化时的枚举类类型或者
* 其子类,否则就是无效值
*/
private boolean isValidKey(Object key) {
if (key == null)
return false;
// Cheaper than instanceof Enum followed by getDeclaringClass
Class keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}
get方法很简单,只要key传入的合理的参数,很快就能得到结果。O(1)的时间复杂度。通过上面的put方法和get方法,结合起来看,EnumMap类进行key-value保存的时候,key专门用一个数组进行保存,然后vaule值存放在数组vals的相对应的位置上。当需要获取指定key的value时,直接通过key即可得到数组vals对应位置上的值。并且不会出现冲突的问题。因为Enum类的各个枚举值不相同。
public V remove(Object key) {
if (!isValidKey(key))
return null;
int index = ((Enum)key).ordinal();
Object oldValue = vals[index];
vals[index] = null;
if (oldValue != null)
size--;
return unmaskNull(oldValue);
}
private boolean removeMapping(Object key, Object value) {
if (!isValidKey(key))
return false;
int index = ((Enum)key).ordinal();
if (maskNull(value).equals(vals[index])) {
vals[index] = null;
size--;
return true;
}
return false;
}
/**
* Returns true if key is of the proper type to be a key in this
* enum map.
* 检查key值是否是有效值
* key值不能为null
* 同时key的类型必须是初始化时的枚举类类型或者
* 其子类,否则就是无效值
*/
private boolean isValidKey(Object key) {
if (key == null)
return false;
// Cheaper than instanceof Enum followed by getDeclaringClass
Class keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}
删除方法很简单,key值是不会删除的,因为key值是枚举类常量值,只会把vals数组中对应位置上的value值删除。
/**
* @param key the key whose presence in this map is to be tested
* @return true if this map contains a mapping for the specified
* 首先检验key值是否是有效值
* 然后获取对应的key值的value值是否为null
* 这里需要注意的是,put方法存放key-value对的时候,
* 如果value值为null,则会通过maskNull()方法将null值,转换成NULL对象
*所以实际存入vals数组中的value值不为null值。
*/
public boolean containsKey(Object key) {
return isValidKey(key) && vals[((Enum)key).ordinal()] != null;
}
private boolean containsMapping(Object key, Object value) {
return isValidKey(key) &&
maskNull(value).equals(vals[((Enum)key).ordinal()]);
}
上面两个方法看明白的话,就很容易明白EnumMap类是如何进行key-value对的管理的。key值初始化的时候已经获取,保存在了数组中,value值是通过put或者putAll方法添加到数组中的,containsKey(Object key)方法不是通过key值数组判断的,而是通过key值对应位置上的value值是否存在进行判断的。也就是说key值对应的vals数组中是否有值,决定了EnumMap类对象是否包含key值。