• 源码阅读(16):Java中主要的Map结构——HashMap容器(上)


    (接上文《源码阅读(15):Java中主要的Map结构——概述》)

    2.4、java.util.AbstractMap抽象类

    AbstractMap抽象类是实现了Map接口的一个抽象类,用来向下层具体的Map容器实现提供了一些默认的功能实现逻辑,以便减少下层具体的Map容器构建过程的代码量,降低自定义Map容器的实现难度。

    2.4.1、AbstractMap抽象类基本介绍

    AbstractMap抽象类中已实现的那些默认逻辑,实际上不能独立运行,因为这些实现过程中的重要过程都是缺失的。例如AbstractMap抽象类中对size()方法进行了实现,如下所示:

    public abstract class AbstractMap<K,V> implements Map<K,V> {
      // ......
      public int size() {
        return entrySet().size();
      } 
      // ......
    }
    

    仔细观察就会发现,以上代码片段使用了Map接口中定义的entrySet()方法,后者将返回Map容器中存储的Map.Entry对象集合。并且通过entrySet()方法返回的Map.Entry对象集合还有一个特点,就是其中的对象信息不回重复。Map.Entry接口已经在上文介绍过了,每一个Map.Entry对象,就表示存储在Map容器中的每一个K-V键值对信息。但可惜的是AbstractMap抽象类中并没有对entrySet()方法进行实现,而是要求下层具体的Map容器进行实现:

    public abstract class AbstractMap<K,V> implements Map<K,V> {
      // ......
      public abstract Set<Entry<K,V>> entrySet();
      // ......
    }
    

    例如java.util.HashMap类中对以上方法的实现如下所示:

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {
      // ......
      public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
      }
      // ......
    }
    

    就像上文介绍的那样,AbstractMap抽象类的存在目的是为了减少程序员实现具体Map容器时的编码工作量。AbstractMap抽象类还为下层不同性质的Map容器实现提供了编码建议。例如,如果基于AbstractMap抽象类实现的具体Map容器是一个不可新增K-V键值对信息的容器,那么程序员只需要实现上文中提到的entrySet()方法即可,因为AbstractMap抽象类中定义了不支持put()方法和remove()方法,如下所示:

    public abstract class AbstractMap<K,V> implements Map<K,V> {
      // ......
      public V put(K key, V value) {
        throw new UnsupportedOperationException();
      }
      // ......
    }
    
    // AbstractMap类中实现的remove()方法,最终将依赖使用Iterator迭代器中的remove()方法。
    public interface Iterator<E> {
      default void remove() {
        throw new UnsupportedOperationException("remove");
      }
    }
    

    再例如如果基于AbstractMap抽象类实现的具体Map容器是一个可新增K-V键值对信息的容器,则需要这个具体的Map容器自行实现put()方法,以及entrySet()方法所使用的迭代器中的remove()方法——具体来说就是以上代码片段中提到的java.util.Iterator接口中的remove()方法。

    To implement an unmodifiable map, the programmer needs only to extend thisclass and provide an implementation for the entrySet method, whichreturns a set-view of the map’s mappings. Typically, the returned setwill, in turn, be implemented atop AbstractSet. This set shouldnot support the add or remove methods, and its iteratorshould not support the remove method.
     
    To implement a modifiable map, the programmer must additionally overridethis class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.

    2.4.2、AbstractMap抽象类中的SimpleEntry

    AbstractMap抽象类还提供了一个Map.Entry接口的默认实现实现——AbstractMap.SimpleEntry。后者对一个K-V键值对进行了简单的定义:

    public static class SimpleEntry<K,V>
            implements Entry<K,V>, java.io.Serializable
    {
      private final K key;
      private V value;
      public SimpleEntry(K key, V value) {
        this.key   = key;
        this.value = value;
      }
    
      // ......
      /**
       * Returns the key corresponding to this entry.
       * @return the key corresponding to this entry
       */
      public K getKey() {
          return key;
      }
    
      /**
       * Returns the value corresponding to this entry.
       * @return the value corresponding to this entry
       */
      public V getValue() {
          return value;
      }
      
      // ......
    }
    

    这个实现很简单,以至于直接使用该类的具体容器并不多,举例如下:
    在这里插入图片描述
    比如本专题后文将详细介绍的HashMap容器中,就自行实现了Map.Entry接口,如下所示:

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    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;
      }
      // ......
    }
    

    3、主要的Map结构——HashMap容器

    HashMap容器从字面的理解就是,基于Hash算法构造的Map容器。从数据结构的知识体系来说,HashMap容器是散列表在Java中的具体表达(并非线性表结构)。具体来说就是,利用K-V键值对中键对象的某个属性(默认使用该对象的“内存起始位置”这一属性)作为计算依据进行哈希计算(调用hashCode方法),然后再以计算后的返回值为依据,将当前K-V键值对在符合HashMap容器构造原则的基础上,放置到HashMap容器的某个位置上,且这个位置和之前添加的K-V键值对的存储位置完全独立,不一定构成连续的存储关系。

    hashCode()方法遵循以下默认实现原则:
    1、默认的要求下:在同一进程中的同一对象,无论它的hashCode()方法被调用多少次,返回的值都是一样的。
     
    2、默认要求下,如果根据对象的equals(object)方法进行比较,得到两个对象相等的结果。那么调用两个对象的hashCode()方法就会得到相同的返回值;换句话说,这种情况下,如果调用两个对象的hashCode()方法得到了不同的返回值,那么根据对象的equals(object)方法进行比较,将得到两个对象不相等的结果。

    请记住以上规则,在后续讲解HashMap容器的核心工作原理时,会默认为读者已经知晓以上知识点。这里举例说明Object这个类中的equals(object)方法和hashCode()方法:

    public class Object {
      // Object类中的hashCode方法使用JNI进行实现
      // 实际上就是基于对象的内存起始地址得到一个hash值
      public native int hashCode();
      
      // Object类中的equals()方法,使用两个对象的内存地址起始位置得到对比结果
      // 这两个方法的实现原则结果很明显遵循了上文中提到的原则
      public boolean equals(Object obj) {
        return (this == obj);
      }
    }
    

    3.1、HashMap基本使用

    HashMap容器的使用方法很简单,如下所示:

    // ......
    // 以下代码示例了最简单的HashMap使用方式
    HashMap<String, String> map = new HashMap<>();
    // Key建信息采用字符串作为标识
    // Value值信息也是字符串类型
    map.put("key1", "vlaue1");
    map.put("key2", "vlaue2");
    map.put("key3", "vlaue3");
    map.put("key4", "vlaue4");
    map.put("key5", "vlaue4");
    /*
     * 值信息允许重复,但键信息不允许重复:
     * key3的键信息在这里重新关联的值是valueX
     * 那么之前的value3的值信息将被替换
     * */
    map.put("key3", "vlaueX");
    
    // 通过以下迭代器的输出,我们可以知道
    // 若干键信息在HashMap中并不是顺序存储的
    Set<String> keys = map.keySet();
    for (String key : keys) {
      System.out.println(String.format("key = %s and key's value = %s"  , key , map.get(key)));
    }
    

    以上代码的执行后输出的效果为:

    key = key1 and key's value = vlaue1
    key = key2 and key's value = vlaue2
    key = key5 and key's value = vlaue4
    key = key3 and key's value = vlaueX
    key = key4 and key's value = vlaue4
    

    除了java.lang.String类型的Value信息以外,HashMap可以存储任意类型的Value信息,只不过如果要对HashMap进行序列化/反序列化,就需要这个Value信息的类实现了java.io.Serializable标识接口,关于HashMap的序列化和反序列化过程在本文后续有专门讲解。以下代码示意了的Value信息类型是由程序员自定义的MyObject类:

    // 当然value信息的类型还可以是其它类,例如我们自定义的MyObject类
    HashMap<String, MyObject> myMap = new HashMap<>();
    myMap.put("key2", new MyObject("field21", "filed22"));
    myMap.put("key3", new MyObject("field31", "filed32"));
    myMap.put("key4", new MyObject("field41", "filed42"));
    myMap.put("key5", new MyObject("field51", "filed52"));
    myMap.put("key6", new MyObject("field61", "filed62"));
    // 同样通过以下迭代也可以观察到key并不是顺序存储的
    Set<String> keys = myMap.keySet();
    for (String key : keys) {
      System.out.println(String.format("key = %s and key's value = {%s}"  , key , myMap.get(key)));
    }
    
    // 这是程序员自定义的MyObject类
    static class MyObject {
      private String filed1;
      private String filed2;
      // ...... 还有很多属性(get/set就不再赘述了)
      public String toString() {
        return String.format("field1 = %s , field2 = %s", this.filed1 , this.filed2);
      }
    }
    

    当然HashMap中的key信息也可以是除了除了java.lang.String类以外的任何类定义,但是Key的类型在整个HashMap容器工作机制中就比较微妙了,本专题将通过后文的详细介绍,力图把这个问题讲清楚。

    ================
    (接下文《源码阅读(17):Java中主要的Map结构——HashMap容器(中)》)

  • 相关阅读:
    slf4j+log4j的使用
    <context:component-scan>详解
    Spring装配Bean---使用xml配置
    Spring应用上下文中Bean的生命周期
    bootstrap table 复选框选中后,翻页之后保留先前选中数据
    前后端分离的时代,如何解决前后端接口联调问题?
    利用vue-cli搭建vue项目
    vue之注册自定义的全局js函数
    小程序之图片上传
    微信小程序-蓝牙连接
  • 原文地址:https://www.cnblogs.com/liulaolaiu/p/11744199.html
Copyright © 2020-2023  润新知