HashTable,HashMap,TreeMap

共性和差异

  • 共性
都是最常见Map实现,以键值对形式存储、操作数据的容器类型
  • HashTable
是最早期Java提供一个hash表实现,本身是同步的,线程安全,不支持null键、值,同步导致性能开销,很少推荐使用
  • HashMap
应用更加广泛的hash表实现,行为与HashTable一致,主要区别于HashMap不支持同步,支持null键值,HashMap进行put、get操作,可以达到常数时间性能,所以其实绝大部分利用键值对存取场景的首选。
如实现一个用户ID和用户信息对应的运行时存储结构。
  • TreeMap
基于红黑树的一种提供顺序访问的Map,与HashMap不同,其get、put、remove之类操作都是O(log(n))时间复杂度,具体顺序由指定Comparator来决定,或根据键的自然顺序来判断。

Map整体结构

  • HashTable
比较特别,作为类似Vector、Stack的早期集合相关类型,其扩展了Dictionary类,类结构上与HashMap之类明显不同。
HashMap等其它Map实现则是扩展了AbstractMap,里面包含了通用方法抽象。不同Map用途,从类图结构就能体现出来,设计目的已经体现在不同接口。
  • 最佳实践
大部分Map使用场景,通常是放入、访问或者删除,对顺序无特殊要求,HashMap是最好的选择,其性能变现非常依赖于HashCode有效性,务必掌握HashCode和equals一些基本约定
- equals相等,hashcode一定要相等
- 重写hashcode也要重写equals
- hashCode需要保持一致性,状态改变返回的hash值仍然要一致。
- equals对称、反射、传递等特性。
  • 有序Map-LinkedHashMap、TreeMap
二者都可以保证某种顺序,但是有差异
LinkedHashMap通常提供的是遍历顺序符合插入顺序,其实现是通过为键值对维护一个双向链表。注意,通过特定构造函数,可创建反映访问顺序的实例,所谓put、get、compute等,都算作"访问"
如:构建空间占用敏感的资源池,希望可以自动将最不常用的对象释放掉,即可利用LinkedHashMap提供机制实现

import java.util.LinkedHashMap;
import java.util.Map;  
public class LinkedHashMapSample {
    public static void main(String[] args) {
        LinkedHashMap<String, String> accessOrderedMap = new LinkedHashMap<String, String>(16, 0.75F, true){
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { 
			// 实现自定义删除策略,否则行为就和普遍Map没有区别
                return size() > 3;
            }
        };
        accessOrderedMap.put("Project1", "Valhalla");
        accessOrderedMap.put("Project2", "Panama");
        accessOrderedMap.put("Project3", "Loom");
        accessOrderedMap.forEach( (k,v) -> {
            System.out.println(k +":" + v);
        });
        // 模拟访问
        accessOrderedMap.get("Project2");
        accessOrderedMap.get("Project2");
        accessOrderedMap.get("Project3");
        System.out.println("Iterate over should be not affected:");
        accessOrderedMap.forEach( (k,v) -> {
            System.out.println(k +":" + v);
        });
        // 触发删除
        accessOrderedMap.put("Project4", "Mission Control");
        System.out.println("Oldest entry should be removed:");
        accessOrderedMap.forEach( (k,v) -> {// 遍历顺序不变
            System.out.println(k +":" + v);
        });
    }
}

  • TreeMap
对于TreeMap,它的整体顺序由键的顺序关系决定,通过Comparator或Comparable(自然顺序)来决定
如构造一个具有优先级的调度系统,本质是一个典型有限队列场景,Java标准库提供基于二叉堆实现的PriorityQueue,他们都是依赖于同一种排序机制,当然也包括TreeMap的马甲TreeSet
类似HashCode和equals的约定,为了避免模棱两可情况,自然顺序同样需要符合一个约定,就是CompareTo返回值要和equals一致
如TreeMap的源码:

import java.util.LinkedHashMap;
import java.util.Map;  
public class LinkedHashMapSample {
    public static void main(String[] args) {
        LinkedHashMap<String, String> accessOrderedMap = new LinkedHashMap<String, String>(16, 0.75F, true){
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { // 实现自定义删除策略,否则行为就和普遍Map没有区别
                return size() > 3;
            }
        };
        accessOrderedMap.put("Project1", "Valhalla");
        accessOrderedMap.put("Project2", "Panama");
        accessOrderedMap.put("Project3", "Loom");
        accessOrderedMap.forEach( (k,v) -> {
            System.out.println(k +":" + v);
        });
        // 模拟访问
        accessOrderedMap.get("Project2");
        accessOrderedMap.get("Project2");
        accessOrderedMap.get("Project3");
        System.out.println("Iterate over should be not affected:");
        accessOrderedMap.forEach( (k,v) -> {
            System.out.println(k +":" + v);
        });
        // 触发删除
        accessOrderedMap.put("Project4", "Mission Control");
        System.out.println("Oldest entry should be removed:");
        accessOrderedMap.forEach( (k,v) -> {// 遍历顺序不变
            System.out.println(k +":" + v);
        });
    }
}

当不遵守约定时,两个不符合唯一性(equals)要求的对象被当做是一个(compareTo返回0),这会导致歧义的行为表现。

