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

前言

我知道你們都很熟悉hashmap,而且有事沒事都會new一個,可是hashmap的一些特性你們都是看了忘,忘了再記,今天這個例子能夠幫助你們很好的記住。java

場景

用戶提交一張試卷答案到服務端,post報文可精簡爲數據庫

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

提交地址採用restful風格bash

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

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

正常解決思路

  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萬道題目或更多,服務端運算量將增大不少。app

利用hashmap來解決

首先,看看它的定義函數

基於哈希表的 Map 接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了不一樣步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。post

主要看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. 判分

代碼以下:this

//拿到用戶提交的數據
        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()。
相關文章
相關標籤/搜索