HashMap原理、源码、实践

本文详细解析了HashMap的工作原理,包括其内部实现、存取机制、冲突处理等关键内容,并对比了HashMap与其他相关数据结构如LinkedHashMap和WeakHashMap的区别。

HashMap是一种十分常用的数据结构,作为一个应用开发人员,对其原理、实现的加深理解有助于更高效地进行数据存取。本文所用的jdk版本为1.5。 

使用HashMap 

《Effective JAVA》中认为,99%的情况下,当你覆盖了equals方法后,请务必覆盖hashCode方法。默认情况下,这两者会采用Object的“原生”实现方式,即: 

Java代码   收藏代码
  1. protected native int hashCode();  
  2. public boolean equals(Object obj) {  
  3.     return (this == obj);  
  4. }  



hashCode方法的定义用到了native关键字,表示它是由C或C++采用较为底层的方式来实现的,你可以认为它返回了该对象的内存地址;而缺省equals则认为,只有当两者引用同一个对象时,才认为它们是相等的。如果你只是覆盖了equals()而没有重新定义hashCode(),在读取HashMap的时候,除非你使用一个与你保存时引用完全相同的对象作为key值,否则你将得不到该key所对应的值。 

另一方面,你应该尽量避免使用“可变”的类作为HashMap的键。如果你将一个对象作为键值并保存在HashMap中,之后又改变了其状态,那么HashMap就会产生混乱,你所保存的值可能丢失(尽管遍历集合可能可以找到)。可参考http://www.ibm.com/developerworks/cn/java/j-jtp02183/ 

HashMap存取机制 

Hashmap实际上是一个数组和链表的结合体,利用数组来模拟一个个桶(类似于Bucket Sort)以快速存取不同hashCode的key,对于相同hashCode的不同key,再调用其equals方法从List中提取出和key所相对应的value。 

JAVA中hashMap的初始化主要是为initialCapacity和loadFactor这两个属性赋值。前者表示hashMap中用来区分不同hash值的key空间长度,后者是指定了当hashMap中的元素超过多少的时候,开始自动扩容,。默认情况下initialCapacity为16,loadFactor为0.75,它表示一开始hashMap可以存放16个不同的hashCode,当填充到第12个的时候,hashMap会自动将其key空间的长度扩容到32,以此类推;这点可以从源码中看出来: 

Java代码   收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6. }  



而每当hashMap扩容后,内部的每个元素存放的位置都会发生变化(因为元素的最终位置是其hashCode对key空间长度取模而得),因此resize方法中又会调用transfer函数,用来重新分配内部的元素;这个过程成为rehash,是十分消耗性能的,因此在可预知元素的个数的情况下,一般应该避免使用缺省的initialCapacity,而是通过构造函数为其指定一个值。例如我们可能会想要将数据库查询所得1000条记录以某个特定字段(比如ID)为key缓存在hashMap中,为了提高效率、避免rehash,可以直接指定initialCapacity为2048。 

另一个值得注意的地方是,hashMap其key空间的长度一定为2的N次方,这一点可以从一下源码中看出来: 

Java代码   收藏代码
  1. int capacity = 1;  
  2. while (capacity < initialCapacity)   
  3.     capacity <<= 1;  



即使我们在构造函数中指定的initialCapacity不是2的平方数,capacity还是会被赋值为2的N次方。 

为什么Sun Microsystem的工程师要将hashMap key空间的长度设为2的N次方呢?这里参考R.W.Floyed给出的衡量散列思想的三个标准: 


    • 一个好的hash算法的计算应该是非常快的 

 

    • 一个好的hash算法应该是冲突极小化 

 

    • 如果存在冲突,应该是冲突均匀化 




