java.io.ByteArrayInputStream 源码分析

您还未登录 ! 登录 注册

论坛首页 → Java企业应用论坛 → 

深入理解HashMap

全部 Hibernate Spring Struts iBATIS 企业应用 Lucene SOA Java综合 Tomcat 设计模式 OO JBoss

? 上一页 1 2 3 … 8 9 下一页
?

浏览 65519
























































































锁定老帖子 主题:深入理解HashMap

该帖已经被评为精华帖

作者 正文

  • annegu

  • 等级: 


  • 性别: 

  • 文章:
    38

  • 积分:
    360

  • 来自:
    杭州

发表时间:2009-12-02  
最后修改:2010-05-16

< > 猎头职位: 上海: Junior Product
Manager

相关文章: 


推荐群组: JAVA
3T
  更多相关推荐

Java综合

/**     
*@author annegu 
    *@date 2009-12-02      */ 
Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论。 
1、hashmap的数据结构 
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外。Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。 
 
从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。我们来看看java代码:

Java代码  

  1. /**

  2. * The table, resized as necessary. Length MUST Always be a power of two.

  3. *  FIXME 这里需要注意这句话,至于原因后面会讲到

  4. */

  5. transient Entry[] table;

Java代码  

  1. static class Entry<K,V> implements Map.Entry<K,V> {

  2. final K key;

  3. V value;

  4. final int hash;

  5. Entry<K,V> next;

  6. ..........

  7. }

上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。 
        
当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~ 
2、hash算法 
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

Java代码  

  1. static int indexFor(int h, int length) {

  2. return h & (length-1);

  3. }

首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 
        
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 
 

         
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 
         
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。 
所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

Java代码  

  1. // Find a power of 2 >= initialCapacity

  2. int capacity = 1;

  3. while (capacity < initialCapacity)

  4. capacity <<= 1;


3、hashmap的resize 
      
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 
        
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new
HashMap(1000), 但是理论上来讲new
HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new
HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000,
我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。 

