先看看hashmap在整个Collection中的位置

HashMap中存储数据的结构是

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table;

上面的英文就不用说了。

原来基础的存储结构式Entry的数组!

至于Entry是HashMap的一个内部类

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    .....
}

看到里面的这个参数Entry<K,V> next大家应该都明白了,HashMap中每个Entry键值对都是一个链表!!!

下面我们看看map的put,get,iterator方法及遍历

put方法

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
    //计算key的hash  里面的实现比较麻烦 可以不用理会
        int hash = hash(key);
    //由hash码得到存储位置 计算方法是hash与table.length-1相与 这样的好处就是能保证要存放的位置肯定不会超过table的范围
    //前面的hash方法与indexFor 我没有仔细研究 不过大家可以认为 两个不同的hash会对应不同的存储位置
        int i = indexFor(hash, table.length);
    //e.next 链表
    //如果i的位置上已经有元素了 继续看for循环
    //否则就new一个新的Entry
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
        //如果要存储的key的hash值与已经存在在那个位置元素的key的hash值相等 并且两个key的内容也相等
        //话说这里我看的不是太懂  e本身是一个新的对象
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;        //以下两行代码到底想干什么?
                e.recordAccess(this);   //把要加入的值 给了e 是什么意思?
                return oldValue;        //返回的是之前已经存在的那个键值对里的value
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

   void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果相应的位置已经有东西了 并且总的容量也到了 就扩容
    //size threshold 这里面有点概念性的知识 大家看看源码就知道了
        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);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        //之前的那个位置的数据(不管有没有)转放到e里面
    //现在有个问题 什么时候table[bucketIndex]里面才有数据呢?
        Entry<K,V> e = table[bucketIndex];
    //看构造函数就知道 把e作为新entry的next 拉链法!!!
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    .....
    }

什么时候table[bucketIndex]里面才有数据呢? 换句话说拉链法是怎么实现的呢?我们先看下面的遍历。

hashmap的遍历

package iterator;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Iterator;
import java.util.HashMap;

import java.util.Collection;

/*
 * @desc 遍历HashMap的测试程序。
 *   (01) 通过entrySet()去遍历key、value,参考实现函数:
 *        iteratorHashMapByEntryset()
 *   (02) 通过keySet()去遍历key、value,参考实现函数:
 *        iteratorHashMapByKeyset()
 *   (03) 通过values()去遍历value,参考实现函数:
 *        iteratorHashMapJustValues()
 *
 * @author skywang
 */
public class HashMapIteratorTest {

    public static void main(String[] args) {
        int val = 0;
        String key = null;
        Integer value = null;
        Random r = new Random();
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        map.put("ss",12);  //第一次 会调用addEntry(hash, key, value, i);
        map.put("ss",13);  //第二次 会进入put方法里的for循环 然后直接return
        map.put("ss",12);  //第三次 会进入put方法里的for循环 然后直接return 那么什么时候才会使用拉链法呢
                       //当若干个key的内容不同但是hashCode相同时
        for (int i=0; i<12; i++) {
            // 随机获取一个[0,100)之间的数字
            val = r.nextInt(10);

            key = String.valueOf(val);
            value = r.nextInt(5);
            // 添加到HashMap中
            map.put(key, value);

        }

        // 通过entrySet()遍历HashMap的key-value
        iteratorHashMapByEntryset(map) ;

        // 通过keySet()遍历HashMap的key-value
        iteratorHashMapByKeyset(map) ;

        // 单单遍历HashMap的value
        iteratorHashMapJustValues(map);
    }

    /*
     * 通过entry set遍历HashMap
     * 效率高!
     */
    @SuppressWarnings("unchecked")
    private static void iteratorHashMapByEntryset(HashMap<String, Integer> map) {
        if (map == null)
            return ;

        System.out.println("\niterator HashMap By entryset");
        String key = null;
        Integer integ = null;
        Iterator<?> iter = map.entrySet().iterator();
        while(iter.hasNext()) {
            Map.Entry<String, Integer> entry = (Entry<String, Integer>)iter.next();

            key = (String)entry.getKey();
            integ = (Integer)entry.getValue();
            System.out.println(key+" -- "+integ.intValue());

        }
    }

    /*
     * 通过keyset来遍历HashMap
     * 效率低!
     */
    private static void iteratorHashMapByKeyset(HashMap<String, Integer> map) {
        if (map == null)
            return ;

        System.out.println("\niterator HashMap By keyset");
        String key = null;
        Integer integ = null;
        Iterator<String> iter = map.keySet().iterator();
        while (iter.hasNext()) {
            key = iter.next();
            integ = map.get(key);
            System.out.println(key+" -- "+integ.intValue());
        }
    }