为了将各元素的hashCode保存至长度为Length的key数组中,一般采用取模的方式,即index = hashCode % Length。不可避免的,存在多个不同对象的hashCode被安排在同一位置,这就是我们平时所谓的“冲突”。如果仅仅是考虑元素均匀化与冲突极小化,似乎应该将Length取为素数(尽管没有明显的理论来支持这一点,但数学家们通过大量的实践得出结论,对素数取模的产生结果的无关性要大于其它数字)。为此,Craig Larman and Rhett Guthrie《Java Performence》中对此也大加抨击。为了弄清楚这个问题,Bruce Eckel(Thinking in JAVA的作者)专程采访了java.util.hashMap的作者Joshua Bloch,并将他采用这种设计的原因放到了网上(http://www.roseindia.net/javatutorials/javahashmap.shtml) 。 

上述设计的原因在于,取模运算在包括JAVA在内的大多数语言中的效率都十分低下,而当除数为2的N次方时,取模运算将退化为最简单的位运算,其效率明显提升(按照Bruce Eckel给出的数据,大约可以提升5~8倍) 。看看JDK中是如何实现的: 

Java代码   收藏代码
  1. static int indexFor(int h, int length) {  
  2.     return h & (length-1);  
  3. }  



当key空间长度为2的N次方时,计算hashCode为h的元素的索引可以用简单的与操作来代替笨拙的取模操作!假设某个对象的hashCode为35(二进制为100011),而hashMap采用默认的initialCapacity(16),那么indexFor计算所得结果将会是100011 & 1111 = 11,即十进制的3,是不是恰好是35 Mod 16。 

上面的方法有一个问题,就是它的计算结果仅有对象hashCode的低位决定,而高位被统统屏蔽了;以上面为例,19(10011)、35(100011)、67(1000011)等就具有相同的结果。针对这个问题, Joshua Bloch采用了“防御性编程”的解决方法,在使用各对象的hashCode之前对其进行二次Hash,参看JDK中的源码: 

Java代码   收藏代码
  1. static int hash(Object x) {  
  2.         int h = x.hashCode();  
  3.         h += ~(h << 9);  
  4.         h ^=  (h >>> 14);  
  5.         h +=  (h << 4);  
  6.         h ^=  (h >>> 10);  
  7.         return h;  
  8.     }  



采用这种旋转Hash函数的主要目的是让原有hashCode的高位信息也能被充分利用,且兼顾计算效率以及数据统计的特性,其具体的原理已超出了本文的领域。 

加快Hash效率的另一个有效途径是编写良好的自定义对象的HashCode,String的实现采用了如下的计算方法: 

Java代码   收藏代码
  1. for (int i = 0; i < len; i++) {  
  2. h = 31*h + val[off++];  
  3. }  
  4. hash = h;  



这种方法HashCode的计算方法可能最早出现在Brian W. Kernighan和Dennis M. Ritchie的《The C Programming Language》中,被认为是性价比最高的算法(又被称为times33算法,因为C中乘数常量为33,JAVA中改为31),实际上,包括List在内的大多数的对象都是用这种方法计算Hash值。 

另一种比较特殊的hash算法称为布隆过滤器,它以牺牲细微精度为代价,换来存储空间的大量节俭,常用于诸如判断用户名重复、是否在黑名单上等等,可以参考李开复的数学之美系列第13篇(http://googlechinablog.com/2006/08/blog-post.html) 

Fail-Fast机制 

众所周知,HashMap不是线程安全的集合类。但在某些容错能力较好的应用中,如果你不想仅仅因为1%的可能性而去承受hashTable的同步开销,则可以考虑利用一下HashMap的Fail-Fast机制,其具体实现如下: 

Java代码   收藏代码
  1. Entry<K,V> nextEntry() {   
  2. if (modCount != expectedModCount)  
  3.     throw new ConcurrentModificationException();  
  4.                      ……  
  5. }  



其中modCount为HashMap的一个实例变量,并且被声明为volatile,表示任何线程都可以看到该变量被其它线程修改的结果(根据JVM内存模型的优化,每一个线程都会存一份自己的工作内存,此工作内存的内容与本地内存并非时时刻刻都同步,因此可能会出现线程间的修改不可见的问题) 。使用Iterator开始迭代时,会将modCount的赋值给expectedModCount,在迭代过程中,通过每次比较两者是否相等来判断HashMap是否在内部或被其它线程修改。HashMap的大多数修改方法都会改变ModCount,参考下面的源码: 

Java代码   收藏代码
  1. public V put(K key, V value) {  
  2.     K k = maskNull(key);  
  3.         int hash = hash(k);  
  4.         int i = indexFor(hash, table.length);  
  5.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  6.             if (e.hash == hash && eq(k, e.key)) {  
  7.                 V oldValue = e.value;  
  8.                 e.value = value;  
  9.                 e.recordAccess(this);  
  10.                 return oldValue;  
  11.             }  
  12.         }  
  13.         modCount++;  
  14.         addEntry(hash, k, value, i);  
  15.         return null;  
  16.     }  



以put方法为例,每次往HashMap中添加元素都会导致modCount自增。其它诸如remove、clear方法也都包含类似的操作。 
从上面可以看出,HashMap所采用的Fail-Fast机制本质上是一种乐观锁机制,通过检查状态——没有问题则忽略——有问题则抛出异常的方式,来避免线程同步的开销,下面给出一个在单线程环境下发生Fast-Fail的例子: 

Java代码   收藏代码
  1. class Test {    
  2.     public static void main(String[] args) {               
  3.         java.util.HashMap<Object,String> map=new java.util.HashMap<Object,String>();    
  4.        map.put(new Object(), "a");    
  5.        map.put(new Object(), "b");    
  6.        java.util.Iterator<Object> it=map.keySet().iterator();    
  7.        while(it.hasNext()){    
  8.            it.next();    
  9.            map.put("", "");         
  10.         System.out.println(map.size());    
  11.     }    
  12. }  



运行上面的代码会抛出java.util.ConcurrentModificationException,因为在迭代过程中修改了HashMap内部的元素导致modCount自增。若将上面代码中 map.put(new Object(), "b") 这句注释掉,程序会顺利通过,因为此时HashMap中只包含一个元素,经过一次迭代后已到了尾部,所以不会出现问题,也就没有抛出异常的必要了。 
在通常并发环境下,还是建议采用同步机制。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止意外的非同步访问。 

LinkedHashMap 

遍历HashMap所得到的数据是杂乱无章的,这在某些情况下客户需要特定遍历顺序时是十分有用的。比如,这种数据结构很适合构建 LRU 缓存。调用 put 或 get 方法将会访问相应的条目(假定调用完成后它还存在)。putAll 方法以指定映射的条目集合迭代器提供的键-值映射关系的顺序,为指定映射的每个映射关系生成一个条目访问。Sun提供的J2SE说明文档特别规定任何其他方法均不生成条目访问,尤其,collection 集合类的操作不会影响底层映射的迭代顺序。 

LinkedHashMap的实现与 HashMap 的不同之处在于,前者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是集合中元素的插入顺序。该类定义了header、before与after三个属性来表示该集合类的头与前后“指针”,其具体用法类似于数据结构中的双链表,以删除某个元素为例: 

Java代码   收藏代码
  1. private void remove() {  
  2.        before.after = after;  
  3.        after.before = before;  
  4. }  



实际上就是改变前后指针所指向的元素。 

显然,由于增加了维护链接列表的开支,其性能要比 HashMap 稍逊一筹,不过有一点例外:LinkedHashMap的迭代所需时间与其的所包含的元素成比例;而HashMap 迭代时间很可能开支较大,因为它所需要的时间与其容量(分配给Key空间的长度)成比例。一言以蔽之,随机存取用HashMap,顺序存取或是遍历用LinkedHashMap。 

LinkedHashMap还重写了removeEldestEntry方法以实现自动清除过期数据的功能,这在HashMap中是无法实现的,因为后者其内部的元素是无序的。默认情况下,LinkedHashMap中的removeEldestEntry的作用被关闭,其具体实现如下: 

Java代码   收藏代码
  1. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {  
  2.     return false;  
  3. }  



可以使用如下的代码覆盖removeEldestEntry: 

Java代码   收藏代码
  1. private static final int MAX_ENTRIES = 100;  
  2.   
  3. protected boolean removeEldestEntry(Map.Entry eldest) {  
  4.     return size() > MAX_ENTRIES;  
  5. }  



它表示,刚开始,LinkedHashMap中的元素不断增长;当它内部的元素超过MAX_ENTRIES(100)后,每当有新的元素被插入时,都会自动删除双链表中最前端(最旧)的元素,从而保持LinkedHashMap的长度稳定。 

缺省情况下,LinkedHashMap采取的更新策略是类似于队列的FIFO,如果你想实现更复杂的更新逻辑比如LRU(最近最少使用) 等,可以在构造函数中指定其accessOrder为true,因为的访问元素的方法(get)内部会调用一个“钩子”,即recordAccess,其具体实现如下: 

Java代码   收藏代码
  1. void recordAccess(HashMap<K,V> m) {  
  2.     LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
  3.     if (lm.accessOrder) {  
  4.         lm.modCount++;  
  5.         remove();  
  6.         addBefore(lm.header);  
  7.     }  
  8. }  



上述代码主要实现了这样的功能:如果accessOrder被设置为true,则每次访问元素时,都将该元素移至headr的前面,即链表的尾部。将removeEldestEntry与accessOrder一起使用,就可以实现最基本的内存缓存,具体代码可参考http://bluepopopo.iteye.com/blog/180236。 

WeakHashMap 

99%的JAVA教材教导我们不要去干预JVM的垃圾回收机制,但JAVA中确实存在着与其密切相关的四种引用:强引用、软引用、弱引用以及幻象引用。 

JAVA中默认的HashMap采用的是采用类似于强引用的强键来管理的,这意味着即使作为key的对象已经不存在了(指没有任何一个引用指向它),也仍然会保留在HashMap中,在某些情况下(例如内存缓存)中,这些过期的条目可能会造成内存泄漏等问题。 

WeakHashMap采用的策略是,只要作为key的对象已经不存在了(超出生命周期),就不会阻止垃圾收集器清空此条目,即使当前机器的内存并不紧张。不过,由于GC是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象,除非你显示地调用它,可以参考下面的例子: 

Java代码   收藏代码
  1. public static void main(String[] args) {  
  2.     Map<String, String>map = new WeakHashMap<String, String>();  
  3.     map.put(new String("Alibaba"), "alibaba");  
  4.     while (map.containsKey("Alibaba")) {  
  5.         try {  
  6.             Thread.sleep(500);  
  7.          } catch (InterruptedException ignored) {  
  8.          }  
  9.          System.out.println("Checking for empty");  
  10.          System.gc();  
  11.     }  



上述代码输出一次Checking for empty就退出了主线程,意味着GC在最近的一次垃圾回收周期中清除了new String(“Alibaba”),同时WeakHashMap也做出了及时的反应,将该键对应的条目删除了。如果将map的类型改为HashMap的话,由于其内部采用的是强引用机制,因此即使GC被显示调用,map中的条目依然存在,程序会不断地打出Checking for empty字样。另外,在使用WeakHashMap的情况下,若是将 

Java代码   收藏代码
  1. map.put(new String("Alibaba"), "alibaba");   



改为 

Java代码   收藏代码
  1. map.put("Alibaba", "alibaba");   



程序还是会不断输出Checking for empty。这与前面我们分析的WeakHashMap的弱引用机制并不矛盾,因为JVM为了减小重复创建和维护多个相同String的开销,其内部采用了蝇量模式(《JAVA与模式》),此时的“Alibaba”是存放在常量池而非堆中的,因此即使没有对象指向“Alibaba”,它也不会被GC回收。弱引用特别适合以下对象:占用大量内存,但通过垃圾回收功能回收以后很容易重新创建。 

介于HashMap和WeakHashMap之中的是SoftHashMap,它所采用的软引用的策略指的是,垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。软引用对于垃圾收集器来说是一种“睁一只眼,闭一只眼”方式,即 “只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。” 就这一点看,它其实要比WeakHashMap更适合于实现缓存机制。遗憾的是,JAVA中并没有实现相关的SoftHashMap类(Apache和Google提供了第三方的实现),但它却是提供了两个十分重要的类java.lang.ref.SoftReference以及ReferenceQueue,可以在对象应用状态发生改变是得到通知,可以参考com.alibaba.common.collection.SofthashMap中processQueue方法的实现: 

Java代码   收藏代码
  1. private ReferenceQueue queue = new ReferenceQueue();  
  2. ValueCell vc;  
  3. Map hash = new HashMap(initialCapacity, loadFactor);  
  4. ……  
  5. while ((vc = (ValueCell) queue.poll()) != null) {  
  6. if (vc.isValid()) {  
  7.           hash.remove(vc.key);  
  8.            } else {  
  9.              valueCell.dropped--;  
  10.            }  
  11. }  
  12. }  



processQueue方法会在几乎所有SoftHashMap的方法中被调用到,JVM会通过ReferenceQueue的poll方法通知该对象已经过期并且当前的内存现状需要将它释放,此时我们就可以将其从hashMap中剔除。事实上,默认情况下,Alibaba的MemoryCache所使用的就是SoftHashMap。 

http://grunt1223.iteye.com/blog/544497

<think>我们正在讨论Android中的HashMap原理,包括其数据结构实现机制。根据引用内容,我们可以总结以下几点:1.HashMap的主干是一个Entry数组(引用[2])。2.HashMap通过计算散列码决定存储位置,使用链地址法解决哈希冲突(引用[3])。3.Android推荐使用ArrayMap替代HashMap以优化内存(引用[1])。4.遍历HashMap可以通过keySet()或values()方法(引用[4])。下面我将详细解释HashMap原理。###一、HashMap数据结构HashMap采用**数组+链表/红黑树**的结构:```mermaidgraphTDA[Entry数组]-->B[Entry0]A-->C[Entry1]A-->D[...]A-->E[Entryn]B-->F[Entry对象]F-->G[键值对key/value]F-->H[下一节点next]G-->|形成链表|I[下一个Entry]```-**数组**:主干数组`table`,初始为空数组,长度总是2的幂(便于位运算计算索引)-**链表节点**:`Entry`类(Java8后改为`Node`)包含:```javaclassEntry<K,V>{finalKkey;Vvalue;Entry<K,V>next;//指向下一个节点(链表结构)inthash;//键的哈希值}```###二、核心实现机制####1.哈希计算索引定位```java//计算键的哈希值(扰动函数减少碰撞)finalinthash=(key==null)?0:(h=key.hashCode())^(h>>>16);//计算数组索引(n为数组长度)intindex=(n-1)&hash;```>**设计细节**:>-使用`hashCode()`的高16位异或低16位,使哈希分布更均匀[^3]>-`(n-1)&hash`等价于`hash%n`,但位运算更快(要求n为2的幂)####2.解决哈希冲突-**链地址法**:相同索引位置的Entry通过链表连接-**链表转红黑树**:当链表长度≥8且数组长度≥64时,链表转为红黑树(提升查询效率)####3.扩容机制当`size>threshold(容量*负载因子)`时,数组扩容为2倍:```javavoidresize(){intnewCapacity=oldCapacity<<1;//两倍扩容//重新计算每个节点的索引:newIndex=e.hash&(newCapacity-1)}```>**扩容后节点位置变化**:>节点在新数组中的位置要么是**原位置**,要么是**原位置+旧容量**(由hash值的第log2(oldCapacity)位决定)###三、关键操作源码解析####1.插入元素put()```javapublicVput(Kkey,Vvalue){//1.计算hash值inthash=hash(key);//2.计算数组索引iinti=indexFor(hash,table.length);//3.遍历链表/树for(Entry<K,V>e=table[i];e!=null;e=e.next){if(e.hash==hash&&(key==e.key||key.equals(e.key))){VoldValue=e.value;e.value=value;//更新值returnoldValue;}}//4.未找到相同key,创建新节点addEntry(hash,key,value,i);returnnull;}```####2.获取元素get()```javapublicVget(Objectkey){inthash=hash(key);inti=indexFor(hash,table.length);for(Entry<K,V>e=table[i];e!=null;e=e.next){if(e.hash==hash&&(key==e.key||key.equals(e.key)))returne.value;}returnnull;}```###四、Android的特殊优化1.**ArrayMap替代方案**Android推荐使用`ArrayMap`,其采用**双数组结构**(哈希值数组+键值对数组),内存占用更小[^1]:```mermaidgraphLRA[哈希值数组]-->|索引|B[键值对数组]B-->C[key1]B-->D[value1]B-->E[key2]B-->F[value2]```-查找时对哈希数组二分查找,适合小数据量(千级以下)2.**SparseArray特化版本**针对键为整型或长整型的情况,Android提供:-`SparseArray`:键为int,避免自动装箱-`LongSparseArray`:键为long###五、性能对比(HashMapvsArrayMap)|特性|HashMap|ArrayMap||--------------------|------------------|-------------------||内存占用|高(每个对象额外存储)|低(连续数组)||查找时间复杂度|O(1)|O(logn)||插入/删除性能|快(直接定位)|慢(需移动元素)||适用场景|大数据量|小数据量(<1000)|>**最佳实践**:在Android开发中优先使用ArrayMap/SparseArray,除非需要高频操作的大数据集[^1]。§§相关问题§§1.HashMap在多线程环境下会出现什么问题?2.ArrayMap的具体实现机制是怎样的?3.红黑树在HashMap中是如何工作的?4.为什么HashMap要求数组长度为2的幂?</think>### Android HashMap 原理详解(数据结构实现机制) #### 一、核心数据结构 HashMap 采用 **数组 + 链表/红黑树** 的组合结构: ```mermaid graph TD A[Entry数组] --> B[索引0] A --> C[索引1] A --> D[...] A --> E[索引n] B --> F[Entry对象] F --> G[key1/value1] F --> H[next指针] H --> I[Entry对象] I --> J[key2/value2] I --> K[null] ``` 1. **主干数组**(`transient Entry<K,V>[] table`) - 初始为空数组 `EMPTY_TABLE` - 长度总是 2 的幂(便于位运算优化)[^2] 2. **Entry节点**(存储键值对) ```java static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; // 链表指针 int hash; // key的哈希值 } ``` #### 二、核心实现机制 ##### 1. 哈希计算索引定位 ```java // 计算键的哈希值(扰动函数减少碰撞) final int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 计算数组索引(n = table.length) int index = (n - 1) & hash; ``` - **设计要点**: - 高位异或低位:解决低位相同导致的哈希冲突[^3] - `(n-1) & hash` 等价于 `hash % n`(位运算更快) ##### 2. 解决哈希冲突 - **链地址法**:相同索引位置的Entry形成链表 - **链表转红黑树**(JDK8+):当链表长度 ≥ 8 且数组长度 ≥ 64 时,转为红黑树(时间复杂度从 $O(n)$ 降至 $O(\log n)$) ##### 3. 扩容机制(Rehashing) 触发条件:`size > threshold`(`threshold = capacity * loadFactor`) ```java void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; // 创建新数组(2倍扩容) Entry[] newTable = new Entry[newCapacity]; // 遍历旧数组重新分配节点 for (Entry<K,V> e : oldTable) { while (null != e) { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); // 重新计算索引 e.next = newTable[i]; // 头插法 newTable[i] = e; e = next; } } } ``` - **扩容后节点位置**: `newIndex = e.hash & (newCapacity-1)` 节点位置要么不变,要么偏移 `oldCapacity` #### 三、关键操作源码解析 ##### 1. put() 方法 ```java public V put(K key, V value) { if (table == EMPTY_TABLE) inflateTable(threshold); // 初始化数组 int hash = hash(key); int i = indexFor(hash, table.length); // 遍历链表查找key是否存在 for (Entry<K,V> e = table[i]; e != null; e = e.next) { if (e.hash == hash && (key == e.key || key.equals(e.key))) { V oldValue = e.value; e.value = value; // 更新值 return oldValue; } } addEntry(hash, key, value, i); // 新增节点 return null; } ``` ##### 2. get() 方法 ```java public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key); // 遍历链表/红黑树 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) return e.value; } return null; } ``` #### 四、Android 特殊优化 1. **ArrayMap 替代方案** - 双数组结构(`int[] mHashes` 存储哈希值,`Object[] mArray` 存储键值对) - 内存占用比 HashMap 少 30%(避免创建 Entry 对象)[^1] 2. **SparseArray 家族** - `SparseArray`:键为 int 类型(避免 Integer 装箱) - `LongSparseArray`:键为 long 类型 - `SparseBooleanArray` 等特化版本 #### 五、线程安全问题 1. **非线程安全表现**: - 多线程 put 可能导致链表成环(JDK7 头插法问题) - 扩容时可能导致数据丢失 2. **线程安全替代方案**: ```java // 方案1:使用 Collections 包装 Map<String, String> safeMap = Collections.synchronizedMap(new HashMap<>()); // 方案2:使用 ConcurrentHashMap(分段锁技术) Map<String, String> concurrentMap = new ConcurrentHashMap<>(); // 方案3:Android 推荐 ArrayMap(非线程安全,需自行加锁) ArrayMap<String, String> arrayMap = new ArrayMap<>(); ``` #### 六、遍历方式 ```java // 遍历键 for (K key : map.keySet()) { // 处理key } // 遍历值 for (V value : map.values()) { // 处理value } // 遍历键值对(推荐) for (Map.Entry<K, V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue(); } ``` > **最佳实践**: > - 小数据量(<1000项):优先使用 `ArrayMap` 或 `SparseArray` > - 大数据量:使用 `HashMap` 但避免频繁扩容(初始化时指定容量) > - 多线程场景:使用 `ConcurrentHashMap` 或同步包装类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值