一個簡單的例子帶你理解Hashmap

邵磊發表於2017-09-16

前言

我知道大家都很熟悉hashmap,並且有事沒事都會new一個,但是hashmap的一些特性大家都是看了忘,忘了再記,今天這個例子可以幫助大家很好的記住。

場景

使用者提交一張試卷答案到服務端,post報文可精簡為

[{"question_id":"100001","answer":"A"},{"question_id":"100002","answer":"A"},{"question_id":"100003","answer":"A"},{"question_id":"100004","answer":"A"}]複製程式碼

提交地址採用restful風格

http://localhost:8080/exam/{試卷id}/answer複製程式碼

那麼如何比對客戶端傳過來的題目就是這張試卷裡的呢,假設使用者偽造了試卷怎麼辦?

正常解決思路

  1. 得到試卷所有題目id的list
  2. 2層for迴圈比對題號和答案
  3. 判定分數

大概程式碼如下[他人程式碼]

//讀取post題目
for (MexamTestpaperQuestion mexamTestpaperQuestion : mexamTestpaperQuestions) {
    //通過考試試卷讀取題目選項物件
    MexamQuestionOption questionOption = mexamQuestionDao.findById(mexamTestpaperQuestion.getQuestionId());
          map1.put("questionid", mexamTestpaperQuestion.getQuestionId());
          map1.put("answer", mexamQuestionDao.findById(mexamTestpaperQuestion.getQuestionId()).getAnswer());
          questionAnswerList.add(map1);
          //將每題分add到一個List
}

//遍歷試卷內所有題目
for (Map<String, Object> stringObjectMap : list) {
    //生成每題結果物件
    mexamAnswerInfo = new MexamAnswerInfo();
    mexamAnswerInfo.setAnswerId(answerId);
    mexamAnswerInfo.setId(id);
    mexamAnswerInfo.setQuestionId(questionid);
    mexamAnswerInfo.setResult(anwser);
    for (Map<String, Object> objectMap : questionAnswerList) {
        if (objectMap.get("questionid").equals(questionid)) {
            //比較答案
            if (anwser.equals(objectMap.get("answer"))) {
                totalScore += questionOption.getScore();
                mexamAnswerInfo.setIsfalse(true);
            } else {
                mexamAnswerInfo.setIsfalse(false);
            }
        }
    }
    mexamAnswerInfoDao.addEntity(mexamAnswerInfo);
}複製程式碼

使用普通的2層for迴圈解決了這個問題,一層是資料庫裡的題目,一層是使用者提交的題目,這時候bug就會暴露出來,假設使用者偽造了1萬道題目或更多,服務端運算量將增大很多。

利用hashmap來解決

首先,看看它的定義

基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

主要看HashMap k-v均支援空值,我們何不將使用者提交了答案add到一個HashMap裡,其中題目id作為key,答案作為value,而且HashMap的key支援以字母開頭。我們只需要for迴圈試卷所有題目,然後通過這個map.put("題目id")就能得到答案,然後比較答案即可,因為HashMap的key是基於hashcode的形式儲存的,所以在程式中該方案效率很高。

思路:

  1. 將提交答案以questionid為key,answer為value加入一個hashmap
  2. for迴圈實體列表,直接比對答案
  3. 判分