    /*
     * 遍历HashMap的values
     */
    private static void iteratorHashMapJustValues(HashMap<String, Integer> map) {
        if (map == null)
            return ;

        Collection<Integer> c = map.values();
        Iterator<Integer> iter= c.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
       }
    }
}

拉链法

上面已经说了使用entrySet的方式效率高,大家以后就采用这个吧,另外还提到了什么时候用拉链法看下面这个例子

        HashMap<Person, Integer> map = new HashMap<Person, Integer>();
        map.put(new Person("dlf", 14),12);
        map.put(new Person("dlf", 15),12);
        map.put(new Person("sdfe", 16),12);

对于person这个类,我重写了hashCode方法,但是没有重写equals方法;

person的hashCode方法:

    public int hashCode() {
        int h = 0;
        if (name.equals("dlf")) {
            return 123456789;
        }
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

大家再看看hashMap里面的put方法

     

  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
        //如果要存储的key的hash值与已经存在在那个位置元素的key的hash值相等 并且两个key的内容也相等
        //话说这里我看的不是太懂  e本身是一个新的对象
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;        //以下两行代码到底想干什么?
                e.recordAccess(this);   //把要加入的值 给了e 是什么意思?
                return oldValue;        //返回的是之前已经存在的那个键值对里的value
            }
        }

当我

        map.put(new Person("dlf", 14),12);
        map.put(new Person("dlf", 15),12);

第二个person的hashcode与第一个person的hashcode是一样的,但是看看上面if条件句的第二部分

(k = e.key) == key || key.equals(k)

此时调用Object的equals方法也就是==,那么==比较的是什么呢?栈里面存储的地址值,两个新new处理的对象地址那自然不同的,所以if条件不满足,跳出for循环;

最后的结果是



iterator HashMap By entryset

dlf 15 -- 12

dlf 14 -- 12

sdfe 16 -- 12



iterator HashMap By keyset

dlf 15 -- 12

dlf 14 -- 12

sdfe 16 -- 12

12

12

12

get方法

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    // 获取key的hash值
    int hash = hash(key.hashCode());
    // 在“该hash值对应的链表”上查找“键值等于key”的元素             大家看到了 是在hash对应的位置查找,而不是查找整个table
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

iterator方法

我们就看效率最高的entrySet方法

        Set<Entry<Person, Integer>> set=map.entrySet();     //为了更清楚些 我分开写
        Iterator<?> iter = set.iterator();

最开始调用entrySet方法的时候,entrySet对象还为null,会调用new EntrySet;

待得到set集合后,会再次调用iterator方法

  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();
        }
        .......
    }

    Iterator<Map.Entry<K,V>> newEntryIterator()   {
        return new EntryIterator();
    }

大家看到了最后返回的Iterator是一个EntryIterator。

看EntryIterator的代码,它是继承了HashIterator;

 

   private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();             //这个方法在HashIterator中定义
        }
    }

