深入理解Java中的HashMap的实现原理
HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口。关系图例如以下所看到的:
详细实现例如以下:
- import java.util.*;
- public class SimpleMap<K,V> extends AbstractMap<K,V> {
- //keys存储全部的键
- private List<K> keys = new ArrayList<K>();
- //values存储全部的值
- private List<V> values = new ArrayList<V>();
- /**
- * 该方法获取Map中全部的键值对
- */
- @Override
- public Set entrySet() {
- Set<Map.Entry<K, V>> set = new SimpleSet<Map.Entry<K,V>>();
- //keys的size和values的size应该一直是一样大的
- Iterator<K> keyIterator = keys.iterator();
- Iterator<V> valueIterator = values.iterator();
- while(keyIterator.hasNext() && valueIterator.hasNext()){
- K key = keyIterator.next();
- V value = valueIterator.next();
- SimpleEntry<K,V> entry = new SimpleEntry<K,V>(key, value);
- set.add(entry);
- }
- return set;
- }
- @Override
- public V put(K key, V value) {
- V oldValue = null;
- int index = this.keys.indexOf(key);
- if(index >= 0){
- //keys中已经存在键key,更新key相应的value
- oldValue = this.values.get(index);
- this.values.set(index, value);
- }else{
- //keys中不存在键key,将key和value作为键值对加入进去
- this.keys.add(key);
- this.values.add(value);
- }
- return oldValue;
- }
- @Override
- public V get(Object key) {
- V value = null;
- int index = this.keys.indexOf(key);
- if(index >= 0){
- value = this.values.get(index);
- }
- return value;
- }
- @Override
- public V remove(Object key) {
- V oldValue = null;
- int index = this.keys.indexOf(key);
- if(index >= 0){
- oldValue = this.values.get(index);
- this.keys.remove(index);
- this.values.remove(index);
- }
- return oldValue;
- }
- @Override
- public void clear() {
- this.keys.clear();
- this.values.clear();
- }
- @Override
- public Set keySet() {
- Set<K> set = new SimpleSet<K>();
- Iterator<K> keyIterator = this.keys.iterator();
- while(keyIterator.hasNext()){
- set.add(keyIterator.next());
- }
- return set;
- }
- @Override
- public int size() {
- return this.keys.size();
- }
- @Override
- public boolean containsValue(Object value) {
- return this.values.contains(value);
- }
- @Override
- public boolean containsKey(Object key) {
- return this.keys.contains(key);
- }
- @Override
- public Collection values() {
- return this.values();
- }
- }
事实上我们仅仅要重写entrySet和put方法,该类就能够正确执行。那我们为什么还要重写剩余的那些方法呢?AbstractMap这种方法做了非常多处理操作。Map中的非常多方法在AbstractMap都实现了,而且非常多方法都依赖于entrySet方法,举个样例。Map接口中的values方法是让我们返回该Map中全部的值的Collection。我们能够看一下AbstractMap中对values方法的实现:
- public Collection<V> values() {
- if (values == null) {
- values = new AbstractCollection<V>() {
- public Iterator<V> iterator() {
- return new Iterator<V>() {
- private Iterator<Entry<K,V>> i = entrySet().iterator();
- public boolean hasNext() {
- return i.hasNext();
- }
- public V next() {
- return i.next().getValue();
- }
- public void remove() {
- i.remove();
- }
- };
- }
- public int size() {
- return AbstractMap.this.size();
- }
- public boolean isEmpty() {
- return AbstractMap.this.isEmpty();
- }
- public void clear() {
- AbstractMap.this.clear();
- }
- public boolean contains(Object v) {
- return AbstractMap.this.containsValue(v);
- }
- };
- }
- return values;
- }
大家能够看到。代码不少。主要的思路是先通过entrySet生成包括全部键值对的Set,然后通过迭代获取当中的value值。当中生成包括全部键值对的Set肯定须要开销。所以我们在自己的实现里面重写了values方法,就一句话,return this.values,直接返回我们的values字段。
所以我们重写大部分方法的目的都是让方法的实现更快更简洁。
下面是我们自己实现的键值对类SimpleEntry。实现了Map.Entry<K,V>接口,代码例如以下:
- import java.util.Map;
- //Map中存储的键值对,键值对须要实现Map.Entry这个接口
- public class SimpleEntry<K,V> implements Map.Entry<K, V>{
- private K key = null;//键
- private V value = null;//值
- public SimpleEntry(K k, V v){
- this.key = k;
- this.value = v;
- }
- @Override
- public K getKey() {
- return this.key;
- }
- @Override
- public V getValue() {
- return this.value;
- }
- @Override
- public V setValue(V v) {
- V oldValue = this.value;
- this.value = v;
- return oldValue;
- }
- }
下面是我们自己实现的集合类SimpleSet,继承自抽象类AbstractSet<K,V>,代码例如以下:
- import java.util.AbstractSet;
- import java.util.ArrayList;
- import java.util.Iterator;
- public class SimpleSet<E> extends AbstractSet<E> {
- private ArrayList<E> list = new ArrayList<E>();
- @Override
- public Iterator<E> iterator() {
- return this.list.iterator();
- }
- @Override
- public int size() {
- return this.list.size();
- }
- @Override
- public boolean contains(Object o) {
- return this.list.contains(o);
- }
- @Override
- public boolean add(E e) {
- boolean isChanged = false;
- if(!this.list.contains(e)){
- this.list.add(e);
- isChanged = true;
- }
- return isChanged;
- }
- @Override
- public boolean remove(Object o) {
- return this.list.remove(o);
- }
- @Override
- public void clear() {
- this.list.clear();
- }
- }
我们測试下我们写的SimpleMap这个类,測试包括两部分。一部分是測试我们写的SimpleMap是不是正确,第二部分測试性能怎样,測试代码例如以下:
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.Map;
- public class Test {
- public static void main(String[] args) {
- //測试SimpleMap的正确性
- SimpleMap<String, String> map = new SimpleMap<String, String>();
- map.put("iSpring", "27");
- System.out.println(map);
- System.out.println(map.get("iSpring"));
- System.out.println("-----------------------------");
- map.put("iSpring", "28");
- System.out.println(map);
- System.out.println(map.get("iSpring"));
- System.out.println("-----------------------------");
- map.remove("iSpring");
- System.out.println(map);
- System.out.println(map.get("iSpring"));
- System.out.println("-----------------------------");
- //測试性能怎样
- testPerformance(map);
- }
- public static void testPerformance(Map<String, String> map){
- map.clear();
- for(int i = 0; i < 10000; i++){
- String key = "key" + i;
- String value = "value" + i;
- map.put(key, value);
- }
- long startTime = System.currentTimeMillis();
- for(int i = 0; i < 10000; i++){
- String key = "key" + i;
- map.get(key);
- }
- long endTime = System.currentTimeMillis();
- long time = endTime - startTime;
- System.out.println("遍历时间:" + time + "毫秒");
- }
- }
- //创建HashMap的实例
- HashMap<String, String> map = new HashMap<String, String>();
- //測试性能怎样
- testPerformance(map);
我们的SimpleMap是用ArrayList来存储keys和values的。ArrayList本质是用数组实现的,我们的SimpleMap的get方法是这样实现的:
- @Override
- public V put(K key, V value) {
- V oldValue = null;
- int index = this.keys.indexOf(key);
- if(index >= 0){
- //keys中已经存在键key,更新key相应的value
- oldValue = this.values.get(index);
- this.values.set(index, value);
- }else{
- //keys中不存在键key,将key和value作为键值对加入进去
- this.keys.add(key);
- this.values.add(value);
- }
- return oldValue;
- }
须要性能开销的主要是this.keys.indexOf(key)这句代码,这句代码从ArrayList中查找指定元素的索引,本质就是从数组开头走,往后找。直至数组的末尾。例如以下图所看到的:
- public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
在put方法中,。调用了对象的hashCode方法,该方法返回一个int类型的值。是个初始的哈希值,这个值就相当于车牌号,比如"鲁E.DE829",HashMap中有个hash方法。该hash方法将我们得到的初始的哈希值做进一步处理。得到终于的哈希值,就好比我们将车牌号传入hash方法。然后返回该存放车辆的大桶,即返回"鲁",这样HashMap就把这辆车放到标有“鲁”的大桶里面了。上面说到的hash方法叫做哈希函数。专门负责依据传入的值返回指定的终于哈希值。详细实现例如以下:
- static int hash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
这里简单说一下哈希函数,哈希函数有多种实现方式。比方最简单的就是取余法,比方对i%10取余,然后依照余数创建不同的区块或桶。
比方有100个数,各自是从1到100,那么分别对10取余,那么就能够把这100个数放到10个桶子里面了,这就是所谓的哈希函数。
仅仅只是HashMap中的hash函数看起来比較复杂,进行的是位操作,可是其作用与简单的取余哈希法的作用是等价的。就是把元素分类放置。
- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
- }
- public V get(Object key) {
- if (key == null)
- return getForNullKey();
- int hash = hash(key.hashCode());
- 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;
- }
在get方法中。也是先调用了对象的hashCode方法。就相当于车牌号,然后再将该值让hash函数处理得到终于的哈希值,也就是桶的索引。
然后我们再去这个标有“鲁”的桶里面去找我们的键值对,首先先取出桶里面第一个键值对,比对一下是不是我们要找的元素,假设是就直接返回了。假设不是就通过键值对的next顺藤摸瓜通过单向链表继续找下去,直至找到。 例如以下图所看到的:
- import java.util.HashMap;
- public class Car {
- private final String num;//车牌号
- public Car(String n){
- this.num = n;
- }
- public String getNum(){
- return this.num;
- }
- @Override
- public boolean equals(Object obj) {
- if(obj == null){
- return false;
- }
- if(obj instanceof Car){
- Car car = (Car)obj;
- return this.num.equals(car.num);
- }
- return false;
- }
- public static void main(String[] args){
- HashMap<Car, String> map = new HashMap<Car, String>();
- String num = "鲁E.DE829";
- Car car1 = new Car(num);
- Car car2 = new Car(num);
- System.out.println("Car1 hash code: " + car1.hashCode());
- System.out.println("Car2 hash code: " + car2.hashCode());
- System.out.println("Car1 equals Car2: " + car1.equals(car2));
- map.put(car1, new String("Car1"));
- map.put(car2, new String("Car2"));
- System.out.println("map.size(): " + map.size());
- }
- }
我们在main函数中写了一些測试代码,我们创建了一个HashMap。该HashMap的用Car作为键,用字符串作为值。我们用同一个字符串实例化了两个Car,分别为car1和car2,然后将这两个car都放入到HashMap中,输出结果例如以下:
Car2 hash code: 2027651571
Car1 equals Car2: true
map.size(): 2
通过我们前面研究可知,假设是两个元素相等。那么这两个元素应该放到同一个HashMap的桶里。可是因为我们的car1和car2的hashCode不同,所以HashMap将car1和car2分别放到不同的桶子里面了,这就出问题了。相等(equals)的两个元素(car1和car2)假设hashCode返回值不同,那么这两个元素就会放到HashMap不同的区间里面。
所以我们写代码的时候要保证相互equals的两个对象的哈希值必然要相等。即必须保证hashCode的返回值相等。那怎样解决问题?我们仅仅须要重写hashCode方法就可以,代码例如以下:
- @Override
- public int hashCode() {
- return this.num.hashCode();
- }
又一次执行main中的測试代码,输出结果例如以下:
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1
下次查找该对象的时候,还是计算其哈希值,依据哈希值确定区块或桶,然后在这个小范围内查找元素,这样就快多了。
深入理解Java中的HashMap的实现原理的更多相关文章
- Java中的HashMap的工作原理是什么?
问答题23 /120 Java中的HashMap的工作原理是什么? 参考答案 Java中的HashMap是以键值对(key-value)的形式存储元素的.HashMap需要一个hash函数,它使用ha ...
- JDK学习---深入理解java中的HashMap、HashSet底层实现
本文参考资料: 1.<大话数据结构> 2.http://www.cnblogs.com/dassmeta/p/5338955.html 3.http://www.cnblogs.com/d ...
- JDK学习---深入理解java中的LinkedList
本文参考资料: 1.<大话数据结构> 2.http://blog.csdn.net/jzhf2012/article/details/8540543 3.http://blog.csdn. ...
- 理解Java中的弱引用(Weak Reference)
本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限,叙述中难免存在不准确或是不清晰的地方,希望大家可以指出, ...
- 深入理解Java中的不可变对象
深入理解Java中的不可变对象 不可变对象想必大部分朋友都不陌生,大家在平时写代码的过程中100%会使用到不可变对象,比如最常见的String对象.包装器对象等,那么到底为何Java语言要这么设计,真 ...
- 沉淀再出发:java中的HashMap、ConcurrentHashMap和Hashtable的认识
沉淀再出发:java中的HashMap.ConcurrentHashMap和Hashtable的认识 一.前言 很多知识在学习或者使用了之后总是会忘记的,但是如果把这些只是背后的原理理解了,并且记忆下 ...
- 关于Java中的HashMap的深浅拷贝的测试与几点思考
0.前言 工作忙起来后,许久不看算法,竟然DFA敏感词算法都要看好一阵才能理解...真是和三阶魔方还原手法一样,田园将芜,非常可惜啊. 在DFA算法中,第一步是需要理解它的数据结构,在此基础上,涉及到 ...
- java 中遍历hashmap 和hashset 的方法
一.java中遍历hashmap: for (Map.Entry<String, Integer> entry : tempMap.entrySet()) { String ...
- Java中关于HashMap的元素遍历的顺序问题
Java中关于HashMap的元素遍历的顺序问题 今天在使用如下的方式遍历HashMap里面的元素时 1 for (Entry<String, String> entry : hashMa ...
随机推荐
- 03008_ServletContext
1.什么是ServletContext? (1)ServletContext代表是一个web应用的环境(上下文)对象,ServletContext对象 内部封装是该web应用的信息,Servle ...
- 如何理解redo和undo的作用
目录 如何理解redo和undo的作用 redo undo UNDO和REDO的区别 如何理解redo和undo的作用 redo 重做日志(redo)包含所有数据产生的历史改变记录,是oracle在线 ...
- webdriver高级应用- 禁止IE的保护模式
#encoding=utf-8 from selenium import webdriver from selenium.webdriver.common.desired_capabilities i ...
- Selenium WebDriver- 操作JavaScript的prompt弹窗(使用率低)
#encoding=utf-8 import unittest import time from selenium import webdriver from selenium.webdriver i ...
- 爬取本blog的所有标题和链接
#coding=utf-8 from bs4 import BeautifulSoup import urllib.request for i in range(1,54): url = " ...
- oracle中xhost报错
一.命令找不到 xhost:command not found yum whatprovides "*/xhost" Loaded plugins: product-id, sec ...
- HDU1071 The area
Ignatius bought a land last week, but he didn't know the area of the land because the land is enclos ...
- [Codeforces Round #297 Div. 2] E. Anya and Cubes
http://codeforces.com/contest/525/problem/E 学习了传说中的折半DFS/双向DFS 先搜前一半数,记录结果,然后再搜后一半数,匹配之前结果. #include ...
- NOI热身赛A. 小w、小j和小z
$n \leq 100000$个点在数轴上运动,给初始位置和速度.能删$k$个点,问最晚什么时候发生第一次碰撞. 这个贪心题有点惊.. 首先肯定二分答案,然后就是判断怎么删这$k$个点.我想可以把有冲 ...
- hdu 5971 Wrestling Match 二分图染色
题目链接 题意 \(n\)人进行\(m\)场比赛,给定\(m\)场比赛的双方编号:再给定已知的为\(good\ player\)的\(x\)个人的编号,已知的为\(bad\ player\)的\(y\ ...