HashMap 初探

作者: Real_man | 来源:发表于2018-03-15 15:52 被阅读210次

    概览

    这篇文章,我们打算探索一下Java集合(Collections)框架中Map接口中HashMap的实现。Map虽然是Collctions框架的一部分,但是Map并没有实现Collection接口,而Set和List是实现Collection接口的。

    简单来说,HashMap主要通过key存储value值,并且提供了添加,获取和操作存储value的方法。HashMap的实现基于HashTable。

    HashMap内部呈现

    Key-value对在内部是以buckets的方式存储在一起,最终成为一个表。存储和检索操作的时间是固定的,也就是时间复杂度为O(1)。

    这篇文章暂时不过于涉及HashMap的底层,我们先对HashMap有个整体认知。

    put方法

    Map中通过put方法来存储一个value。

        /**
         * 建立键值对应关系,如果之前已经存在对应的key,
         * 返回之前存储的value,之前如果不存在对应的key,返回null
         */
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    

    知识点一: 当Map的调用put方法的时候,key对象被调用hashCode()方法,获得一个hash值供hashmap使用。
    我们创建一个对象来证实一下。

    public class MyKey {
        private int id;
        
        @Override
        public int hashCode() {
            System.out.println("调用 hashCode()");
            return id;
        }
    
        // constructor, setters and getters 
    }
    
       @Test
        public void mapKeyTest(){
            HashMap<MyKey,String> map = new HashMap<MyKey, String>();
            String retV = map.put(new MyKey(1),"value1");
        }
    

    可以看到控制台的输出信息

    调用 hashCode()
    

    知识点二: hash()方法计算出的hash值可以标识它在buckets数组中的索引位置。

    HashMap的hash()方法如下:可以与put方法进行关联。

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    HashMap有一个特点,它可以存储null的key和null的value。当key时null的时候,执行put方法,它会自动分配hash为0. 这也意味着key为null的时候没有hash操作,这样就避免了空指针异常。

    get() 方法

    为了获取存储在hashMap中的对象,我们需要知道与它对应的key。然后通过get方法把对应的key传到参数里。调用HashMap的get方法的时候,也会调用key对象的hashCode方法

        @Test
        public void mapKeyTest(){
            HashMap<MyKey,String> map = new HashMap<MyKey, String>();
            MyKey key1 = new MyKey(1);
            map.put(key1,"value1");
            String retV =  map.get(key1);
        }
    

    控制台上可以看到两行输出

    调用 hashCode()
    调用 hashCode()
    

    HashMap中的集合视图

    HashMap提供了三种方式,让我们可以把key和value作为其它集合来使用。

    Set<K> keys = map.keySet()
    Collection<V> values = map.values()
    Set<Entry<K, V>> entries = map.entrySet();
    

    注意: 在iteators创建完毕后,对map的任何结构修改,都会抛出一个异常。

    @Test
    public void givenIterator_whenFailsFastOnModification_thenCorrect() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "baeldung");
        map.put("type", "blog");
     
        Set<String> keys = map.keySet();
        Iterator<String> it = keys.iterator();
        map.remove("type");
        while (it.hasNext()) {
            String key = it.next();
        }
    }
    
    // 会抛出java.util.ConcurrentModificationException异常
    

    HashMap中唯一允许的修改是在iterator中移除元素。

    public void givenIterator_whenRemoveWorks_thenCorrect() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "baeldung");
        map.put("type", "blog");
     
        Set<String> keys = map.keySet();
        Iterator<String> it = keys.iterator();
     
        while (it.hasNext()) {
            it.next();
            it.remove();
        }
     
        assertEquals(0, map.size());
    }
    
    

    HashMap在iterator上的性能相比于LinkedHashMap和treeMap,性能非常糟糕。最差情况下为O(n),n为hashmap中条目的个数。

    HashMap性能

    HashMap的性能主要有两个参数影响,初始容量和负载因子。初始容量为Map底层桶数组的长度,负载因子为当桶容量的长度为多大的时候,重新开辟新的空间。

        int threshold;
        final float loadFactor;
    

    默认的初始容量为16,默认的负载因子为0.75. 我们也可以自定义它们的值。

    Map<String,String> hashMapWithCapacity=new HashMap<>(32);
    Map<String,String> hashMapWithCapacityAndLF=new HashMap<>(32, 0.5f);
    

    初始容量:

    大的初始容量用于条目数较多,但是少量迭代(iteration)
    小的初始容量用于条目数较少,但是多次迭代(iteration)

    负载因子:
    0.75是一个很折衷的方案了。在我们初始化HashMap的时候,初始容量和负载因子都应该考虑在内,比如为了减少重新hash的操作,初始容量乘以负载因子应该大于能存储的最大条目数,这样就不会发生重新hash的操作。

    最后

    HashMap内部有很多东西值得探索,这篇仅仅对HashMap做了一层表面的分析。接下来会深入分析。

    百度的面试题:

    HashMap的源码,实现原理 ,底层结构。

    参考

    相关文章

      网友评论

        本文标题:HashMap 初探

        本文链接:https://www.haomeiwen.com/subject/rbehqftx.html