zoukankan      html  css  js  c++  java
  • HashMap / HashTable / HashSet

    1、HashMap 总结
    Map映射中不能包含重复的键,允许放入keynull的元素,也允许插入valuenull的元素。
    HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
    HashMap 的实现不是同步的,不是线程安全的。

     HashMap 的API

    void                 clear()
    Object               clone()
    boolean              containsKey(Object key)
    boolean              containsValue(Object value)
    Set<Entry<K, V>>     entrySet()
    V                    get(Object key)
    boolean              isEmpty()
    Set<K>               keySet()
    V                    put(K key, V value)
    void                 putAll(Map<? extends K, ? extends V> map)
    V                    remove(Object key)
    int                  size()
    Collection<V>        values()

    entrySet()返回键-值集的Set集合; keySet()返回键集的Set集合; values()返回值集的Collection集合。

    因为Map中不能包含重复的键;每个键最多只能映射到一个值。所以,键-值集、键集都是Set,值集时Collection。

    Map.Entry是Map中内部的一个接口。Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。HashMap通过“拉链法”解决哈希冲突的。

    2、HashMap存储原理 

    系统初始化 HashMap 时,会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。 无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。

    有两个参数可以影响HashMap的性能:初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

    将对象放入到HashMapHashSet中时,有两个方法需要特别关心:hashCode()equals()hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMapHashSet中,需要@Override hashCode()equals()方法。

    HashMap类的put方法:

    public V put(K key, V value) {
        // 如果 key 为 null,调用 putForNullKey 方法进行处理。
        // null的hash值总是0,会被存储到table[0]
        if (key == null)
            return putForNullKey(value);
        // 根据key的hashCode方法返回值,计算 Hash 值
        int hash = hash(key.hashCode());
        // 搜索指定 hash 值在对应 table 中的索引
        int i = indexFor(hash, table.length);
        // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 找到指定 key 与需要放入的 key 相等(hash 值相同通过 equals 比较放回 true)
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry
        modCount++;
        // 将 key、value 添加到 i 索引处
        addEntry(hash, key, value, i);
        return null;
    } 
    • 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。
    • key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。
    • indexFor(hash, table.length)用来计算在table数组中存储Entry对象的精确的索引(即,该对象应该保存在 table 数组的哪个索引处)。
    • 如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。
    • 如果再次放入同样的key,会调用equals()方法来检查key的相等性(key.equals(k)),用当前Entry的value来替换之前的value。
    • 如果两个key不同,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。调用addEntry方法。

    addEntry方法:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 获取指定 bucketIndex 索引处的 Entry
        Entry<K, V> e = table[bucketIndex]; //// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        // 如果 Map 中的 key-value 对的数量超过了极限
        if (size++ >= threshold)
            // 把 table 对象的长度扩充到 2 倍。
            resize(2 * table.length); //
    }

    总结:当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。 

    系统总是将新添加的 Entry对象放入 table 数组的 bucketIndex索引处——如果 bucketIndex 索引处已经有了一个 Entry对象,那新添加的 Entry对象指向原有的 Entry对象(产生一个 Entry链),如果 bucketIndex 索引处没有 Entry对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry对象指向 null,也就是没有产生 Entry链。 

    • size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。 
    • threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。 

    HashMap类的get方法:

    public V get(Object key) {
        // 如果 key 是 null,调用 getForNullKey 取出对应的 value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码
        int hash = hash(key.hashCode());
        // 直接取出 table 数组中指定索引处的值,
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next){
            Object k;
            // 如果该 Entry 的 key 与被搜索 key 相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
    • 对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
    • key的hashcode()方法被调用,然后计算hash值。
    • 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。

    总结:

    • HashMap有一个叫做Entry的内部类,它用来存储key-value对。
    • 上面的Entry对象是存储在一个叫做table的Entry数组中。
    • table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
    • key的hashcode()方法用来找到Entry对象所在的桶。
    • 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
    • key的equals()方法用来确保key的唯一性。
    • value对象的equals()和hashcode()方法根本一点用也没有。

      

    3、HashMap 的遍历

    Map<String, Integer> map = new HashMap<String, Integer>();
    map.put("d", 2);
    map.put("c", 1);
    map.put("b", 1);
    map.put("a", 3);

    (1)遍历 Map.Entry

        Iterator<Map.Entry<String, Integer>> iter = map.entrySet().iterator();
        while(iter.hasNext()){
            Map.Entry<String, Integer> entry = iter.next();
            String key = entry.getKey();
            Integer val = entry.getValue();
        }

    (2)遍历 Key

    Iterator<String> iter = map.keySet().iterator();
    while(iter.hasNext()){
        String key = iter.next();
    }

    (3)遍历 Value

    Iterator<Integer> iter = map.values().iterator();
    while(iter.hasNext()){
        Integer key = iter.next();
    }

    4、HashMap 排序

    List<Map.Entry<String, Integer>> entryList =
            new ArrayList<Map.Entry<String, Integer>>(map.entrySet());
            
    Collections.sort(entryList, new Comparator<Map.Entry<String, Integer>>() {
        @Override
        public int compare(Entry<String, Integer> o1, Entry<String, Integer> o2) {
            // 根据value排序
            return o2.getValue().compareTo(o1.getValue());  // return o2.getValue() - o1.getValue();
            // 根据key排序
            //return o1.getKey().compareTo(o2.getKey());
        }
    });
    
    // after sorted
    for(int i=0; i<entryList.size(); i++){
        Map.Entry<String, Integer> entry = entryList.get(i);
        String key = entry.getKey();
        Integer val = entry.getValue();
        System.out.println(key + " " + val);
    }

    5、HashTable (public class Hashtable extends Dictionary implements Map)

    • Hashtable 中的方法是同步的。HashMap的同步问题可通过Collections的一个静态方法得到解决:Map Collections.synchronizedMap(Map m)
    • Hashtable中,key和value都不允许出现null值。
    • 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
    • Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。 

    6、HashSet

    HashSet封装了一个 HashMap 对象来存储所有的集合元素,对HashSet的函数调用都会转换成合适的HashMap方法。所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。 

    7、集合中键值是否允许null小结

    • List:可以有多个null,可以有重复值
    • HashMap:允许一个null键与多个null值,若重复键,则覆盖以前值
    • TreeMap:不允许null键(实际上可以插入一个null键,如果这个Map里只有一个元素是不会报错的,因为一个元素时没有进行排序操作,也就不会报空指针异常,但如果插入第二个时就会立即报错),但允许多个null值,覆盖已有键值。
    • HashSet:能插入一个null(因为内部是以 HashMap实现 ),忽略不插入重复元素。
    • TreeSet:不能插入null (因为内部是以 TreeMap 实现 ) ,元素不能重复,如果待插入的元素存在,则忽略不插入,对元素进行排序。
    • HashTable:不允许null键与null值(否则运行进报空指针异常)。也会覆盖重复值。

    http://fangjian0423.github.io/2016/03/29/jdk_hashmap/    HashMap原理

    https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet%20and%20HashMap.md    基于jdk1.7 -- HashMap原理

  • 相关阅读:
    《.NET内存管理宝典 》(Pro .NET Memory Management) 阅读指南
    《.NET内存管理宝典 》(Pro .NET Memory Management) 阅读指南
    《.NET内存管理宝典 》(Pro .NET Memory Management) 阅读指南
    使用Jasmine和karma对传统js进行单元测试
    《.NET内存管理宝典 》(Pro .NET Memory Management) 阅读指南
    《.NET内存管理宝典 》(Pro .NET Memory Management) 阅读指南
    nginx 基于IP的多虚拟主机配置
    Shiro 框架的MD5加密算法实现原理
    项目实战:Qt+OSG三维点云引擎(支持原点,缩放,单独轴或者组合多轴拽拖旋转,支持导入点云文件)
    实用技巧:阿里云服务器建立公网物联网服务器(解决阿里云服务器端口,公网连接不上的问题)
  • 原文地址:https://www.cnblogs.com/hesier/p/5627066.html
Copyright © 2011-2022 走看看