程式碼如下:

        //拿到使用者提交的資料
        Map<String, String> resultMap = new HashMap<>();

        JSONArray questions = JSON.parseArray(params.get("questions").toString());
        for (int size = questions.size(); size > 0; size--) {
            JSONObject question = (JSONObject) questions.get(size - 1);
            resultMap.put(question.getString("questionid"), question.getString("answer"));
        }
        //拿到試卷下的所有試題
        List<MexamTestpaperQuestion> mexamTestpaperQuestions = mexamTestpaperQuestionDao.findBy(map);
        int totalScore = 0;
        for (MexamTestpaperQuestion mexamTestpaperQuestion : mexamTestpaperQuestions) {
            MexamQuestionOption questionOption = mexamQuestionDao.findById(mexamTestpaperQuestion.getQuestionId());
            MexamAnswerInfo mexamAnswerInfo = new MexamAnswerInfo();
            mexamAnswerInfo.setAnswerId(answerId);
            mexamAnswerInfo.setId(id);
            mexamAnswerInfo.setQuestionId(questionOption.getId());
            mexamAnswerInfo.setResult(resultMap.get(questionOption.getId()));
            //拿到試卷的id作為resultMap的key去查,能查到就有這個題目,然後比對answer,進行儲存
            if (questionOption.getAnswer().equals(resultMap.get(questionOption.getId()))) {
                mexamAnswerInfo.setIsfalse(true);
                totalScore += questionOption.getScore();
            } else {
                mexamAnswerInfo.setIsfalse(false);
            }
            mexamAnswerInfoDao.addEntity(mexamAnswerInfo);
        }複製程式碼

分析HashMap

先看看文件

大概翻譯為如下幾點

  1. 實現Map ,可克隆,可序列化
  2. 基於雜湊表的Map介面實現。
  3. 此實現提供所有可選的對映操作,並允許 空值和空鍵。(HashMap 類大致相當於Hashtable,除非它是不同步的,並且允許null)。
    這個類不能保證Map的順序; 特別是不能保證訂單在一段時間內保持不變。
  4. 這個實現為基本操作(get和put)提供了恆定時間的效能,假設雜湊函式在這些儲存桶之間正確分散元素。集合檢視的迭代需要與HashMap例項的“容量” (桶數)及其大小(鍵值對映數)成正比 。 因此,如果迭代效能很重要,不要將初始容量設定得太高(或負載因子太低)是非常重要的。

  5. HashMap的一個例項有兩個影響其效能的引數:初始容量和負載因子。 容量是在雜湊表中桶的數量,和初始容量是簡單地在建立雜湊表中的時間的能力。該 負載係數是的雜湊表是如何充分允許獲得之前它的容量自動增加的措施。當在雜湊表中的條目的數量超過了負載因數和電流容量的乘積,雜湊表被重新雜湊(即,內部資料結構被重建),使得雜湊表具有桶的大約兩倍。

那麼put邏輯是怎麼樣的呢?

HashMap的key在put時,並不需要挨個使用equals比較,那樣時間複雜度O(n),也就說HashMap內有多少元素就需要迴圈多少次。
而HashMap是將key轉為hashcode,關於hashcode的確可能存在多個string相同的hashcode,但是最終HashMap還會比較一次bucketIndex。bucketIndex是HashMap儲存k-v的位置,時間複雜度只有O(1)。

圖解

原始碼

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        // 以key的雜湊碼作為key  
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 處理key為null,HashMap允許key和value為null 
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //以樹形結構儲存
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //以連結串列形式儲存
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果是key已存在則修改舊值,並返回舊值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        //如果key不存在,則執行插入操作,返回null。
        afterNodeInsertion(evict);

        return null;
    }

}複製程式碼

put方法分兩種情況:bucket是以連結串列形式儲存的還是以樹形結構儲存的。
如果是key已存在則修改舊值,並返回舊值。
如果key不存在,則執行插入操作,返回null。
put操作,當發生碰撞時,如果是使用連結串列處理衝突,則執行的尾插法。

put操作的大概流程:

  1. 通過hash值得到所在bucket的下標,如果為null,表示沒有發生碰撞,則直接put
  2. 如果發生了碰撞,則解決發生碰撞的實現方式:連結串列還是樹。
  3. 如果能夠找到該key的結點,則執行更新操作。
  4. 如果沒有找到該key的結點,則執行插入操作,需要對modCount++。
  5. 在執行插入操作之後,如果size超過了threshold,這要擴容執行resize()。

相關文章