- 浏览: 180006 次
- 性别:
- 来自: 武汉
文章分类
最新评论
-
beiizl:
用了博主的方法和代码,不同证书居然可以正常通讯?
Java SSLSocket的使用 -
SHANGLIJAVA:
sorry,运行时没看清。博主的代码确实没问题。。。
Java SSLSocket的使用 -
SHANGLIJAVA:
YoungeeOne 写道最后一个为什么初始化一个空的证书,也 ...
Java SSLSocket的使用 -
q979713444:
那这个的心跳怎么弄呢
Java SSLSocket的使用 -
43350860:
busybox不是每台机器有安装的, 有没有比较裸的办法获取p ...
android中查看端口占用
HashMap继承AbstractMap并实现Map接口。类图如下
1.AbstractMap
不妨先从AbstractMap源码看起。AbstractMap的实现较为简单明了, 总结如下:
- 这个类提供了Map接口的实现的一个基本骨架,通过继承这个类来实现自己的Map,仅需要完成极少量的工作:实现AbstractMap中抽象的entrySet()方法,并提供一个Map.Entry的实现即可
- 这个类没有为性能做优化,几个基本的方法如下
remove()--线性时间
get()-------线性时间
put()-------不支持
entrySet()-未实现
putAll()----调用put()完成
clear()------在entrySet()返回的Set上执行clear()
它的remove()和get()方法使用最直观的办法,说白了就是遍历entrySet()返回的Set, 发现目标则执行相应的remove或get操作。(remove(), get(), clear()全都依赖于entrySet(); 又规定Map接口的entrySet()返回当前映射表的视图而不是副本--即对视图的修改会影响原来的映射表, remove(), get(), clear()只需要对目前一个并未实现的视图进行操作即可。真会偷懒,没干多少活,目标就实现了! )
还需要注意的是, AbstractMap的实现并没有涉及到hash相关的东西,这也是它的名字中看不到Hash的原因。
2.HashMap
2.1. entrySet()方法及"视图"
既然HashMap继承自AbstractMap,它首先必须实现的是抽象的entrySet()方法。以下是源码
public Set<Map.Entry<K,V>> entrySet() { return entrySet0(); } private Set<Map.Entry<K,V>> entrySet0() { Set<Map.Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); } public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<K,V> e = (Map.Entry<K,V>) o; Entry<K,V> candidate = getEntry(e.getKey()); return candidate != null && candidate.equals(e); } public boolean remove(Object o) { return removeMapping(o) != null; } public int size() { return size; } public void clear() { HashMap.this.clear(); } }
entrySet()的实现相当简单,仅仅是返回一个HashMap.EntrySet (下文简称为EntrySet)的实例。
2.1.1. EntrySet实现
EntrySet的类图如下
EntrySet继承自AbstractSet。AbstractSet的子类仅需要实现size()和iterator()方法即可。EntrySet各方法具体实现如下
size()------直接返回外部HashMap实例的size(The number of key-value mappings)即可
clear()-----直接在调用外部HashMap实例中的clear()方法即可
contains()-通过参数obj(一个Map.Entry)的key找到对应的Entry , 假定名为candidate, 比较candidate和obj是否"相等"
iterator()--返回一个EntryIterator实例
remove()--调用外部HashMap实例的removeMapping()方法。
EntryIterator是如何工作的呢。 它的源码如下
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } }
原来是继承自HashIterator, 而且仅仅是覆盖了HashIterator的next()方法
HashIterator部分实现了Iterator接口, 未实现next()方法,所以仍然是一个抽象类。 我们问题的是entrySet()到底是怎样实现HashMap的视图而非副本的, 所以只看关键的源码
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } }
可以看到,HashIterator(也即EntryIterator)内部维护了到HashMap的table(一个Entry数组,存储HashMap的键值对, 下文有介绍)中Entry的引用。所以在EntryIterator的迭代器上调用remove,最终会影响到HashMap保存的键值对。
综合起来,1) EntrySet的remove()调用外部HashMap实例的removeMapping()方法; 2)EntryIterator内部维护到HashMap的table中Entry的引用, 这是实现 "视图" 效果的关键。
2.1.2 EntrySet"视图"效果的验证
调用EntrySet的remove()方法。 结果表明会影响HashMap
map.put("map_key", "map_value"); map.put("map_key2", "map_value2"); System.out.println("remove之前的map " + map); Set<Entry<String, String>> entrySet = map.entrySet(); System.out.println("remove之前的entrySet " + entrySet); System.out.println(entrySet); // 1. 通过remove删除一个entry?? // 1.1 可是我们没法直接生成一个完全一样的entry, 通过下面的方法获取一个 Entry<String, String> entry = entrySet.toArray(new Entry[2])[0]; System.out.println(entry); // 1.2 删除 entrySet.remove(entry); // entrySet变了吗? System.out.println("remove之后的entrySet " + entrySet); // map变了吗? System.out.println("remove之后的map " + map);
调用EntrySet的add()方法。 结果表明add方法未被HashMap.EntrySet实现 , 所以抛出异常(实际当中确实也不应当像以下代码这么使用HashMap)
map.put("map_key", "map_value"); map.put("map_key2", "map_value2"); System.out.println("add之前的map " + map); System.out.println("add之前的entrySet " + map.entrySet()); map2.put("map2_key", "map2_value"); entrySet = map.entrySet(); // 通过add增加一个entry // 可是我们直接生成一个entry太麻烦, 通过下面的方法获取一个 // entrySet.add(map2.entrySet().toArray(new Entry[2])[0]); // entrySet变了吗? System.out.println("add之后的entrySet " + entrySet); // map变了吗? System.out.println("add之后的map " + map);
调用EntrySet的iterator的remove()方法。 结果表明会影响HashMap
map.put("map_key", "map_value"); map.put("map_key2", "map_value2"); System.out.println("remove之前的map " + map); System.out.println("remove之前的entrySet " + map.entrySet()); entrySet = map.entrySet(); Iterator<Entry<String, String>> it = entrySet.iterator(); if (it.hasNext()) { it.next(); it.remove(); } // entrySet变了吗? System.out.println("remove之后的entrySet " + entrySet); // map变了吗? System.out.println("remove之后的map " + map);
2.2. 构造方法
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; /** * The number of key-value mappings contained in this map. */ transient int size; /** * The next size value at which to resize (capacity * load factor). * @serial */ int threshold; /** * The load factor for the hash table. * * @serial */ final float loadFactor; 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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
table---HashMap的槽位(slot)或桶位(bucket)。 这个一个数组,java中存储一组元素最快的数据结构是数组,所以使用它来表示键的信息是合理的。但是这里有一个问题,数组大小固定,而HashMap中保存键值对的数量却不固定。答案是:数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,即散列码(hashCode)。为解决数组容量被固定的问题,不同的键可以产生相同的下标,也即可能会有冲突。因此数组多大就不重要了,任何键总能在数组中找到它的位置。
loadFactor--负载因子。 由于table大小有限,而HashMap中保存键值对的数量不固定,所以不存在完美的散列函数(不同对象的hashCode()方法返回的值有可能相同)。所以table只能存储外部链表之类的数组结构来解决冲突问题。在链表中只能使用equals()方法进行线性查询,这部分的查询相对会比较慢, 主要影响因素有: 1) 散列函数的质量,质量越高冲突越少,则table中的分布均匀,每个链表将越短; 2) 负载因子,负载因子越大查询性能越低,负载因子越小内存浪费越多。 随着HashMap中键值对越来越多, 冲突越来越多,键值对数量达到一定时候,HashMap有必要根据负载因子调整table大小。
2.3. get()方法
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ 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; } /** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ 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); } /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
get()方法的处理过程即在下图中查找某个(椭圆形)元素的过程。
即先通过某种hashCode得到该元素所在链表在table中的索引位置,再由这个索引位置取得对应链表,最后在链表中进行线性查询。
这里要注意一下另外两个方法
hash(int h)-----对hashCode()返回的hash码进行再hash, 主要是防止出现低位(lower bits)相同的(糟糕的)hash码, 减少冲突
index()---------对上述方法得到的hash码进行再调整,保证得到一个合法的索引值(< table.length)。
2.4. put()方法
put()方法源码如下
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; }
对比get()方法,put()方法不难理解。 但要注意两点:1)put()方法可能会从结构上修改HashMap,所以会操作modCount以便"快速失败"检查;2) 如果之前不存在相同的键,则会向HashMap中增加一个新的键值对,即addEntry()。当size超过threshold时,将调整tabler大小为原来的2倍。addEntry()源码如下
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
3. 总结
以上分析了HashMap几个基本且关键的方法。 水平有限, 可能疏漏。 最后摘抄一段HashMap的文档进行总结:
HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
此实现假定哈希函数将元素正确分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代集合视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)的和成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
注意,此实现不是同步的。如果多个线程同时访问此映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的不同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
由所有此类的“集合视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间任意发生不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
发表评论
-
在线工具和文档
2013-10-07 16:25 018款在线代码片断测试工具 http://www.i ... -
ant打包
2013-09-04 14:14 0编辑一个-post-build target, 在编译 ... -
javadoc的使用
2013-09-04 14:12 0中文乱码问题 javadoc *.java -enc ... -
HttpURLConnection的用法
2013-03-14 22:43 0http://stackoverflow.com ... -
工作备忘-使用edtFTPj时遇到的一个问题
2013-01-10 16:02 01. 问题 在前面那篇Java SSLSocket的 ... -
tttttt
2012-12-23 21:57 0优秀androi博客 http://blog.csdn ... -
Java SSLSocket的使用二
2012-12-21 16:57 0使用 commons-net连接ftp ... -
Java SSLSocket的使用之二---让edtFTPj支持FTPS
2012-12-21 16:56 5867免费版的edtFTPj不支持FTPS等安全协议, 所以不能访 ... -
Java SSLSocket的使用
2012-12-20 19:06 607971. 什么是SSLSocket JDK文档指出,SSLSoc ... -
Java SSLSocket的使用总结
2012-12-20 13:29 0SSLSocket的使用 SSL的理解 edtFtp ... -
JNI调用机制备忘
2012-12-19 14:18 0《Android内核剖析》笔记 1. 为什么要使用JN ... -
开发工具收集
2012-12-15 10:17 0eclipse插件 http://developer. ... -
(正式)面试总结
2012-12-11 20:31 02012-12-10 自我介绍 项目介绍。 哪个比较 ... -
(正式)Java之JUnit
2012-12-10 19:43 01. InstrumentationTestRunner的使用 ... -
(正式)Java之Ant
2012-12-07 19:38 0示例1 使用ant jar打包部分class文件 cl ... -
GBK与UTF-8字符集
2012-12-05 23:18 0http://www.divcss5.com/html/h53 ... -
(正式)Android学习之开发框架
2012-12-02 19:57 0android之andEngine使用入门 -
(正式)Java学习之性能优化
2012-12-02 16:37 0java程序性能优化 java ee性能问 ... -
Java之任务调度
2012-12-01 00:35 0几种java 任务调度的实现方法与比较 -
(正式)Java之Apache Commons
2012-11-30 23:59 0利用Commons Lang编写更少的代码 利用 ...
相关推荐
在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...
Java集合专题总结:HashMap和HashTable源码学习和面试总结 本文总结了Java集合专题中的HashMap和HashTable,涵盖了它们的源码学习和面试总结。HashMap是一种基于哈希表的集合类,它的存储结构是一个数组,每个元素...
【Java学习笔记(源码)】是一份详细记录了Java编程语言学习过程的资源集合,包含实际的源代码示例。这份笔记旨在帮助初学者和有一定经验的开发者深入理解和掌握Java语言的核心概念、语法以及常见应用。以下是笔记中...
HashMap是Java编程语言中最常用的集合类之一,它提供了一种基于键值对(key-value pair)的数据存储方式,具有高效查找、插入和删除操作。在Java 8中,HashMap的实现有了很多改进,以提高性能和空间利用率。下面我们...
java学习笔记,包括JVM,并发,JDK一些工具的源码,spring,hashMap实现源码分析
hashmap源码 随着Java学习的不断深入,发现很多知识在脑海里都是一个个碎片,建此仓库的目的希望把零碎的知识点都整合起来,提高自己的学习效率。欢迎志同道合的朋友,一起来维护该仓库 目录 网络基础 WEB TCP协议 ...
这个"java学习源码(很全面)"的压缩包文件显然包含了丰富的学习资源,旨在帮助已经具备一定编程基础的学习者深入理解Java的核心概念和技术。现在,我们将逐个探讨这些关键知识点。 1. **Java基础**:这部分内容...
hashmap源码 [toc] java网络编程学习 简介 用于存放学习java网络编程过程遇见的重难点笔记和相关代码 附带代码大部分是《java网络编程 第四版》的课程代码 对运算字符串进行识别的作业 @since2020.11.26 IO流 - 多...
"Java学习资料/练习源码"这个标题表明这是一份针对Java初学者或进阶者的学习资源,其中可能包含了各种练习项目和源代码,旨在帮助用户深入理解和掌握Java编程。 描述中的"Java学习资料/练习源码"进一步强调了这份...
"java学习随堂演示源码"通常是指在学习Java编程过程中,为了帮助理解概念和实践编程技巧而创建的示例代码。这些源码可能是教授或讲师在课堂上讲解时用到的,用于解释特定的编程概念、数据结构、算法或框架。 1. **...
在Java编程语言中,HashMap是集合框架中一个重要的类,用于存储键值对的数据结构。面试中,HashMap的源码分析与实现是一个常见的考察点,...深入学习和实践HashMap源码,能够帮助我们更好地理解和优化Java应用程序。
《深入解析Java 7 HashMap源码》 HashMap作为Java集合框架中的重要成员,一直以来都是面试和技术探讨的热点。本文将详细剖析Java 7版本HashMap的内部实现机制,帮助读者理解其核心原理,提升对Java并发集合、数据...
HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察...这套学习资料应该包含了HashMap的实例分析、源码解读、常见面试题以及实战演练等内容,确保你全面掌握这一核心Java数据结构。
Java中的HashMap是编程中最常用的集合...如果你深入研究这个项目的源码,不仅可以学习到高级的Java编程技巧,还能了解如何针对具体应用场景优化数据结构,这对于提升编程技能和理解数据结构的底层工作原理非常有帮助。
这本书的源码包含了书中所有示例程序,是学习和理解Java编程的重要参考资料。源码的详细分析可以帮助我们深入理解Java语言的内部工作机制,提升编程技能。 1. **基础语法**: - 类与对象:Java是面向对象的语言,...
Java随机点名源码是一种基于Java编程语言的小型应用程序,用于在给定的姓名列表中随机选择学生进行点名。这个程序特别适用于教师或者需要在人群中随机选取对象的场合,如会议、活动等。该程序的最新版本是在2019年...
Java JDK实例开发宝典源码是一份非常宝贵的资源,它涵盖了Java编程语言的各个方面,旨在帮助开发者深入理解和熟练运用Java JDK...这份“JAVA JDK实例开发宝典源码”无疑是一份珍贵的学习资料,值得每个Java开发者拥有。
这份“java之经典源码大全”资源无疑是对于学习和理解Java技术的宝贵资料。以下是一些可能涵盖的知识点: 1. **Java基础**:源码大全可能会包含基本的Java语法,如变量声明、数据类型、流程控制(if、switch、for、...
这个源码包提供了完整的代码实现,是学习Java编程、数据库操作以及软件工程实践的良好资源。以下是这个系统的一些关键知识点: 1. **MVC设计模式**:在学生信息管理系统中,通常会采用Model-View-Controller(模型-...
在本文中,我们将对 HashMap 的 put 方法的源码进行详细解读,分析put 方法源码中的内在逻辑关系,欣赏源码独特之美,从中学习更为精致的编程思维。 首先,让我们看一下 put 方法的源码: ```java public V put(K ...