[转]Java手把手系列:使用HashMap的基本世界观(存储/获取/遍历/集合观)

原创 DrunkCoder 写bug咯 2019-09-21

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如何转换的,它的容量、冲突等面试常见的还没有涉及,我们会在后面的文章一一道来。

 
 

微信扫一扫
关注该公众号

posted @ 2020-09-04 10:15  AlamZ  阅读(186)  评论(0编辑  收藏  举报