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原理

  • 相关阅读:
    Dos.ORM logo.Net轻量级开源ORM框架 Dos.ORM
    C# FUNC 应用
    WCF教程网址
    C#扩展方法实现 byte[] 输出为HEX字符串形式
    apache配置,禁止指定后缀访问
    IServerChannelSinkProvider
    在服务器端的远程对象中加上抽象工厂的接口和实现类
    pannel加载窗体
    权限框架
    工作周记
  • 原文地址:https://www.cnblogs.com/hesier/p/5627066.html
Copyright © 2011-2022 走看看