深入学习Java之HashMap
来源:互联网 发布:百度云域名注册 编辑:程序博客网 时间:2024/06/10 04:05
深入学习Java之HashMap - 未完成
前言
在前面的几个小节中,我们学习了List接口以及List接口下的几个常用的实现,ArrayList
、LinkedList
、Vector
,接下来的几个小节里,我们将继续学习容器中比较常用的一些实现,包含Map接口、Set接口以及它们对应的实现,本小节主要来学习Map
接口及其实现HashMap
HashMap的继承结构
从上图中可以看到,HashMap实现了Map接口,Cloneable接口以及Serializable接口,并且继承AbastractMap抽象类,其中的Cloneable、Serializable接口只是标记接口,而且我们在前面的小节中已经学习过,所以这里我们就不展开了
同之前学习List一样,我们先从宏观上来学习Map接口以及AbstractMap,然后再深入学习HashMap,剖析HashMap的源码实现
Map接口
所谓的Map,其实就是键值对映射的集合,所谓的键值对,就是指一个由键和值组成的二元组,其中可以通过键来获取值,而且一般来说,如果键相同,则对应的值是相同的,也就是说,如果一个Map中有两个相同的键值对,则他们理论上是同一个键值对。数组可以理解为最简单的键值对集合,也就是最简单的Map,其中的索引就是键,也就是Key,数组中的元素就是值,也就是Value,比如a[1] = a, a[2] = b
其中的 1、2是键,而a、b就是它们所对应的值,也可以把Map理解为就是把key映射到value的一个数据结构
接下来我们来看下Java中的Map接口
从上图中可以看到,Map接口中提供了非常多的方法,接下来我们来简单了解各个方法的作用
- contain开头的方法主要用于查看是否map中是否包含该元素,如
containKey
、containValue
- get方法用于根据key获取对应的值
- put开头的方法用于将键值对放入map中,如
put()
、putAll()
- remove开头的方法用于将键值对从map中移除
keySet
、values
、entrySet
分为用于获取map中键的集合,值的容器以及键值对的集合
Map中还有一个非常重要的元素,Entry,用于对应存放在Map中的元素的形式,也就是上面所说的键值对,结构如下
从上图中可以看到,Entry接口中定义了操作一个Entry的方法,如获取键、获取值、根据键设置值等,这几个方法相对来说比较见名之意,所以这里我们就不做细致的展开,等到具体学习的时候再进行展开
AbstractMap抽象类
从上面的Map的结构图中可以看到,AbstractMap实现了Map接口,AbstractMap中实现了Map接口中部分通用的方法,如下面具体代码所示
查看Map中是否包含某个值
public boolean containsValue(Object value) { // 获得EntrySet的迭代器 Iterator<Entry<K,V>> i = entrySet().iterator(); // 如果输入的值是null,则查找第一个null元素 if (value==null) { while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getValue()==null) return true; } // 如果不是null,则查找对应的值 } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (value.equals(e.getValue())) return true; } } return false; }
查看Map中是否包含某个键
public boolean containsKey(Object key) { // 获取EntrySet的迭代器 Iterator<Map.Entry<K,V>> i = entrySet().iterator(); // 判断输入的键是否是null,如果是,则查看键为null的entry if (key==null) { while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getKey()==null) return true; } // 如果不是null,则查找对应的键 } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (key.equals(e.getKey())) return true; } } return false; }
根据key获取值
public V get(Object key) { Iterator<Entry<K,V>> i = entrySet().iterator(); if (key==null) { while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getKey()==null) return e.getValue(); } } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (key.equals(e.getKey())) return e.getValue(); } } return null; }
删除值
public V remove(Object key) { Iterator<Entry<K,V>> i = entrySet().iterator(); Entry<K,V> correctEntry = null; if (key==null) { while (correctEntry==null && i.hasNext()) { Entry<K,V> e = i.next(); if (e.getKey()==null) correctEntry = e; } } else { while (correctEntry==null && i.hasNext()) { Entry<K,V> e = i.next(); if (key.equals(e.getKey())) correctEntry = e; } } V oldValue = null; if (correctEntry !=null) { oldValue = correctEntry.getValue(); i.remove(); } return oldValue; }
AbstractMap中还有其他一些实现方法,不过由于这些方法在Map的不同实现中会有不同,所以这里我们就不做过多的展开了
深入学习HashMap
在前面的内容中,我们从宏观的角度学习Map的一些常用方法,以及AbstractMap中实现的Map的几个方法,接下来我们将摄入地来学习Map实现类之一的HashMap,并且对HashMap的源码进行剖析
Hash结构的简单介绍
哈希结构,也就是Hash,是一种常用的数据结构,主要就是通过将键进行哈希计算,将大范围的数据映射到一个小的范围中,从而减少对其所占用的空间,比如说,有数据范围在1-100w的数据,而这些数据可能只有1000个,如果采用数组来存放,则需要的空间时非常大的,而且,造成的浪费也是非常明显的,这个时候,如果采用一个hash函数,将1-100w的数据范围映射到一个比较小的空间,比如最简单的MOD 1w(也就是哈希映射,MOD 1W,也就是所采用的哈希函数)则将数据的空间有效地减少了,不过由于将大范围的数据映射到小范围,则必然会造成一些数据映射到同一个空间,这就是哈希冲突,而解决哈希冲突除了需要一个良好的哈希函数外,还需要有处理哈希冲突的方法
常用的哈希函数
- 直接寻址法,取key或者key的某个线性函数
- 数字分析法,根据key自身的特性,选取某几位
- 平方取中法,key平方后取中间几位
- 折叠法,将key切割成位数一样的几个部分,然后进行折叠
- 随机数法,采用随机函数
- 除留余数法,key MOD一个数,上面举例所采用的方法
解决哈希冲突的方法
- 开放地址法
- 再哈希法
- 链地址法(比较常用),将key相同的元素组成一个链
如果对于上面的概念不是很熟悉的话,则需要额外查看资料进行补充,可以参考常见hash算法的原理、哈希冲突的处理方法
HashMap源码剖析
HashMap,是一个非常常用的数据结构,其本质就是对key做了hash的Map的集合,由于采用了Hash方法,HashMap的取元素的效率非常高,接近于O(1),而存放,删除元素的效率也是比较高的,接下来我们来剖析JDK中HashMap的实现,从前辈们的代码中学习具体的HashMap的具体实现
需要注意的小细节
- HashMap是允许null值以及null键的,也就是说,在HashMap中,key=null是允许的,value=null也是允许的
- HashMap是非线程安全的
- HashMap中的元素的顺序是无法保证的
- HashMap中有两个比较重要的属性,装填因子(loadfactor,默认为0.75)和容量(bcapacity)
HashMap的成员
// 默认容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30 // 默认装填因子 static final float DEFAULT_LOAD_FACTOR = 0.75f // HashMap的核心,本质就是键值对节点数组,数组中的每个元素都是一个链表 // 从这里也可以看出,HashMap中采用的哈希冲突解决方法为链地址法 transient Node<K,V>[] table; // HashMap中所有的键值对集合 transient Set<Map.Entry<K,V>> entrySet; // 键值对数量 transient int size; // 装填因子 final float loadFactor; // 阈值,当容量达到该值时,进行扩容 int threshold; // 节点,也就是前面所提到的键值对 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } // 计算hashcode public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } // 设置值并且返回旧值 public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个Node是否相等,只有键以及值都相等才算相等 public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
构造方法
// 提供初始容量和装填因子来构造 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } // 通过给定的数值计算大小 // 这里计算的目的的使得n的左边的第一个1的右边全部为1 // 最大值为2^32,然后执行n+1,也就是产生进位,使得 // 所有n成为原本的值的2倍中最近接2的幂的数 // 好厉害啊,原来还可以这么做,学习了:) static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } // 仅提供初始容量,采用默认的装填因子,也就是0.75f public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 使用另一个Map来构造,此时采用默认的装填因子 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } // 将一个Map中的元素放入HashMap final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 如果此时的HashMap中没有元素,也就是采用该Map的元素来初始化HashMap if (table == null) { // 使用该map的大小/装填因子,计算出此时所需要的table的大小 // 装填因子 = 实际使用容量/table大小 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } // 如果此时的hashMap不为空,则判断所需要的大小是否已经超过需要进行扩容的阈值,如果超过,则进行扩容 else if (s > threshold) resize(); // 遍历该map,并且将所有的键值对放入HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } } // 调整大小,也就是扩容 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //如果原来的hashMap中已经有元素了 if (oldCap > 0) { // 如果就容量已经超过最大值,则将阈值调整至Integer.MAX_VALUE if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 将新容量调整为原来的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 将新阈值调整为原来的两倍 } else if (oldThr > 0) // 如果阈值大于0,则将新容量设置为阈值 newCap = oldThr; else { // 将容量设置为默认容量,也就是16,并且计算初始时的阈值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 如果是树节点,则按树的操作方式,这里的底层是红黑树,不过 // 目前还没有学习到,无法对其进行解析 // HashMap果然复杂,还要好好加油才是 :( else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 将原本的数据取出 do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } // 将指定的键值对放入HashMap中 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 这里写得非常精简,首先tab指向table然后判断table是否为空 // 不为空则n=tab的长度,如果n=0,则进行扩容并且获取扩容后的长度 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 根据hash值计算元素所要放入的位置,如果此时该位置没有元素,则直接放入即可 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 如果有元素,则说明产生了hash冲突 else { Node<K,V> e; K k; // 判断所要插入的元素是否是第一个元素,判断的标准为hash相等,key相等 // 如果是,则直接替换 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; } } 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(); afterNodeInsertion(evict); return null; }
本来想继续研究下去的,不过,感觉有一些内容还不了解,所以目前只研究到这里,等过两天研究懂了再进行补充
- 深入学习Java之HashMap
- Java深入之HashMap
- 深入Java集合学习之HashMap的实现原理详解
- java学习之hashMap
- Java之HashMap学习
- Java学习之HashMap
- Java之HashMap学习
- Java 学习笔记18:深入Java HashMap
- 深入学习集合之HashMap实现原理
- 深入学习集合之HashMap实现原理
- 深入学习java的HashMap实现原理
- Java集合深入学习总结-HashMap
- 深入Java集合系列之三:HashMap
- 深入Java集合系列之三:HashMap
- Java学习笔记之HashMap
- java 集合学习之hashMap
- JAVA学习之HashMap原理
- HashMap之深入理解
- xml之JDOM解析(含乱码处理)
- ubuntu修改为阿里源
- 黑盒测试与白盒测试
- firefox浏览器一分钟去广告--去广告插件安装教程(adblock plus)
- 集合框架(数据结构之链表二)--------(上一篇补充)
- 深入学习Java之HashMap
- UVA
- jQuery-强大的jQuery选择器 (详解)
- 深入理解JAVA虚拟机之JVM内存以及垃圾回收
- Ice-3.5.1在CentOS 6.5系统中的编译配置教程
- BestCoder 度度熊的01世界 (DFS)
- 演示如何将数据库文件写入到 Excel
- ajax+springmvc+jquery用户登录验证
- scala 映射map