`

JDK源代码学习系列一---java.util(2)

    博客分类:
  • Java
阅读更多
前一篇 JDK源代码学习系列一---java.util(1):http://www.iteye.com/topic/905329

       就目前看来,上一篇有点标题党的味道,源代码学习却没有一句JDK源代码,把大家都骗进来了,为了改过自新,哥现在开始要贴源代码了。我相信我要讲的,我所看到的,很多jer都知道的比我深刻,有同学已经在回复中贴出了部分源码,但是我的学习我得自己过一遍。所以我们接下去是讨论,我说我的,各位大大们觉得我理解有误的地方可以用砖头拍我。
       继续来学习HashMap。
       首先,我们来看下什么叫hash。百度有如下解释,参考性拿来使用:
       “Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。”
       接着,我们来看一下HashMap的一些结构。HashMap是存储key-value键值对的,即Entry。宏观上来说它底层是一个Entry的数组。
    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;

       但是,Entry并不是简单纯粹的键值对,它的结构如下:
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;
...

       重点请关注Entry<K,V> next;,这个让人想到的是链表。
       为了能形象的看到整个HashMap的结构,哥从百度上找了张HashMap的结构图(如有侵权行为,请通知本人,请勿直接投隐藏...):



       可见table这个数组存储的并非是单纯的键值对,实际存储的是链表,而每一个Entry的next将指向该链表的下一个元素。那为什么在这个地方需要链表的结构呢?
       我们来看一下HashMap的put方法:
     /**
     * 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为null,则调用putForNullKey(value),由此可见HashMap支持null为key
        if (key == null)
            return putForNullKey(value);
        //获取key的hash值
        int hash = hash(key.hashCode());
        //由此可见新元素插入HashMap的位置将由其hash值决定,所以通过hash值和table长
        //度算出该key的hash值所对应的table数组索引i。
        int i = indexFor(hash, table.length);
        //遍历table[i]这个位置的链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果hash值与key都相同,表明该链表中已存在该key的entry,则更新该entry的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //若程序走到这里,table[i]处尚没有该key对应的entry,插入entry
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    ......

     /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    ......

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    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);
    }
     


     获取hash值需要使用哈希算法,但是通过上文Hash解释的红色字体部分“不同的输入可能会散列成相同的输出”可以得知,哈希算法可能会出现不同的key得到重复的hash值,这就是hash冲突。这样的entry都会被插入到table的i处,为了不覆盖不同的key的entry,多个hash值相同的key的entry就会被按照链表的结构串接起来。
     到这里,我希望我讲清楚了HashMap的结构与其成因。

     现在,我们来看看上一篇中留下的问题。
    
     首次插入p1,p2的时候,p1与p2的hash值不同,所以插入后的结构应该如下:

     可见key为p1,p2的hash值得到分别为x,y,根据indexFor方法得到该插入table的索引值分别为m,n。因此entry分别被插入到table的索引m,n处。但是后来p2的id被修改了,因为entry中的key虽然是final的,但它只是一个引用,实例对象还是可以被修改的。一旦p2的id被修改,根据Person类重写的hashcode方法,其重新计算得到的hash值也将改变。因为将p2的id改为了与p1相同,所以重新计算得到p2的hash值与p1相同,都为x。我们来看一下HashMap的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();
        //重新计算key的hash值,此时p2与p1的hash值相同
        int hash = hash(key.hashCode());
        //此处的循环并不是一key对应多个value的循环,而是通过重新计算得到key的hash值
        //找出该key所对应的entry在table数组中的链表所在的索引,然后遍历相同hash的
        //entry链表找到目标key,并返回该key对应的value
        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;
    }

     因为此时在n位置的p2对应的entry中的hash是final字段,在插入时就已经固定,所以重新计算得到hash并不会更新,且该链表结构也不会改变。p2对应的entry还在老位置,但是通过get(p2)等效于get(p1),因为他们的hash值相同。所以虽然出现了一键对应多个value的情况,但实际上不论通过get(p1)还是get(p2)只能找到m处的entry了。
     那么现在你会想到,既然p2对应的entry还在老位置,我有什么方法可以重新找到这个entry呢。如果我给你两种方案:
     1.将p2的id改回去,然后通过m.get(p2):
 ......
 
 p2.setId("2");

 ......

     2.新建一个p3,把p3的id设为"2",即跟原来的p2相同,然后直接通过m.get(p3):
      ......
      Person p3 = new Person();
      p3.setName("name3");
      ......
      System.out.println("Map m 通过get方法用key p3:"+p3+"时,获取的value:"+m.get(p3));

    大家觉得两种结果分别会怎样呢?原因又是什么?


   

    
  • 大小: 12.5 KB
  • 大小: 13 KB
分享到:
评论
6 楼 liuxuejin 2011-03-08  
没有说hashmap里面对hashcode进行hash的函数的作用,注释我看不明白
5 楼 chenyong0214 2011-02-19  
不可否认写的非常好!
4 楼 sepac 2011-02-17  
顶一个!没看过源码的路过!希望楼主继续!Java新手天天有!需要你的帮助!
3 楼 kingkan 2011-02-16  
kakaluyi 写道
链表为了解决哈希冲突,还有一种方式是再hash,但是jdk里面用到的是链表法来解决hash冲突的问题


再hash的话,不如链表快了。
2 楼 kakaluyi 2011-02-16  
链表为了解决哈希冲突,还有一种方式是再hash,但是jdk里面用到的是链表法来解决hash冲突的问题
1 楼 superobin 2011-02-16  
第一种做法肯定没问题,就当没改过呗
第二种做法除非p2 equals p3 ,否则肯定拿不出来。

相关推荐

    jdk-8u11-linux-x64.tar.gz

    Java Development Kit (JDK) 是Java编程语言的核心组件,它为开发者提供了编译、调试和运行Java应用程序所需的所有工具。`jdk-8u11-linux-x64.tar.gz` 是Oracle公司发布的JDK 8更新11版本的Linux 64位版本的压缩包...

    jdk-8u191-linux-x64.tar.gz

    总之,"jdk-8u191-linux-x64.tar.gz"是一个针对64位Linux系统的Java 8更新191版本的安装包,包含了一系列增强的编程特性,如Lambda表达式、Stream API和新的日期时间API,以及Nashorn JavaScript引擎。安装这个JDK...

    jdk-8u333-linux-x64.tar.gz

    总之,JDK 8对于Java开发者来说是一个重要的里程碑,它引入了一系列创新特性,提升了开发效率。在Linux环境下,安装和配置JDK 8是一项基础任务,需要理解相关步骤和环境变量的设置。通过正确安装和配置,开发者可以...

    最新版linux jdk-8u251-linux-x64.tar.gz

    - **javac**:Java编译器,将源代码编译成可执行的字节码。 - **java**:JVM的启动器,用于执行`.class`文件。 - **jar**:打包工具,可以将多个类文件打包成一个`.jar`文件。 - **javadoc**:生成API文档的工具...

    jdk-8u144-linux-x64.tar.gz

    因此,对于个人和开源项目,推荐使用OpenJDK,它是一个开放源代码的JDK实现,同样支持Java 8,并且在CentOS上安装过程类似。 总的来说,`jdk-8u144-linux-x64.tar.gz`是一个在64位Linux环境下进行Java开发的基础,...

    jdk-8u152-linux-x64.tar.gz 【jdk1.8,jdk8,linux 64位版】

    - **javac**: Java编译器,将源代码编译成可执行的字节码(.class文件)。 - **java**: Java解释器,负责运行编译后的Java类文件。 - **jar**: Jar工具,用于创建、修改和提取Java档案(JAR)文件。 - **javadoc**: ...

    jdk-8u172-linux-x64.tar.zip

    Java Development Kit(JDK)是Java编程语言的核心组件,它为开发者提供了编译、调试和运行Java应用程序所需的所有工具。"jdk-8u172-linux-x64.tar.gz"是一个针对Linux操作系统的64位版本JDK的压缩包文件。这个文件...

    jdk-8u181-linux-x64.tar.gz

    - **javac**:Java编译器,将源代码编译成字节码。 - **java**:Java虚拟机(JVM),用于运行Java应用程序。 - **jar**:Java归档工具,用于打包和管理类库。 - **javadoc**:生成API文档的工具。 - **jps**,**...

    Linux x64下jdk1.8:jdk-8u211-linux-x64.tar.gz

    - **Date和Time API**:全新的API替代了过时的`java.util.Date`和`java.util.Calendar`,提供了更好的日期和时间处理能力。 - **类型注解**:增强了类型系统,可以在类型层次上使用注解,有助于静态分析和代码生成...

    jdk-8u191-linux-x64 .tar.gz

    Java Development Kit(JDK)是Java编程语言的核心组件,它包含了一组开发工具,用于编写、编译、调试和运行Java应用程序。标题中的"jdk-8u191-linux-x64 .tar.gz"指的是Oracle JDK 8的第191次更新的64位Linux版本,...

    jdk-8u202-windows-x64.zip

    1. 使用JDK提供的`javac`编译器将源代码(.java文件)编译成字节码(.class文件)。 2. 使用`java`命令执行已编译的字节码,运行Java应用程序。 3. 使用`jar`工具打包和提取Java应用,创建可执行的JAR文件。 总结,...

    jdk-8u271-linux-x64.tar.gz.rar

    使用`javac`命令编译Java源代码,`java`命令运行编译后的字节码。 7. **JDK 8的新特性**: - Lambda表达式:这是一种简洁的函数式编程语法,使得处理集合数据更加高效。 - 方法引用和构造器引用:它们简化了函数...

    jdk-8u162-linux-x64.tar.gz

    6. **安装完成后,你可以开始编写Java代码,使用`javac`编译器将源代码编译成字节码,再用`java`命令执行。 JDK 1.8在开发社区中被广泛使用,其丰富的功能和改进极大地提升了Java开发者的效率。了解和熟练掌握这些...

    jdk1.8(jdk-8-windows-x86.rar)

    - 使用JDK 1.8开发Java应用,可以通过`javac`命令编译源代码,然后使用`java`命令运行字节码文件。 - 对于开发工具,如Eclipse、IntelliJ IDEA等,需要配置相应的JDK版本以支持JDK 1.8的新特性。 ### 总结 JDK 1.8...

    jdk-8u91-linux-x64.tar.gz

    使用JDK的`javac`编译器将源代码转化为字节码,然后使用`java`命令运行程序。开发者也可以使用集成开发环境(IDEs)如Eclipse或IntelliJ IDEA来提升开发效率。 总之,`jdk-8u91-linux-x64.tar.gz`是Java开发的关键...

    jdk-8u333-linux.zip

    9. **开发与调试**: 使用这个JDK版本,开发者可以使用javac编译Java源代码,使用jar打包工具创建可执行的jar文件,使用javadoc生成API文档,以及使用jdb进行调试。同时,JDK还包含了Java虚拟机(JVM),用于执行编译...

    java-jdk1.8-jdk-8u191-windows-x64.zip

    Java JDK 1.8是Java开发工具包的一个重要版本,主要针对Windows x64操作系统设计。JDK(Java Development Kit)是开发和运行Java应用程序所必需的软件集合,包括Java编译器、Java运行环境、类库以及各种工具。在这个...

    jdk-8u144-linux-i586.tar.gz

    这个版本的JDK包含了Java运行环境(Java Runtime Environment,JRE)和一系列开发工具,如Java编译器(javac)、Java虚拟机(JVM)、Java文档生成器(javadoc)以及性能分析工具等。 1. **JDK的核心组件**: - **...

    jdk-8u201-linux-x64_.tar.gz

    1. **黑名单正则.java**:这是一个Java源代码文件,可能包含了一个用于处理黑名单的正则表达式逻辑。在Java中,正则表达式可以用来进行字符串匹配、查找、替换等操作,对于数据过滤和验证非常有用。 2. **jdk1.8.0_...

    jetty-util-8.1.8.v20121106-API文档-中文版.zip

    赠送源代码:jetty-util-8.1.8.v20121106-sources.jar; 赠送Maven依赖信息文件:jetty-util-8.1.8.v20121106.pom; 包含翻译后的API文档:jetty-util-8.1.8.v20121106-javadoc-API文档-中文(简体)版.zip; Maven...

Global site tag (gtag.js) - Google Analytics