1. 程式人生 > >一個簡單的例子帶你理解Hashmap

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

前言

我知道大家都很熟悉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

那麼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()。