简介
Java为数据结构中的映射定义了一个接口java.util.Map
1、HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。非线程安全。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
2、Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类。线程安全。并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。
3、LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
4、TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
内部实现
(1) 存储结构-字段
(2) 功能实现-方法
存储结构-字段
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
这里需要讲明白两个问题:数据底层具体存储的是什么?这样的存储方式有什么优点呢?
HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组
1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; //用来定位数组索引位置 3 final K key; 4 V value; 5 Node<K,V> next; //链表的下一个node 6 Node(int hash, K key, V value, Node<K,V> next) { ... } 7 public final K getKey(){ ... } 8 public final V getValue() { ... } 9 public final String toString() { ... } 10 public final int hashCode() { ... } 11 public final V setValue(V newValue) { ... } 12 public final boolean equals(Object o) { ... } 13 }
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
默认容量和加载因子
HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
源码
1 static final int DEFAULT_INITIAL_CAPACITY = 16; 2 static final float DEFAULT_LOAD_FACTOR = 0.75f; 3 int threshold; 4 public HashMap() { 5 this.loadFactor = DEFAULT_LOAD_FACTOR; 6 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); 7 table = new Entry[DEFAULT_INITIAL_CAPACITY]; 8 init(); 9 }
默认的容量是16,而 threshold是16*0.75 = 12;相对准确的估算数据量,将极大的影响HashMap的性能,因为resize是一个重新分配的过程,耗时应该是里面最大的。加载因子较小,会有更多的空间空闲,我不知道这个0.75是不是一个折中方案。也许0.9也是一个不错的选择,特别是那些数据量虽然很大,但不是经常变化的地方,比如公司人员,城市列表等相对比较固定的数据。
小结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。