HashMap源码分析

  • HashMap内部实现分析
内部可以看做数组(Node<K,V>[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过hash值决定键值对在数组的寻址;hash值相等时,则以链表形式存储
如果链表超过阈值(TREEIFY_THRESHOLD,8),链表就会改造成树性结构(元素过多时,树形结构查询效率快于链表结构,但内存占用增加,链表元素长度遵循泊松分布,出现概率很小)

1.从非拷贝构造函数实现来看,这个表格(数组)似乎并没有在最初初始化好,仅仅设置初始值,如下

public HashMap(int initialCapacity, float loadFactor){  
    // ... 
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}


2.HashMap也遵循lazy-load原则来保证性能,首次初始化时(拷贝构造函数,仅介绍最通用场景).从put方法看出,还有putVal()调用
putVal方法有做了如下工作:
(1).如果表格是null,resize方法并把它负责初始化它,这从tab = resize()可以看出。
(2).resize方法兼顾两个职责,创建初始存储表格;容量不满足需求时,进行扩容
(3).放置新的键值对时,如果表格新增的容量大于表格的初始化的容量,则扩容中一倍
(4).具体键值对在hash表格中的位置(数组index)取决于下位运算: i = (n-1) & hash
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}



final V putVal(int hash, K key, V value, boolean onlyIfAbent,
               boolean evit) {
    Node<K,V>[] tab; Node<K,V> p; int , i;
    if ((tab = table) == null || (n = tab.length) = 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == ull)
        tab[i] = newNode(hash, key, value, nll);
    else {
        // ...
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for first 
           treeifyBin(tab, hash);
        //  ... 
     }
}

3.HashMap的key不是本身的hashcode,而是通过高位数据移位到低位进行异或运算
原因是:有些数据计算出的hash值差异主要在高位,而hashMap里的hash寻址是忽略容量以上的高位的,该处理可以有效避免类似情况的hash碰撞
static final int hash(Object kye) {
    int h;
	//有些数据计算出的hash值差异主要在高位,而hashMap里的hash寻址是忽略容量以上的高位的,该处理可以有效避免类似情况的hash碰撞
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
}

4.我们提到的链表结构(此处叫bin),会在达到一定门限时,发生树化,需要对bin进行处理,putVal()方法逻辑相对集中,包含hashMap初始化、扩容、树化,全部和他有关


5.reszie()
依据源码,不考虑极端情况(容量理论最大极限MAXIMUM_CAPACITY 指定,数值为1<<30 ,即2的30次方)可归纳为:门限值等于(负载因子)X(容量),如果构建hashMap为指定就是根据相应默认值来设置
门限通常以倍数进行调整(newThr = oldThr << 1),如前面putVal中的逻辑,当元素超过门限(map容量),则调整map大小
扩容后,需要将老的元素重新放到新的数组,这是扩容的一个主要开销
final Node<K,V>[] resize() {
    // ...
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACIY &&
                oldCap >= DEFAULT_INITIAL_CAPAITY)
        newThr = oldThr << 1; // double there
       // ... 
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {  
        // zero initial threshold signifies using defaultsfults
        newCap = DEFAULT_INITIAL_CAPAITY;
        newThr = (int)(DEFAULT_LOAD_ATOR* DEFAULT_INITIAL_CAPACITY;
    }
    if (newThr ==0) {
        float ft = (float)newCap * loadFator;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
    }
    threshold = neThr;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newap];
    table = n;
    // 移动到新的数组结构e数组结构 
   }

容量、负载因子和树化

  • 三者关系
容量和负载因子决定可用桶的数量,空桶太多浪费空间,太满严重影响操作性能。所以性能跟负载因子有个
极端情况下,假设一个桶,那么就会退化为链表,完全不能提供常数时间存的性能
  • 容量,负载因子设置策略
如果事先知道hashmap存取键值对数量,可以考虑一些设置容量大小,具体如下:负载因子 * 容量 > 元素数量
预估容量要大于"预估元素数量/负载因子",同时是2的幂数
负载因子相关建议:
1.如无特别要求,不要更改,jdk默认负载因子符合通用场景需求
2.如确实需调整,不超过0.75(过大会增加hash冲突,严重影响操作效率)
3.如果使用过小负载因子,会导致空间浪费,而且会频繁进行扩展,产生无谓开销,本身访问性能受影响。

树化改造

  • bin数量与TREEIFY_THRESHOLD,MIN_TREEIFY_CAPACITY
如果bin数量大于TREEIFY_THRESHOLD时,容量小于MIN_TREEIFY_CAPACITY ,会进行简单扩容一倍;如果容量大于MIN_TREEIFY_CAPACITY则会进行树化改造

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //树化改造逻辑
    }
}
  • 树化改造原因
**本质是安全问题**。因为元素放置过程,如有hash冲突,则都会放到一个链表里,查询链表效率是线性,严重影响存取性能,使用树化结构会是其达到o(log(n))
现实世界构造hash冲突不复杂,会遇到恶意代码利用hash冲突进行攻击,利用大量hash值冲突的数据与服务器交互,导致CPU大量占用
发布了150 篇原创文章 · 获赞 15 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/dymkkj/article/details/104469864