一个简单的例子带你理解Hashmap

本文介绍了一种利用HashMap优化在线考试系统中答案校验的方法。通过对用户提交的答案使用HashMap存储,可以显著提高答案匹配效率,减少服务器负载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

觉得好的话,记得关注我哦!
掘金:
https://juejin.im/user/57cd55218ac247006459c40c
Github:
https://github.com/qq273681448

<think>好的,用户让我用Java写一个简单HashMap,我得先理清楚HashMap的基本原理。首先,HashMap是基于哈希表实现的键值对存储结构。需要处理哈希冲突,通常用链表或者红黑树。不过用户要的是简单版,可能链表就够了。 接下来,我得考虑结构。应该有一个固定大小的数组,每个数组元素是一个链表的头节点。每个节点包含键、值和下一个节点的引用。先定义节点类,静态内部类可能比较合适,包含key、value和next。 然后,初始化数组。初始容量设为默认值,比如16,负载因子0.75,当元素数量超过容量乘以负载因子时,需要扩容。但为了简单,可能先不实现自动扩容,或者简化处理。 哈希函数是关键。Java的HashMap用的是key的hashCode(),然后通过高位异或和取模运算确定索引。这里可以简单用hashCode()和数组长度取模,但要注意处理负数,可以用&操作代替取模,因为数组长度是2的幂。不过简单起见,可能直接用Math.abs,虽然不够严谨,但适合示。 put方法需要计算索引,找到对应的链表,遍历看是否有相同的key。如果有,替换value;否则,添加到链表头部或尾部。这里选择添加到头部,因为更简单,不需要遍历到尾部。 get方法类似,计算索引,遍历链表查找key,找到返回value,否则null。 还需要考虑扩容。但简单实现可能先忽略,或者实现一个简单的扩容,比如双倍扩容,重新哈希所有元素。不过这可能增加复杂度,用户要的是简单版,可能暂时省略,或者简单提及。 测试部分,创建实,put几个键值对,然后get验证是否正确。可能出现的问题包括哈希冲突处理不正确,或者索引计算错误。 现在,把这些步骤用代码实现。先定义SimpleHashMap类,内部类Entry。构造函数中初始化数组。put和get方法实现上述逻辑。注意处理null键的情况,但简单版可能暂时不支持null键。 最后,检查代码是否符合Java语法,比如泛型的使用,数组的初始化,链表的操作是否正确。测试一下常见情况,比如插入、查询、冲突处理。 可能遗漏的点:没有处理扩容,迭代器,删除操作,但用户要求简单,所以这些可以忽略。重点展示哈希表的基本结构,put和get的实现。确保代码清晰,注释必要的地方,方便用户理解。</think>以下是用Java实现简化版HashMap的分步说明: 一、基础结构设计 ```java public class SimpleHashMap<K, V> { private static final int INIT_CAPACITY = 16; // 初始容量 private Entry<K, V>[] table; // 存储桶数组 private int size = 0; // 元素计数器 // 链表节点定义 private static class Entry<K, V> { final K key; V value; Entry<K, V> next; Entry(K key, V value, Entry<K, V> next) { this.key = key; this.value = value; this.next = next; } } public SimpleHashMap() { table = new Entry[INIT_CAPACITY]; } } ``` 二、哈希函数实现 ```java private int getIndex(K key) { // 使用哈希码绝对值取模(简化处理) return Math.abs(key.hashCode()) % table.length; } ``` 三、put方法实现 ```java public void put(K key, V value) { int index = getIndex(key); Entry<K, V> entry = table[index]; // 遍历链表查找已存在key while (entry != null) { if (entry.key.equals(key)) { entry.value = value; // 更新已有值 return; } entry = entry.next; } // 创建新节点并插入链表头部 Entry<K, V> newEntry = new Entry<>(key, value, table[index]); table[index] = newEntry; size++; } ``` 四、get方法实现 ```java public V get(K key) { int index = getIndex(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { return entry.value; } entry = entry.next; } return null; // 未找到返回null } ``` 五、使用示 ```java public static void main(String[] args) { SimpleHashMap<String, Integer> map = new SimpleHashMap<>(); map.put("apple", 10); map.put("banana", 5); System.out.println(map.get("apple")); // 输出10 System.out.println(map.get("banana")); // 输出5 System.out.println(map.get("orange")); // 输出null } ``` 六、实现特点说明 1. 哈希冲突处理:采用链表法(拉链法) 2. 时间复杂度:理想情况$O(1)$,最差情况$O(n)$ 3. 当前限制: - 未实现自动扩容 - 未处理红黑树优化 - 不支持null键 - 未实现删除操作 七、扩展建议 1. 添加resize方法实现自动扩容 2. 添加remove方法实现删除功能 3. 使用更优化的哈希函数 4. 当链表过长时转换为红黑树(JDK8+的实现方式) 这个简化实现演示了HashMap的核心原理:通过哈希函数确定存储位置,使用链表处理冲突,通过键的equals方法进行精确匹配。实际Java的HashMap实现更加复杂,包含容量管理、树化优化、并发控制等高级特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值