1、HashMap
HashMap是Map接口最常见的实现,HashMap是非线程安全的,其内部实现是一种基于一个数组和链表的结合体,如下table为HashMap中存储数据的字段:
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; ...... }
上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
加载因子:
final float loadFactor;
默认加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认容量:
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30;
实际扩展容量边界:
int threshold;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
HashMap的初始容量为capacity而不是参数initialCapacity:
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;//左移并赋值 table = new Entry[capacity];
如果执行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9。
当 我们往hashmap中put元素的时候,先根据key的hash值得到这个元素 在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表 的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元 素,然后通过key的equals方法在对应位置的链表中找到需要的元素。如果每个位置上的链表只有一个元素,那么 hashmap的get效率将是最高的。
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
HashMap充许key为null,put对象时,对于key为null的情况,HashMap的做法为获取Entry数组的第一个Entry对象,并基于Entry对象的next属性遍历,当找到了其中的Entry对象的key为null时,则将其中value赋值为新的value然后返回,如果没有key为null的Entry,则增加一个Entry对象:
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
hashmap 中要找到某个元素,需要根据key的hash值来求得对应数组中的位 置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的 分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再 去遍历链表。
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的:
static int indexFor(int h, int length) { return h & (length-1); }HashMap的数组长度永远都是2的n次方,数组的长度-1的二进制永远全部都是1,再与key的hashcode值做 一次“与”运算 (&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑 问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时 hashmap访问的性能最高。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面 annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询 的效率。 所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始 化的,代码如下(HashMap的构造方法中):
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值:
for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
如果遍历完了该位置的链表都没有找到有key相等的,那么将当前对象增加到链表的表头去:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
当 hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行 扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了, 而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当put一个元素时,如果达到了容量限制,也就是threshold的值,数组大小*loadFactor,就会进行数组扩容,新的容量永远是原来的2倍:
if (size++ >= threshold) resize(2 * table.length);
loadFactor 的 默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那 么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
2、TreeMap
TreeMap是一个支持排序的Map实现,是基于红黑树来实现的,TreeMap是非线程安全的。
3、HashTable
HashTable也是Map接口的一个实现,它是线程安全的,跟HashMap的主要区别有下面几点:
3.1 历史原因,Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现
3.2 HashTable是线程安全的,HashMap是非线程安全的
3.3 在HashMap中,null可以作为键 ,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。HashTable中key和value都不能为null。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry tab[] = table; int hash = key.hashCode();//key为空将抛出异常 int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<K,V>(hash, key, value, e); count++; return null; }
3.4 HashTable使用Enumeration,HashMap使用Iterator。
3.5 HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
3.6 哈希值的使用不同,HashTable直接使用对象的hashCode ,代码是这样的:
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;
而HashMap重新计算hash值,而且用与代替求模 :
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); }
HashMap经验:
由于HashMap扩容会影响性能,在已经知道Map大小的情况下,实例化Map时最好要指定其大小,但不是直接指定,因为Map扩容时其容量并没有满,而是到其容量乘加载因子(0.75),所以这里指定其容量其是用已知的大小除以加载因子,但这里也不要除以.075,最好是除以0.7,这样能确保结果大于除以0.75的。
如把一个List里的元素全部放到HashMap里时:
HashMap<String,Foo> map; void addObjects(List<Foo> input){ map = new HashMap<String, Foo>(); for(Foo f: input){ map.put(f.getId(), f); } }
优化后:
HashMap<String,Foo> _map; void addObjects(List<Foo> input){ map = new HashMap<String, Foo>((int)Math.ceil(input.size() / 0.7)); for(Foo f: input){ map.put(f.getId(), f); } }
相关推荐
Java集合框架中的Map接口是Java编程中非常重要的一个部分,它提供了一种存储键值对数据的方式。在Map中,每个键(key)都是唯一的,用于标识对应的值(value),而值可以重复出现。这种数据结构广泛应用于各种场景,...
Java基础知识汇总之集合框架List、Map、Set接口及其子类综合对比
Java集合框架是Java编程语言中的一个核心组成部分,它为存储、管理和操作对象提供了一套统一的接口和类。本文将深入解析Java集合框架的各个方面,包括Collection、List、Set和Map,以及它们的相关实现和使用原理。 ...
在Java集合框架中,主要有六种核心接口:`Collection`, `Set`, `List`, `Queue`, `Deque`, 和 `Map`。此外,还有五个抽象类以及多个实现类,它们共同构成了Java集合框架的基础。 #### 二、核心接口介绍 1. **`...
xmind格式的Java集合框架学习导图,包括Collection接口/Map接口以及具体实现类。 同样包含大厂面试题,也在导图中有所体现。 能学到什么: 更加成体系的知识框架,更加全面的、系统的知识。 思维导图: 思维导图具有...
### Java集合框架总结 #### 一、Java集合框架概述 Java集合框架是Java标准库的一部分,它提供了一系列的接口和类来存储和操作各种类型的对象集合。这些接口和类遵循一致的设计模式,使得开发人员可以方便地管理和...
Java集合框架主要包括Collection接口和Map接口两大分支。Collection接口主要包括List、Set以及Queue三个子接口,而Map接口则用于存储键值对映射。 1. Collection接口与Map接口的区别: - Collection接口是单列集合...
重点探讨了 Java集合框架中的 Map接口及其主要实现类(HashMap、TreeMap、LinkedHashMap),并通过示例代码展示它们的使用方法。文章还深入讨论了泛型在 Map中的应用及其优缺点,最后介绍了一些高级用法,如自定义 ...
进入Java集合框架的核心,我们有四个主要接口:`Collection`、`List`、`Set`和`Map`。`Collection`是最基础的接口,它是所有集合的父接口,但它不提供`get()`方法,通常我们通过`Iterator`遍历`Collection`。`List`...
Java集合框架是Java编程语言中处理对象集合的一套接口和类。该框架提供了用于存储和操作集合的标准方法。在Java集合框架中,基本的接口分为两大类:Collection和Map。 Collection接口用于表示一组对象,称为其元素...
在处理复杂数据存储时,集合框架是必不可少的工具,而Map接口则是集合框架中的一个重要组成部分。Map接口定义了键值对(key-value pairs)的数据结构,使得我们可以根据键来高效地查找对应的值。 在农业信息系统...
Collection 接口是 Java 集合框架的核心接口,它定义了集合的基本操作,例如添加、删除、查询等。Collection 接口下有三个主要的子接口:List、Set 和 Map。 * List:List 接口继承自 Collection 接口,用于存储...
Java集合框架是Java编程语言中的一个核心组成部分,它为数据存储和操作提供了丰富的类库。在Java中,集合框架主要包括接口(如List、Set、Queue)和实现这些接口的类(如ArrayList、HashSet、LinkedList等)。这个...
本文将深入探讨 Java 集合框架,并详细分析 List、Set 和 Map 之间的区别及其应用场景。 Java 集合框架是 Java 程序设计中不可或缺的一部分,它提供了灵活、高效的方式来处理数据集合。List、Set 和 Map 作为集合...
Java集合框架是Java中处理对象集合的核心工具,它通过一系列接口和类提供了一种统一和高效的方式来操作集合。理解并掌握Collection、List、Set和Map等核心接口,可以帮助我们编写出更加健壮、可读和可维护的代码。...