[转]Java手把手系列:使用HashMap的基本世界观(存储/获取/遍历/集合观)
原创
3. HashMap基本世界观
第二小节介绍了什么是哈希表以及Java里面对应的实现类HashMap,本小节就来看看Java里面的HashMap如何使用。
3.1 put/存储
3.1.1 基本用法
往HashMap里面存储一个数据,需要调用其方法:
V put(K key, V value)
put方法接收两个参数:key和value, key会用来计算hash得到其存储位置,value就是要存储的值。
当使用key将一个value添加到HashMap里面,这个key对象本身的hashCode函数会被调用来计算hash值,我们来做一个实验:
-
首先我们生成一个KeyObject的类,用来作为HashMap的key
-
然后在KeyObject的hashCode添加日志,看看调用put的时候会不会输出
public class KeyObject { private int id; @Override public int hashCode() { System.out.println("Calling hashCode() function"); return id; } public KeyObject(int id){ this.id = id; } }
我们会使用KeyObject来映射存储一个String的value值,如下:
public static void test_hashmap_put() { KeyObject key = new KeyObject(1); Map<KeyObject, String> map = new HashMap<>(); map.put(key, "val"); }
上面代码会把数据存入HashMap,本身没有任何其他逻辑,但是执行上述代码的时候,我们会得到输出:
Calling hashCode() function
这个是为什么呢?
3.1.2 put流程(hashCode与hash的计算)
上一小节说到:key对象本身的hashCode函数会被调用来计算hash值,我们一步一步来看put的实现:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到:
-
put会调用putVal
-
putVal里面会调用函数:hash(key)
来看看hash函数做了什么:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
原来如此:
-
hash函数会调用key的hashCode api用来计算hash值
-
这里还会与h本身右移之后做异或(后面来分析index的如何生成)
这个也是上面KeyObject的hashCode函数会被调用的原因。
细心的人会好奇,在putVal函数里面,key已经用来计算hash值了,为什么后面第二个参数还是要传入key呢?
good question!!!
那是因为:
-
Java HashMap计算得到index之后,key和value都一起被存了
-
还记得我们再将HashMap存储结构到时候说到:Java HashMap存储到元素叫Entry,key和value一起被放到Entry里面存起来了
3.1.3 要点总结(面试常问)
-
put的存储过程,key的hashCode如何使用的
-
HashMap里面如何存储key的
3.2 get/检索
3.2.1 基本用法
在HashMap里面获取一个元素,需要调用其方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get 一个元素需要知道它被存入HashMap的时候的key,我们来看一个简单的demo:
public void test_hashmap_get() { Map<String, String> map = new HashMap<>(); map.put("xiaoming", "清华大学"); String val = map.get("xiaoming"); System.out.println(val) }
-
我们先构造一个String->String的map
-
然后才使用key:xiaoming存入value:清华大学
-
然后使用get,传入key:xiaoming检索
-
最后输出的是“清华大学”
3.2.2 get的流程(与put类似)
我们从源码看到,get的时候,依旧会使用key来计算hash值,这个就很容易理解了:
前后的hash值肯定要一致,才能找到要获取的元素。
3.3 从集合角度看HashMap
我们前面说,HashMap不是继承自Collection接口,但是HashMap提供了几种接口,可以把HashMap当作collection来对待。
3.3.1 Key集合
我们可以通过方法:keySet 得到map里面所有的keys
public void test_hashmap_keyset() { Map<String, String> map = new HashMap<>(); map.put("name", "xm"); map.put("type", "student"); Set<String> keys = map.keySet(); System.out.println(keys.size()) System.out.println(keys.contains("name")); System.out.println(keys.contains("type")); }
通过keySet去修改key,同样会影响到HashMap本身,我们来实验以下:
public void test_hashmap_keyset_modify() { Map<String, String> map = new HashMap<>(); map.put("name", "xm"); map.put("type", "student"); //map size = 2 System.out.println(map.size()); Set<String> keys = map.keySet(); keys.remove("name"); //map size = 1 System.out.println(map.size()); }
3.3.2 Values集合
同样地,可以通过values()方法得到map里面所有的values
public void test_hashmap_values() { Map<String, String> map = new HashMap<>(); map.put("name", "xm"); map.put("type", "student"); Collection<String> values = map.values(); //size = 2 System.out.println(values.size()); //true System.out.println(values.contains("xm")); //true System.out.println(values.contains("student")); }
同样地,通过values去修改value,同样会影响到HashMap本身。
3.3.3 Key-Value集合
一般,我们可以通过entrySet获取key和values的pairs集合。
public void test_hashmap_entryset() { Map<String, String> map = new HashMap<>(); map.put("name", "xm"); map.put("type", "student"); Set<Entry<String, String>> entries = map.entrySet(); System.out.println(entries.size()); for (Entry<String, String> e : entries) { String key = e.getKey(); String val = e.getValue(); System.out.println(key); System.out.println(val); } }
3.3.4 关于Map集合的角度的叨叨说明
-
HashMap存储的元素,本身无序的,所以我们上面测试代码,都是无序的
-
上面得到的key/value/key-value集合的迭代器,都是fail-fast的,即:得到迭代器之后,再修改会报异常
-
fail-fast的情况下,只能通过迭代器的remove函数来修改集合
-
hashmap的迭代效率要比tree map和linked hash map低,后面的文章我们来对比。
3.4 HashMap的遍历
HashMap至少可以说出四种遍历方式
3.4.1 迭代器遍历少林篇
直接使用EntrySet的迭代器来遍历,直接获取key & value,代码如下:
public static void traverseByEntrySet{ Map<String,String> map = new HashMap(); Iterator iter = map.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Object key = entry.getKey(); Object val = entry.getValue(); } }
推荐使用这种方式,简单高效!
3.4.2 迭代器遍历峨眉派
使用keyset的迭代器来遍历,然后通过key获取value
public static void traverseByKeySet{ Map<String,String> map = new HashMap(); Iterator iter = map.keySet().iterator(); while (iter.hasNext()) { Object key = iter.next(); Object val = map.get(key); } }
不推荐这种方式,遍历了key,还要通过get去获取value,多此一举!
3.4.3 foreach遍历武当篇
可以使用EntrySet,然后使用foreach遍历这个colleciton
Map<String, String> map = new HashMap<String, String>(); for (Entry<String, String> entry : map.entrySet()) { entry.getKey(); entry.getValue(); }
3.4.4 foreach遍历青城派
和3.4.2 迭代器遍历峨眉派 一样的道理,获取keyset,foreach遍历keyset,然后使用get获取value
Map<String, String> map = new HashMap<String, String>();
for (String key : map.keySet()) {
map.get(key);
}
3.4.5 要点总结(面试常问)
面试常见问题:
四种方式有两种不推荐,效率也有差异,小伙伴你知道吗?
可以下去试试耗时
4. 小节
今天带来了HashMap的基本用法,但是里面的hash值和index如何转换的,它的容量、冲突等面试常见的还没有涉及,我们会在后面的文章一一道来。
微信扫一扫
关注该公众号