随后我们在程序里调用

 

  Map.Entry<Person, Integer> entry = (Entry<Person, Integer>)iter.next();

 //以下为HashIterator类
     //构造函数
     //在我们new EntryIterator的时候 就已经调用这个其父类HashIterator的构造函数了
     //HashIterator的成员变量在构造函数里面 就已经指到table里第一个元素了
       HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
     final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;                     //这个next是构造函数里面就指到table里第一个元素了(第一个不为null的元素)
            if (e == null)
                throw new NoSuchElementException();

            // 先让next=e.next 然后才判断next是否为空
            if ((next = e.next) == null) {
            //从第一行调用来看
        //如果table的第一个Entry(其实就是一个单链表) 就只有一个元素(其next为空)
        //让next找到table的下一个元素
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

有了上面的if ((next = e.next) == null) 这一行,我们就不仅能遍历整个table,还能将table中某个entry中的所有元素也遍历了!不重复,不遗漏。

参考资料

http://www.cnblogs.com/skywang12345/p/3310835.html

我多说两句,上面的博客把java的整个Collection的源代码剖析了一遍,博客主人真乃牛人呀! 大家一定要去看看

HashMap二三事的更多相关文章

  1. Java并发编程二三事

    Java并发编程二三事 转自我的Github 近日重新翻了一下<Java Concurrency in Practice>故以此文记之. 我觉得Java的并发可以从下面三个点去理解: * ...

  2. linux杂记(十二?) 关于账号和密码的二三事

    关于密码的二三事 关于账号和密码的二三事 久了不更linux的相关知识,实在是懒得想内容点(纯粹是懒).那么今天就来谈谈关于linux密码和账号的重要概念. 假如你的主机遭到入侵,那么对方的第一个侵入 ...

  3. MySQL5.7关于密码二三事

    MySQL5.7关于密码二三事 第一个:update user set password=password('root') where user='root' and host='localhost' ...

  4. Java中的匿名内部类及内部类的二三事

    匿名内部类适合创建那些只需要使用一次的类,它的语法有些奇怪,创建匿名内部类会立即创建一个该类的实例,这个类定义立即消失,且不能重复使用. 定义匿名类的格式如下: new 实现接口() |父类构造器(实 ...

  5. Emacs 启动优化二三事

    Emacs 启动优化二三事 */--> div.org-src-container { font-size: 85%; font-family: monospace; } p {font-siz ...

  6. WinForm二三事(三)Control.Invoke&Control.BeginInvoke

    http://www.cnblogs.com/yuyijq/archive/2010/01/11/1643802.html 这个系列从2009年写到2010年,差点又成太监文.随着WPF/Silver ...

  7. iOS7下滑动返回与ScrollView共存二三事

    [转载请注明出处] = =不是整篇复制就算注明出处了亲... iOS7下滑动返回与ScrollView共存二三事 [前情回顾] 去年的时候,写了这篇帖子iOS7滑动返回.文中提到,对于多页面结构的应用 ...

  8. 一只代码小白git托管路上的二三事

    [经验]一只代码小白git托管路上的二三事 写在前面的话 寒假的时候,娄老师给我们布置了代码托管的作业,并要求把托管地址发给学委.因假期的时候没有带电脑回家,所以只是在手机上草草注册了,也稀里糊涂就将 ...

  9. java:Map借口及其子类HashMap二

    java:Map借口及其子类HashMap二 重点:所有的集合必须依赖Iterator输出 Map<String, Integer> map = new HashMap<String ...

随机推荐

  1. The Zen Programmer

    专注 何为专注 关于 休息 怎么睡觉 心无杂念 我的体会 自我分析 初学者心态 无我 不要设置职业目标 敏事慎言 正念 做自己的老板 玩物养志 结语 最近在研读Christian Grobmeier ...

  2. ROS(indigo) 语音工具 科大讯飞 百度 pocketsphinx julius rospeex 16.11.22更新 ROS中文语音

    ROS语音工具汇总,目前先给出链接,只用过一些简单的命令. 中文语音: 参考链接:使用科大讯飞库 1 http://www.ncnynl.com/archives/201611/1069.html 2 ...

  3. 理解性能的奥秘——应用程序中慢,SSMS中快(6)——SQL Server如何编译动态SQL

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(5)--案例:如何应对参数嗅探 我们抛开参数嗅探的话题,回到了本系列的最 ...

  4. spark下使用submit提交任务后报jar包已存在错误

    使用spark submit进行任务提交,离线跑数据,提交后的一段时间内可以application可以正常运行.过了一段时间后,就抛出以下错误: org.apache.spark.SparkExcep ...

  5. 源码篇——AsyncTask机制

    AsyncTask new AsyncTask<String,String,String>(){ // 运行在主线程中,做预备工作 onPreExecute(){ } // 运行在子线程中 ...

  6. linux添加环境变量(path)

    分为三步 1.sudo vim /etc/profile 2.export PATH="全路径:$PATH" 3.source /etc/profile 我的微信二维码如下,欢迎交 ...

  7. Gazebo與ROS版本說明

    使用哪种ROS / Gazebo版本的组合 介绍 本文档提供了有关将不同版本的ROS与不同版本的Gazebo结合使用的选项的概述.建议在安装Gazebo ROS包装之前阅读它.重要!简单的分析,快速和 ...

  8. XMPP系列(七)---获取群组列表

    上一篇介绍了如何创建群组,这一篇就介绍一下,如何获取自己的群组列表. 在上一篇有提到,如果我们创建的群组是公共的群组,那么获取自己的群组列表时,会获取到自己的群组列表和那些公共的群组.而实际做社交的应 ...

  9. 多线程之Java线程阻塞与唤醒

    线程的阻塞和唤醒在多线程并发过程中是一个关键点,当线程数量达到很大的数量级时,并发可能带来很多隐蔽的问题.如何正确暂停一个线程,暂停后又如何在一个要求的时间点恢复,这些都需要仔细考虑的细节.在Java ...

  10. Java并发框架——公平性

    所谓公平性指所有线程对临界资源申请访问权限的成功率都一样,不会让某些线程拥有优先权.通过前面的CLH Node FIFO学习知道了等待队列是一个先进先出的队列,那么是否就可以说每条线程获取锁时就是公平 ...