4、key的hashcode与equals方法改写 
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。 
Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。  在改写equals方法的时候,需要满足以下三点: 
(1) 自反性:就是说a.equals(a)必须为true。 
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。 
(3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。 
通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。 
总结: 
       
本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。 

这是hashmap第一篇,主要讲了一下hashmap的数据结构和计算hash的算法。接下去annegu还会写第二篇,主要讲讲LinkedHashMap和LRUHashMap。先做个预告,呵呵~

声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。

推荐链接

返回顶楼

 

  • luobo25

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    4

  • 积分:
    0

  • 来自:
    北京

发表时间:2009-12-03

当length=2^n时,hashcode & (length-1) ==
hashcode % length,难怪这么巧 ,楼主的研究有价值

返回顶楼

回帖地址

0 0 请登录后投票

 

  • mycybyb

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    138

  • 积分:
    30

  • 来自:
    沈阳

发表时间:2009-12-03

写的很好。
不过,以前学数据结构的时候都学过。

返回顶楼

回帖地址

0 0 请登录后投票

 

  • 火星来客

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    26

  • 积分:
    30

  • 来自:
    火星

发表时间:2009-12-03

mycybyb 写道

写的很好。 
不过,以前学数据结构的时候都学过。

楼上应该没有看楼主的这篇文章,数据结构中应该不会有JDK中map的hash函数的实现说明,而楼主详细的分析了jdk中的hashmap中hash函数真正的原理和价值所在,网上HashMap的文章一堆一堆的,但是没有人能说清楚h&length-1的奥妙所在

返回顶楼

回帖地址

0 0 请登录后投票

 

  • mycybyb

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    138

  • 积分:
    30

  • 来自:
    沈阳

发表时间:2009-12-03  
最后修改:2009-12-03

火星来客 写道

mycybyb 写道

写的很好。 
不过,以前学数据结构的时候都学过。

楼上应该没有看楼主的这篇文章,数据结构中应该不会有JDK中map的hash函数的实现说明,而楼主详细的分析了jdk中的hashmap中hash函数真正的原理和价值所在,网上HashMap的文章一堆一堆的,但是没有人能说清楚h&length-1的奥妙所在

没看能说好吗?  我说写的很好,说的就是h
& length -1这个。 
其他的,都是常识了。 

返回顶楼

回帖地址

0 0 请登录后投票

 

  • 火星来客

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    26

  • 积分:
    30

  • 来自:
    火星

发表时间:2009-12-03  
最后修改:2009-12-03

mycybyb 写道

火星来客 写道

mycybyb 写道

写的很好。 
不过,以前学数据结构的时候都学过。

楼上应该没有看楼主的这篇文章,数据结构中应该不会有JDK中map的hash函数的实现说明,而楼主详细的分析了jdk中的hashmap中hash函数真正的原理和价值所在,网上HashMap的文章一堆一堆的,但是没有人能说清楚h&length-1的奥妙所在

没看能说好吗?  我说写的很好,说的就是h
& length -1这个。 
其他的,都是常识了。 
恩,同意,其他确实是常识,只是从我的面试结果来看,没有常识的程序员比较多。 

返回顶楼

回帖地址

0 0 请登录后投票

 

  • dennis_zane

  • 等级: 资深会员


  • 性别: 

  • 文章:
    1530

  • 积分:
    2148

  • 来自:
    杭州

发表时间:2009-12-03

火星来客 写道

mycybyb 写道

写的很好。 
不过,以前学数据结构的时候都学过。

楼上应该没有看楼主的这篇文章,数据结构中应该不会有JDK中map的hash函数的实现说明,而楼主详细的分析了jdk中的hashmap中hash函数真正的原理和价值所在,网上HashMap的文章一堆一堆的,但是没有人能说清楚h&length-1的奥妙所在
h&length-1,这个也算是常识吧,对数学或者位运算了解点就知道了。 

返回顶楼

回帖地址

0 2 请登录后投票

 

  • 火星来客

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    26

  • 积分:
    30

  • 来自:
    火星

发表时间:2009-12-03  
最后修改:2009-12-03

dennis_zane 写道

h&length-1,这个也算是常识吧,对数学或者位运算了解点就知道了。

&运算本身当然是常识,任何一本计算机图书中几乎都会提到,但是将其和:

Java代码  

  1. int capacity = 1;

  2. while (capacity < initialCapacity)

  3. capacity <<= 1;

组合起来使用,得到%的效果,但是性能比%高,这一点我没有想到,所以这一点上对我个人来说没有将其视为常识。
返回顶楼

回帖地址

2 0 请登录后投票

 

  • yuyee

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    811

  • 积分:
    0

  • 来自:
    杭州

发表时间:2009-12-03

 的确蛮仔细的

返回顶楼

回帖地址

0 0 请登录后投票

 

  • lwyx2000

  • 等级:
    初级会员


  • 性别: 

  • 文章:
    13

  • 积分:
    0

  • 来自:
    福建省

发表时间:2009-12-03

很不错!!

返回顶楼

回帖地址

0 0 请登录后投票

 

? 上一页 1 2 3 … 8 9 下一页
?

论坛首页 → Java企业应用版 
跳转论坛:移动开发技术 Web前端技术 
Java企业应用 编程语言技术 
综合技术 入门技术 招聘求职 海阔天空

 
 
 

? 2003-2014 ITeye.com. [ 京ICP证110151号 京公网安备110105010620 ] 百联优力(北京)投资有限公司
版权所有

时间: 2024-11-02 04:30:46

java.io.ByteArrayInputStream 源码分析的相关文章

java.io.BufferedOutputStream 源码分析

BufferedOutputStream  是一个带缓冲区到输出流,通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统. 俩个成员变量,一个是存储数据的内部缓冲区,一个是缓冲区中的有效字节数. /** * The internal buffer where data is stored. */ protected byte buf[]; /** * The number of valid bytes in the buffer. This value

java.io.ByteArrayOutputStream 源码分析

成员变量 buf是存储数据的缓冲区  count是缓冲区中的有效字节数. /** * The buffer where data is stored. */ protected byte buf[]; /** * The number of valid bytes in the buffer. */ protected int count; 构造参数 默认值32,也可以指定缓冲区到大小 /** * Creates a new byte array output stream. The buffe

Java I/O系列(二)ByteArrayInputStream源码分析及理解

定义 继承了InputStream,数据源是内置的byte数组buf,那read ()方法的使命(读取一个个字节出来),在ByteArrayInputStream就是简单的通过定向的取buf元素实现的 核心源码理解 源码: 1 public ByteArrayInputStream(byte buf[], int offset, int length) { 2 this.buf = buf; 3 this.pos = offset; 4 this.count = Math.min(offset

Java split方法源码分析

Java split方法源码分析 1 public String[] split(CharSequence input [, int limit]) { 2 int index = 0; // 指针 3 boolean matchLimited = limit > 0; // 是否限制匹配个数 4 ArrayList<String> matchList = new ArrayList<String>(); // 匹配结果队列 5 Matcher m = matcher(inp

【JAVA】ThreadLocal源码分析

ThreadLocal内部是用一张哈希表来存储: 1 static class ThreadLocalMap { 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 /** The value associated with this ThreadLocal. */ 4 Object value; 5 6 Entry(ThreadLocal<?> k, Object v) { 7 super(k)

Java中ArrayList源码分析

一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保证容量能容纳所有数据. 1.1.ArrayList 的继承与实现接口 ArrayList继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口. public class  ArrayList<E> ex

Java笔记---ArrayList源码分析

一.前言 一直就想看看java的源码,学习一下大牛的编程.这次下狠心花了几个晚上的时间,终于仔细分析了下 ArrayList 的源码(PS:谁说的一个晚上可以看完的?太瞎扯了).现在记录一下所得. 二.ArrayList 源码分析 2.1 如何分析? 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 Eclipse 找到所需要分析的类(此处就是 ArrayList) 新建类:新建一个类,命名为 ArrayList,将源码拷贝到该类.因为我

Java并发包源码分析

并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善.现代的PC都有多个CPU或一个CPU中有多个核,是否能合理运用多核的能力将成为一个大规模应用程序的关键. Java基础部分知识总结点击Java并发基础总结.Java多线程相关类的实现都在Java的并发包concurrent,concurrent包主要包含3部分内容,第一个是atomic包,里面主要是一些原子类,比如AtomicInteger.

Java - &quot;JUC&quot; Semaphore源码分析

Java多线程系列--"JUC锁"11之 Semaphore信号量的原理和示例 Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可:当信号量中有可用的许可时,线程能获取该许可:否则线程必须等待,直到有可用的许可为止. 线程可以通过release()来释放它所持有的信号量许可. Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的Reent