• [技术干货] Ehcache 介绍(1)--Ehcache 功能特性 【转】
    Ehcache 是一个开源的、基于标准的缓存工具,它能提升性能、减轻数据库负载并简化可扩展性。由于其稳健性、经得起考验的特点以及与其他流行框架的集成,Ehcache 成为最广泛使用的基于 Java 的缓存工具。Ehcache 从进程内缓存一直扩展到混合的进程内/进程外部署,可以处理 TB 的数据。1、Ehcache 特性1.1、支持多级缓存Guava Cache 或 Caffeine,都是纯内存缓存,使用上会受到内存大小的制约,而 Ehcache 则打破了这一约束。Ehcache2.x 支持基于内存和磁盘的二级缓存能力,Ehcache3.x 进一步扩展了此部分能力,增加了对于堆外缓存的支持。此外,结合 Ehcache 原生支持的集群能力,又可以打破单机的限制,完全解决容量这一制约因素。Ehcache 支持如下形式的缓存:1.1.1、堆内缓存(heap)所谓的堆内(heap)缓存,就是我们常规意义上说的内存缓存,严格意义上来说,是指被 JVM 托管占用的部分内存。内存缓存最大的优势就是具有超快的读写速度,但是不足点就在于容量有限、且无法持久化。1.1.2、堆外缓存(off-heap)堆外(off-heap)缓存,同样是存储在内存中。其实就是在内存中开辟一块区域,将其当做磁盘进行使用。由于内存的读写速度特别快,所以将数据存储在这个区域,读写上可以获得比本地磁盘读取更优的表现。这里的“堆外”,主要是相对与 JVM 的堆内存而言的,因为这个区域不在 JVM 的堆内存中,所以叫堆外缓存。这块的关系如下图:既然都是内存中存储,那为何多此一举非要将其划分为堆外缓存呢?直接将这部分的空间类驾到堆内缓存上,不是一样的效果吗?原因如下:JVM 会基于 GC 机制自动的对内存中不再使用的对象进行垃圾回收,而 GC 的时候对系统性能的影响是非常大的。堆内缓存的数据越多,GC 的压力就会越大,对系统性能的影响也会越明显。所以为了降低大量缓存对象 GC 回收动作的影响,便出现了 off-heap 处理方式。在 JVM 堆外的内存中开辟一块空间,可以像使用本地磁盘一样去使用这块内存区域,这样就既享受了内存的高速读写能力,又避免频繁 GC 带来的烦恼。堆内缓存与堆外缓存的区别与各自优缺点:堆内缓存是由 JVM 管理的,在 JVM 中可以直接以引用的形式去读取,所以读写的速度会特别高。而且 JVM 会负责其内容的回收与清理,使用起来比较“省心”。堆外缓存是在内存中划定了一块独立的存储区域,然后可以将这部分内存当做“磁盘”进行使用。需要使用方自行维护数据的清理,读写前需要序列化与反序列化操作,但可以省去 GC 的影响。1.1.3、磁盘缓存(disk)当我们需要缓存的数据量特别大、内存容量无法满足需求的时候,可以使用 disk 磁盘存储来作为补充。相比于内存,磁盘的读写速度显然要慢一些、但是胜在其价格便宜,容量可以足够大。1.1.4、集群缓存(Cluster)作为单机缓存,数据都是存在各个进程内的,在分布式组网系统中,如果缓存数据发生变更,就会出现各个进程节点中缓存数据不一致的问题。为了解决这一问题,Ehcache 支持通过集群的方式,将多个分布式节点组网成一个整体,保证相互节点之间的数据同步。需要注意的是,除了堆内缓存属于 JVM 堆内部,可以直接通过引用的方式进行访问,其余几种类型都属于 JVM 外部的数据交互,所以对这部分数据的读写时,需要先进行序列化与反序列化,因此要求缓存的数据对象一定要支持序列化与反序列化。不同的缓存类型具有不同的运算处理速度,堆内缓存的速度最快,堆外缓存次之,集群缓存的速度最慢。为了兼具处理性能与缓存容量,可以采用多种缓存形式组合使用的方式,构建多级缓存来实现。组合上述几种不同缓存类型然后构建多级缓存的时候,也需要遵循几个约束:A、多级缓存中必须有堆内缓存,必须按照 堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存 的顺序进行组合;B、多级缓存中的容量设定必须遵循 堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存 的原则;C、多级缓存中不允许磁盘缓存与集群缓存同时出现;1.2、支持缓存持久化常规的基于内存的缓存都有一个通病就是无法持久化,每次重启的时候,缓存数据都会丢失,需要重新去构建。而 Ehcache 支持使用磁盘来对缓存内容进行持久化保存。1.3、支持分布式缓存对于分布式系统,或者是集群场景下,并非是本地缓存的主战场。为了保证集群内数据的一致性,很多场景往往直接选择 Redis 等集中式缓存。但是集中式缓存也弊端,比如有些数据并不怎么更新、但是每个节点对其依赖度却非常高,如果频繁地去 Redis 请求交互,又会导致大量的性能损耗在网络 IO 交互处理上。针对这种情况,Ehcache 给出了一个相对完美的答案:本地 + 集群化策略。即在本地缓存的基础上,将集群内各本地节点组成一个相互连接的网,然后基于某种机制,将一个节点上发生的变更同步给其余节点进行同步更新,这样就可以实现各个节点的缓存数据一致。Ehcache 提供了多种不同的解决方案,可以将其由本地缓存变身为“分布式缓存”:RMI 组播方式JMS 消息方式Cache Server 模式JGroup 方式Terracotta 方式1.4、更灵活和细粒度的过期时间设定Guava Cache 与 Caffeine,它们支持设定过期时间,但是仅允许设定缓存容器级别的过期时间,容器内的所有元素都遵循同一个过期时间。Ehcache 不仅支持缓存容器级别的过期时间设定,还会支持为容器中每一条缓存记录设定独立过期时间,允许不同记录有不同的过期时间。这在某些场景下还是非常友好的,可以指定部分热点数据一个相对较长的过期时间,避免热点数据因为过期导致的缓存击穿。1.5、同时支持 JCache 与 SpringCache 规范Ehcache 作为一个标准化的通用缓存框架,同时支持 Java 目前最为主流的两大缓存标准,即官方的 JSR107 标准以及使用非常广泛的 Spring Cache 标准,这样使得业务中可以基于标准化的缓存接口去调用,避免了 Ehcache 深度耦合到业务逻辑中去。1.6、Ehcache、Caffeine、Redis 如何选择1.6.1、CaffeineA、更加轻量级,使用更加简单,可以理解为一个增强版的 HashMapB、足够纯粹,适用于仅需要本地缓存数据的常规场景,可以获取到绝佳的命中率与并发访问性能1.6.2、RedisA、纯粹的集中缓存,为集群化、分布式多节点场景而生,可以保证缓存的一致性B、业务需要通过网络进行交互,相比与本地缓存而言性能上会有损耗1.6.3、EhcacheA、支持多级缓存扩展能力。通过内存+磁盘等多种存储机制,解决缓存容量问题,适合本地缓存中对容量有特别要求的场景B、支持缓存数据持久化操作。允许将内存中的缓存数据持久化到磁盘上,进程启动的时候从磁盘加载缓存数据到内存中C、支持多节点集群化组网。可以将分布式场景下的各个节点组成集群,实现缓存数据一致,解决缓存漂移问题相比而言,Caffeine 专注于提供纯粹且简单的本地基础缓存能力;Redis 则聚焦集中缓存可保证数据的一致性;Ehcache 的功能比较中庸,介于两者之间,既具有本地缓存无可比拟的性能优势,又兼具分布式缓存的多节点数据一致性与容量扩展能力。项目里面进行选型的时候,可以结合上面的差异点,评估下自己的实际诉求,决定如何选择。简单来说,把握如下原则即可:A、如果只是本地简单、少量缓存数据使用的,选择 CaffeineB、如果本地缓存数据量较大、内存不足需要使用磁盘缓存的,选择 EhCacheC、如果是大型分布式多节点系统,业务对缓存使用较为重度,且各个节点需要依赖并频繁操作同一个缓存,选择 Redis参考:https://juejin.cn/post/7167259989826863112转载自https://www.cnblogs.com/wuyongyin/p/17992954
  • 什么是MurmurHash
    MurmurHash是一种非常快速的哈希算法,由Austin Appleby于2008年创建。它被广泛用于哈希表、哈希集合和其他数据结构中。 MurmurHash具有以下特点: 快速:MurmurHash是一种高度优化的哈希算法,对于大多数输入键值,它能够以很高的速度计算出哈希值。 均匀分布:MurmurHash采用了一系列位运算、位移和乘法等操作,以及混合了输入数据的字节来生成哈希值,从而产生较好的哈希分布特性。 不可逆性:MurmurHash是一种单向哈希函数,即无法从哈希值逆向计算出原始输入数据。 MurmurHash算法有多个变种,其中较常见的是MurmurHash3。MurmurHash3提供了32位和128位两种输出版本,可以根据需要选择使用。 MurmurHash在许多应用中被广泛使用,例如: 数据库索引 哈希表和哈希集合的实现 数据校验和计算 随机数生成器的种子选择 分布式系统中的一致性哈希算法 在Java中使用MurmurHash算法,您可以使用第三方库或手动实现该算法。 1.使用第三方库: 一些常见的第三方库可供选择,如Guava、Apache Commons Codec等。这些库提供了现成的MurmurHash实现,您只需引入相应的库依赖即可使用。以下是使用Apache Commons Codec库的示例: import org.apache.commons.codec.digest.MurmurHash3;public class MurmurHashExample { public static void main(String[] args) { String input = "Hello, MurmurHash!"; byte[] bytes = input.getBytes(); int seed = 0; // 哈希种子 int hash = MurmurHash3.hash32x86(bytes, 0, bytes.length, seed); System.out.println("Hash value: " + hash); }}2.手动实现: 如果您不想依赖第三方库,也可以手动实现MurmurHash算法。以下是一个简化版的MurmurHash3_32实现示例:public class MurmurHash { private static final int C1 = 0xcc9e2d51; private static final int C2 = 0x1b873593; private static final int R1 = 15; private static final int R2 = 13; private static final int M = 5; private static final int N = 0xe6546b64; public static int murmurHash3(byte[] data, int length, int seed) { int hash = seed; int remainingBytes = length; int currentIndex = 0; while (remainingBytes >= 4) { int k = getLittleEndianInt(data, currentIndex); k *= C1; k = Integer.rotateLeft(k, R1); k *= C2; hash ^= k; hash = Integer.rotateLeft(hash, R2); hash = hash * M + N; currentIndex += 4; remainingBytes -= 4; } if (remainingBytes > 0) { int k = 0; for (int i = remainingBytes - 1; i >= 0; i--) { k <<= 8; k |= (data[currentIndex + i] & 0xff); } k *= C1; k = Integer.rotateLeft(k, R1); k *= C2; hash ^= k; } hash ^= length; hash = fmix(hash); return hash; } private static int getLittleEndianInt(byte[] data, int index) { return ((data[index++] & 0xFF)) | ((data[index++] & 0xFF) << 8) | ((data[index++] & 0xFF) << 16) | ((data[index] & 0xFF) << 24); } private static int fmix(int h) { h ^= h >>> 16; h *= 0x85ebca6b; h ^= h >>> 13; h *= 0xc2b2ae35; h ^= h >>> 16; return h; } public static void main(String[] args) { String input = "Hello, MurmurHash!"; byte[] bytes = input.getBytes(); int seed = 0; // 哈希种子 int hash = murmurHash3(bytes, bytes.length, seed); System.out.println("Hash value: " + hash); }}请注意,这只是一个简化版的实现,具体实际应用中可能需要根据需求做进一步优化或调整
  • [技术干货] MapUtils工具类【转】
    MapUtils是 Apache Commons 工具包中常用的工具类,使用是需要依赖对应的lab,对应的maven引用如下:<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.1</version></dependency>MapUtils 常用操作java.util.Map 和 java.util.SortedMap。常用方法有:isNotEmpty ( ) 是否不为空isEmpty ( ) 是否为空putAll ( ) 添加所有元素getString ( ) 获取String类型的值getObject ( ) 获取Object类型的值getInteger ( )获取Integer类型的值get*** ( ) 类似上面的EMPTY_MAP 获取一个不可修改的空类型MapunmodifiableMap 获取一个不可以修改的Map(不能新增或删除)unmodifiableSortedMap 获取一个不可以修改的有序的Map(不能新增或删除)fixedSizeMap 获取一个固定长度的mapmultiValueMap 获取一个多值的map(即一个key可以对应多个value值)invertMap 返回一个key与value对调的mappredicatedMap() 返回一个满足predicate条件的maplazyMap 返回一个lazy的map(值在需要的时候可以创建)String name = MapUtils.getString(map, "name"); int money = MapUtils.getInteger(map,"money");//包装类型自动拆箱 boolean empty = MapUtils.isEmpty(map); boolean notEmpty = MapUtils.isNotEmpty(map); //把二维数组转换成mapString[][] user = {{"names","zhangfsan"},{"sexs","1f"}};Map map1 = MapUtils.putAll(map, user);public class MapUtilsTest { private String[][] color2DArray = new String[][] { {"RED", "#FF0000"}, {"GREEN", "#00FF00"}, {"BLUE", "#0000FF"} }; private String[] color1DArray = new String[] { "RED", "#FF0000", "GREEN", "#00FF00", "BLUE", "#0000FF" }; private Map<String, String> colorMap; //...}@Testpublic void whenCreateMapFrom2DArray_theMapIsCreated() { this.colorMap = MapUtils.putAll( new HashMap<>(), this.color2DArray); assertThat( this.colorMap, is(aMapWithSize(this.color2DArray.length))); assertThat(this.colorMap, hasEntry("RED", "#FF0000")); assertThat(this.colorMap, hasEntry("GREEN", "#00FF00")); assertThat(this.colorMap, hasEntry("BLUE", "#0000FF"));}@Testpublic void whenCreateMapFrom1DArray_theMapIsCreated() { this.colorMap = MapUtils.putAll( new HashMap<>(), this.color1DArray); assertThat( this.colorMap, is(aMapWithSize(this.color1DArray.length / 2))); assertThat(this.colorMap, hasEntry("RED", "#FF0000")); assertThat(this.colorMap, hasEntry("GREEN", "#00FF00")); assertThat(this.colorMap, hasEntry("BLUE", "#0000FF"));}@Testpublic void whenVerbosePrintMap_thenMustPrintFormattedMap() { MapUtils.verbosePrint(System.out, "Optional Label", this.colorMap);} /**Optional Label = { RED = #FF0000 BLUE = #0000FF GREEN = #00FF00}/* @Testpublic void whenGetKeyNotPresent_thenMustReturnDefaultValue() { String defaultColorStr = "COLOR_NOT_FOUND"; String color = MapUtils .getString(this.colorMap, "BLACK", defaultColorStr); assertEquals(color, defaultColorStr);}@Testpublic void whenGetOnNullMap_thenMustReturnDefaultValue() { String defaultColorStr = "COLOR_NOT_FOUND"; String color = MapUtils.getString(null, "RED", defaultColorStr); assertEquals(color, defaultColorStr);}@Testpublic void whenInvertMap_thenMustReturnInvertedMap() { Map<String, String> invColorMap = MapUtils.invertMap(this.colorMap); int size = invColorMap.size(); Assertions.assertThat(invColorMap) .hasSameSizeAs(colorMap) .containsKeys(this.colorMap.values().toArray(new String[] {})) .containsValues(this.colorMap.keySet().toArray(new String[] {}));} /**{ #00FF00 = GREEN #FF0000 = RED #0000FF = BLUE}*/@Test(expected = IllegalArgumentException.class)public void whenCreateFixedSizedMapAndAdd_thenMustThrowException() { Map<String, String> rgbMap = MapUtils .fixedSizeMap(MapUtils.putAll(new HashMap<>(), this.color1DArray)); rgbMap.put("ORANGE", "#FFA500");}@Test(expected = IllegalArgumentException.class)public void whenAddDuplicate_thenThrowException() { Map<String, String> uniqValuesMap = MapUtils.predicatedMap(this.colorMap, null, PredicateUtils.uniquePredicate()); uniqValuesMap.put("NEW_RED", "#FF0000");}@Testpublic void whenCreateLazyMap_theMapIsCreated() { Map<Integer, String> intStrMap = MapUtils.lazyMap( new HashMap<>(), TransformerUtils.stringValueTransformer()); assertThat(intStrMap, is(anEmptyMap())); intStrMap.get(1); intStrMap.get(2); intStrMap.get(3); assertThat(intStrMap, is(aMapWithSize(3)));}转载自https://blog.javaex.cn/article/detail/555190270666272768
  • [技术干货] HashMap
    HashMap的底层实现jdk1.7之前采用数组+链表形式,每个节点是一个entity节点。通过hash运算找出数据在数组的位置,采用头插法插入数组对应的链表中。头插法在扩容的时候会调用resize方法,然后又调用transfer方法把里面的entity进行了一次rehash,这个过程中可能会造成链表的死循环。 在jdk1.8之后采用了数组+链表+红黑树的结构,原来的entity节点变成了node节点,采用了尾插法。jdk1.7中可能产生死循环的原因在hashmap扩容的时候会将旧的hashmap的节点依次移到新的hashmap中。假设原来的节点顺序是A、B、C,因为遍历链表时是从头往下的遍历顺序,那么在扩容移动之后的顺序编程C、B、A。在并发情况下如果一个线程对hashmap进行扩容,两个线程刚开始都指向了A,但是扩容后A节点变成了尾,而另一个线程并不知道。这个时候再插入的时候编程了在A的上一个节点进行插入。因为这是A已经不是头节点了就造成了冲突。死链的解决方法升级为JDK1.8使用ConcurrentHashMap代替,因为ConcurrentHashMap是线程安全的。jdk1.8后的HashMap的新能优化Hash算法的优化方面:对每个Hash值的优化,在他的低16位中,让高16位进行了异或运算,让低16位融合了高16位的特征。避免了一些hash的冲突。寻址算法优化:使用与运算代替取模,提升性能。增加了红黑树结构,提高性能。总的来说就是HashMap在put、get的时候进行了hash算法的优化。为什么采用红黑树结构红黑树又良好的稳定性和完整的功能综合力强。相比平衡二叉树AVL,AVL是一种高度平衡的二叉树查询效率很高,但是为了实现这种平衡每次插入删除时就要进行树结构的调整代价比较大。红黑树虽然查询效率略小于AVL但是不用每次都调整树结构。
  • [技术干货] HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!
    前言Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。HashMap众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。Base 1.71.7 中的数据结构图:先来看看 1.7 中的实现。这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?初始化桶大小,因为底层是数组,所以这是数组默认的大小。桶最大值。默认的负载因子(0.75)table 真正存放数据的数组。Map 存放数量的大小。桶大小,可在初始化时显式指定。负载因子,可在初始化时显式指定。重点解释下负载因子:由于给定的 HashMap 的容量大小是固定的,比如默认初始化: 1    public HashMap() { 2        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 3    } 4 5    public HashMap(int initialCapacity, float loadFactor) { 6        if (initialCapacity < 0) 7            throw new IllegalArgumentException("Illegal initial capacity: " + 8                                               initialCapacity); 9        if (initialCapacity > MAXIMUM_CAPACITY)10            initialCapacity = MAXIMUM_CAPACITY;11        if (loadFactor <= 0 || Float.isNaN(loadFactor))12            throw new IllegalArgumentException("Illegal load factor: " +13                                               loadFactor);1415        this.loadFactor = loadFactor;16        threshold = initialCapacity;17        init();18    }给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。根据代码可以看到其实真正存放数据的是transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;这个数组,那么它又是如何定义的呢?Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:key 就是写入时的键。value 自然就是值。开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。hash 存放的是当前 key 的 hashcode。知晓了基本结构,那来看看其中重要的写入、获取函数:put 方法 1    public V put(K key, V value) { 2        if (table == EMPTY_TABLE) { 3            inflateTable(threshold); 4        } 5        if (key == null) 6            return putForNullKey(value); 7        int hash = hash(key); 8        int i = indexFor(hash, table.length); 9        for (Entry<K,V> e = table[i]; e != null; e = e.next) {10            Object k;11            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {12                V oldValue = e.value;13                e.value = value;14                e.recordAccess(this);15                return oldValue;16            }17        }1819        modCount++;20        addEntry(hash, key, value, i);21        return null;22    }判断当前数组是否需要初始化。如果 key 为空,则 put 一个空值进去。根据 key 计算出 hashcode。根据计算出的 hashcode 定位出所在桶。如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。 1    void addEntry(int hash, K key, V value, int bucketIndex) { 2        if ((size >= threshold) && (null != table[bucketIndex])) { 3            resize(2 * table.length); 4            hash = (null != key) ? hash(key) : 0; 5            bucketIndex = indexFor(hash, table.length); 6        } 7 8        createEntry(hash, key, value, bucketIndex); 9    }1011    void createEntry(int hash, K key, V value, int bucketIndex) {12        Entry<K,V> e = table[bucketIndex];13        table[bucketIndex] = new Entry<>(hash, key, value, e);14        size++;15    }当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。get 方法再来看看 get 函数: 1    public V get(Object key) { 2        if (key == null) 3            return getForNullKey(); 4        Entry<K,V> entry = getEntry(key); 5 6        return null == entry ? null : entry.getValue(); 7    } 8 9    final Entry<K,V> getEntry(Object key) {10        if (size == 0) {11            return null;12        }1314        int hash = (key == null) ? 0 : hash(key);15        for (Entry<K,V> e = table[indexFor(hash, table.length)];16             e != null;17             e = e.next) {18            Object k;19            if (e.hash == hash &&20                ((k = e.key) == key || (key != null && key.equals(k))))21                return e;22        }23        return null;24    }首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。判断该位置是否为链表。不是链表就根据 key、key 的 hashcode 是否相等来返回值。为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。啥都没取到就直接返回 null 。Base 1.8不知道 1.7 的实现大家看出需要优化的点没有?其实一个很明显的地方就是:当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。因此 1.8 中重点优化了这个查询效率。1.8 HashMap 结构图:先来看看几个核心的成员变量: 1    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 2 3    /** 4     * The maximum capacity, used if a higher value is implicitly specified 5     * by either of the constructors with arguments. 6     * MUST be a power of two <= 1<<30. 7     */ 8    static final int MAXIMUM_CAPACITY = 1 << 30; 910    /**11     * The load factor used when none specified in constructor.12     */13    static final float DEFAULT_LOAD_FACTOR = 0.75f;1415    static final int TREEIFY_THRESHOLD = 8;1617    transient Node<K,V>[] table;1819    /**20     * Holds cached entrySet(). Note that AbstractMap fields are used21     * for keySet() and values().22     */23    transient Set<Map.Entry<K,V>> entrySet;2425    /**26     * The number of key-value mappings contained in this map.27     */28    transient int size;和 1.7 大体上都差不多,还是有几个重要的区别:TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。HashEntry 修改为 Node。Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。再来看看核心方法。put 方法看似要比 1.7 的复杂,我们一步步拆解:判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。如果当前桶为红黑树,那就要按照红黑树的方式写入数据。如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。如果在遍历过程中找到 key 相同时直接退出遍历。如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。最后判断是否需要进行扩容。get 方法 1    public V get(Object key) { 2        Node<K,V> e; 3        return (e = getNode(hash(key), key)) == null ? null : e.value; 4    } 5 6    final Node<K,V> getNode(int hash, Object key) { 7        Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 8        if ((tab = table) != null && (n = tab.length) > 0 && 9            (first = tab[(n - 1) & hash]) != null) {10            if (first.hash == hash && // always check first node11                ((k = first.key) == key || (key != null && key.equals(k))))12                return first;13            if ((e = first.next) != null) {14                if (first instanceof TreeNode)15                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);16                do {17                    if (e.hash == hash &&18                        ((k = e.key) == key || (key != null && key.equals(k))))19                        return e;20                } while ((e = e.next) != null);21            }22        }23        return null;24    }get 方法看起来就要简单许多了。首先将 key hash 之后取得所定位的桶。如果桶为空则直接返回 null 。否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值。不然就按照链表的方式遍历匹配返回值。从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。1final HashMap<String, String> map = new HashMap<String, String>();2for (int i = 0; i < 1000; i++) {3    new Thread(new Runnable() {4        @Override5        public void run() {6            map.put(UUID.randomUUID().toString(), "");7        }8    }).start();9}但是为什么呢?简单分析下。看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。遍历方式还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: 1Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator(); 2        while (entryIterator.hasNext()) { 3            Map.Entry<String, Integer> next = entryIterator.next(); 4            System.out.println("key=" + next.getKey() + " value=" + next.getValue()); 5        } 6 7Iterator<String> iterator = map.keySet().iterator(); 8        while (iterator.hasNext()){ 9            String key = iterator.next();10            System.out.println("key=" + key + " value=" + map.get(key));1112        }强烈建议使用第一种 EntrySet 进行遍历。第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。坚持看到这里的朋友算是已经把 ConcurrentHashMap 的基础已经打牢了,下面正式开始分析。ConcurrentHashMapConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。Base 1.7如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。它的核心成员变量:1    /**2     * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。3     */4    final Segment<K,V>[] segments;56    transient Set<K> keySet;7    transient Set<Map.Entry<K,V>> entrySet;Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: 1    static final class Segment<K,V> extends ReentrantLock implements Serializable { 2 3        private static final long serialVersionUID = 2249069246763182397L; 4 5        // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 6        transient volatile HashEntry<K,V>[] table; 7 8        transient int count; 910        transient int modCount;1112        transient int threshold;1314        final float loadFactor;1516    }和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。下面也来看看核心的 put get 方法。put 方法 1    public V put(K key, V value) { 2        Segment<K,V> s; 3        if (value == null) 4            throw new NullPointerException(); 5        int hash = hash(key); 6        int j = (hash >>> segmentShift) & segmentMask; 7        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck 8             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment 9            s = ensureSegment(j);10        return s.put(key, hash, value, false);11    }首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。 1        final V put(K key, int hash, V value, boolean onlyIfAbsent) { 2            HashEntry<K,V> node = tryLock() ? null : 3                scanAndLockForPut(key, hash, value); 4            V oldValue; 5            try { 6                HashEntry<K,V>[] tab = table; 7                int index = (tab.length - 1) & hash; 8                HashEntry<K,V> first = entryAt(tab, index); 9                for (HashEntry<K,V> e = first;;) {10                    if (e != null) {11                        K k;12                        if ((k = e.key) == key ||13                            (e.hash == hash && key.equals(k))) {14                            oldValue = e.value;15                            if (!onlyIfAbsent) {16                                e.value = value;17                                ++modCount;18                            }19                            break;20                        }21                        e = e.next;22                    }23                    else {24                        if (node != null)25                            node.setNext(first);26                        else27                            node = new HashEntry<K,V>(hash, key, value, first);28                        int c = count + 1;29                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)30                            rehash(node);31                        else32                            setEntryAt(tab, index, node);33                        ++modCount;34                        count = c;35                        oldValue = null;36                        break;37                    }38                }39            } finally {40                unlock();41            }42            return oldValue;43        }虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。再结合图看看 put 的流程。将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。最后会解除在 1 中所获取当前 Segment 的锁。get 方法 1    public V get(Object key) { 2        Segment<K,V> s; // manually integrate access methods to reduce overhead 3        HashEntry<K,V>[] tab; 4        int h = hash(key); 5        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; 6        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && 7            (tab = s.table) != null) { 8            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile 9                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);10                 e != null; e = e.next) {11                K k;12                if ((k = e.key) == key || (e.hash == h && key.equals(k)))13                    return e.value;14            }15        }16        return null;17    }get 逻辑比较简单:只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。Base 1.81.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。那就是查询遍历链表效率太低。因此 1.8 做了一些数据结构上的调整。看起来是不是和 1.8 HashMap 结构类似?其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next 都用了 volatile 修饰,保证了可见性。put 方法重点来看看 put 函数:根据 key 计算出 hashcode 。判断是否需要进行初始化。f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。如果都不满足,则利用 synchronized 锁写入数据。如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。get 方法根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。就不满足那就按照链表的方式遍历获取值。1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。总结看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。其实这块也是面试的重点内容,通常的套路是:谈谈你理解的 HashMap,讲讲其中的 get put 过程。1.8 做了什么优化?是线程安全的嘛?不安全会导致哪些问题?如何解决?有没有线程安全的并发容器?ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?原文链接:https://blog.csdn.net/weixin_44460333/article/details/86770169
  • [新手课堂] HashMap原理,java8做了什么改变
    HashMap是以键值对存储数据的集合容器HashMap是非线性安全的。HashMap底层数据结构:数组+(链表、红黑树),jdk8之前是用数组+链表的方式实现,jdk8引进了红黑树HashMap数组的默认初始长度是16,key和value都允许null的存在HashMap的内部实现数组是Node[]数组,上面存放的是key-value键值对的节点。HashMap通过put和get方法存储和获取。HashMap的put方法,首先计算key的hashcode值,定位到对应的数组索引,然后再在该索引的单向链表上进行循环遍历,用equals比较key是否存在,如果存在则用新的value覆盖原值,如果没有则向后追加。jdk8中put方法:先判断HashMap是否为空,为空就扩容,不为空计算出key的hash值i,然后看table[i]是否为空,为空就直接插入,不为空判断当前位置的key和table[i]是否相同,相同就覆盖,不相同就查看table[i]是否是红黑树节点,如果是的话就用红黑树直接插入键值对,如果不是开始遍历链表插入,如果遇到重复值就覆盖,否则直接插入,如果链表长度大于8,转为红黑树结构,执行完成后看size是否大于阈值threshold,大于就扩容,否则直接结束。HashMap解决hash冲突,使用的是链地址法,即数组+链表的形式来解决。put执行首先判断table[i]位置,如果为空就直接插入,不为空判断和当前值是否相等,相等就覆盖,如果不相等的话,判断是否是红黑树节点,如果不是,就从table[i]位置开始遍历链表,相等覆盖,不相等插入。HashMap的get方法就是计算出要获取元素的hash值,去对应位置获取即可。HashMap的扩容机制,HashMap的扩容中主要进行两步,第一步把数组长度变为原来的两倍,第二步把旧数组的元素重新计算hash插入到新数组中,jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二步一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。HashMap大小为什么是2的幂次方?效率高+空间分布均匀
  • [技术干货] 如何决定使用 HashMap 还是TreeMap?
    对于在Map中插入、删除和定位元素这类操作,HashMap是 好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历
  • [问题求助] 【AICPU】请问如何实现CANN上面的hashmap,有可参照的数据结构样例吗
    【功能模块】【操作步骤&问题现象】1、2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [新手课堂] 为什么 ThreadLocalMap 选择去重新设计&quot;Map&quot;,而不直接使用 JDK中的 HashMap呢?
    因为 ThreadLocal 自己重新设计的 Map,它可以把自己的 Key 限定为特有类型(ThreadLocal),这个特定类型的Key 使用的是弱引用 WeakReference<ThreadLocal<?>>,而 HashMap 中的 Key 采用的是强引用方式。
  • [新手课堂] HashMap原理,java8做了什么改变
    HashMap是以键值对存储数据的集合容器HashMap是非线性安全的。HashMap底层数据结构:数组+(链表、红黑树),jdk8之前是用数组+链表的方式实现,jdk8引进了红黑树HashMap数组的默认初始长度是16,key和value都允许null的存在HashMap的内部实现数组是Node[]数组,上面存放的是key-value键值对的节点。HashMap通过put和get方法存储和获取。HashMap的put方法,首先计算key的hashcode值,定位到对应的数组索引,然后再在该索引的单向链表上进行循环遍历,用equals比较key是否存在,如果存在则用新的value覆盖原值,如果没有则向后追加。jdk8中put方法:先判断HashMap是否为空,为空就扩容,不为空计算出key的hash值i,然后看table[i]是否为空,为空就直接插入,不为空判断当前位置的key和table[i]是否相同,相同就覆盖,不相同就查看table[i]是否是红黑树节点,如果是的话就用红黑树直接插入键值对,如果不是开始遍历链表插入,如果遇到重复值就覆盖,否则直接插入,如果链表长度大于8,转为红黑树结构,执行完成后看size是否大于阈值threshold,大于就扩容,否则直接结束。HashMap解决hash冲突,使用的是链地址法,即数组+链表的形式来解决。put执行首先判断table[i]位置,如果为空就直接插入,不为空判断和当前值是否相等,相等就覆盖,如果不相等的话,判断是否是红黑树节点,如果不是,就从table[i]位置开始遍历链表,相等覆盖,不相等插入。HashMap的get方法就是计算出要获取元素的hash值,去对应位置获取即可。HashMap的扩容机制,HashMap的扩容中主要进行两步,第一步把数组长度变为原来的两倍,第二步把旧数组的元素重新计算hash插入到新数组中,jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二步一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。HashMap大小为什么是2的幂次方?效率高+空间分布均匀
  • [技术干货] java中JSONObject转换为HashMap(方法+main方法调用实例)
    1.首先要导入json相关的jar包引入的jar包:(版本自行定义,可以选用使用人数偏多的版本,这样比较稳定)commons-beanutils-1.9.2.jarcommons-collections-3.2.1.jarcommons-lang-2.6.jarcommons-logging-1.2.jarezmorph-1.0.6.jarjson-lib-2.4-jdk15.jarjar包的下载可以去下面这个网址搜索:https://mvnrepository.com/2.在Eclipse下(也可以是IntelliJ IDEA或者MyEclipse)新建package和Class(步骤略过,可自行选择名字),这里就使用jsonTest。以下代码块方法见注释,是将JSONObject转换为HashMap的主要方法,传入参数为一个JSONObject对象,返还值为一个HashMap。123456789101112//1.將JSONObject對象轉換為HashMap<String,String>public static HashMap<String, String> JsonObjectToHashMap(JSONObject jsonObj){    HashMap<String, String> data = new HashMap<String, String>();     Iterator it = jsonObj.keys();    while(it.hasNext()){        String key = String.valueOf(it.next().toString());        String value = (String)jsonObj.get(key).toString();        data.put(key, value);    }    System.out.println(data);    return data;}这个方法是将JSON字符串转换为HashMap,传入参数为一段json格式的字符串,返还一个HashMap。12345678910111213141516171819202122//2.将json字符串转换成HashMap<String,String>public static HashMap<String, String> JsonToHashMap(String JsonStrin){        HashMap<String, String> data = new HashMap<String, String>();     try{      // 将json字符串转换成jsonObject       JSONObject jsonObject = JSONObject.fromObject(JsonStrin);       @SuppressWarnings("rawtypes")        Iterator it = jsonObject.keys();      // 遍历jsonObject数据,添加到Map对象       while (it.hasNext())       {        String key = String.valueOf(it.next()).toString();         String value = (String) jsonObject.get(key).toString();          data.put(key, value);       }     }catch (Exception e) {        e.printStackTrace();        //JOptionPane.showMessageDialog(null,"ERROR:["+e+"]");    }    System.out.println(data);    return data;    }   在这里顺便介绍一下Iterator类(迭代器)迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。  Java中的Iterator功能比较简单,并且只能单向移动:  (1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。  (2) 使用next()获得序列中的下一个元素。  (3) 使用hasNext()检查序列中是否还有元素。  (4) 使用remove()将迭代器新返回的元素删除。  Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。3.直接上代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758package JSON; import java.util.HashMap;import java.util.Iterator;import net.sf.json.JSONObject; public class JsonTest {     public static void main(String[] args) {        JSONObject jsonObj = new JSONObject(true);        String content1 = "aaaaa";        String content2 = "bbbbb";        String content3 = "ccccc";        jsonObj.put("a", content1);        jsonObj.put("b", content2);        jsonObj.put("c", content3);        System.out.println(jsonObj.toString());        JsonObjectToHashMap(jsonObj);        String jsonstr = "{name:'王杨',sex:'男',school:'郑州航空工业管理学院'}";        JsonToHashMap(jsonstr);    }         //1.將JSONObject對象轉換為HashMap<String,String>    public static HashMap<String, String> JsonObjectToHashMap(JSONObject jsonObj){        HashMap<String, String> data = new HashMap<String, String>();         Iterator it = jsonObj.keys();        while(it.hasNext()){            String key = String.valueOf(it.next().toString());            String value = (String)jsonObj.get(key).toString();            data.put(key, value);        }        System.out.println(data);        return data;    }    //2.将json字符串转换成HashMap<String,String>    public static HashMap<String, String> JsonToHashMap(String JsonStrin){            HashMap<String, String> data = new HashMap<String, String>();         try{          // 将json字符串转换成jsonObject           JSONObject jsonObject = JSONObject.fromObject(JsonStrin);           @SuppressWarnings("rawtypes")            Iterator it = jsonObject.keys();          // 遍历jsonObject数据,添加到Map对象           while (it.hasNext())           {            String key = String.valueOf(it.next()).toString();             String value = (String) jsonObject.get(key).toString();              data.put(key, value);           }         }catch (Exception e) {            e.printStackTrace();            //JOptionPane.showMessageDialog(null,"ERROR:["+e+"]");        }        System.out.println(data);        return data;        }    }记得修改自己的package名称和 class名称。4.调用main方法测试(1)传入参数为JSONObject:输出结果为:(2)传入参数为JSON字符串:输出结果为:这里可以看到,输出的参数顺序和传入时正好相反。但是输出类型为HashMap,数据存储的格式是以key-value键值对的形式存数于HashMap中的。我们可以通过获取key值来获取到其对应的value。增加如下代码在main方法最后面:123456System.out.println("");//空格换行//通过对应的key键值,获取valueHashMap<String,String> hashmap = JsonToHashMap(jsonstr);System.out.println("--------通过遍历HashMap输出值:-------");System.out.println("name:"+hashmap.get("name")+",sex:"+hashmap.get("sex")+",school:"+hashmap.get("school"));得到如下结果:结语:到此基本的方法介绍完毕,其实是依靠了JSONObject这个对象的fromObject()方法。fromObject()方法可以转换的类型很多,可以是map、list、数组等等。运用在自己的项目中时,可以是bean或者model等自定义的类。1234567891011121314151617181920211. List集合转换成json代码List list = new ArrayList();list.add( "first" );list.add( "second" );JSONArray jsonArray2 = JSONArray.fromObject( list ); 2. Map集合转换成json代码Map map = new HashMap();map.put("name", "json");map.put("bool", Boolean.TRUE);map.put("int", new Integer(1));map.put("arr", new String[] { "a", "b" });map.put("func", "function(i){ return this.arr[i]; }");JSONObject json = JSONObject.fromObject(map); 3. Bean转换成json代码JSONObject jsonObject = JSONObject.fromObject(new JsonBean()); 4. 数组转换成json代码boolean[] boolArray = new boolean[] { true, false, true };JSONArray jsonArray1 = JSONArray.fromObject(boolArray);以上类型均可以借用fromObject()方法转换为一个JSONObject类型实例。json作为轻量级的数据格式,在前后端数据交互时很常见,每个公司应该都有自己的JSON转换方法,是公司常见的工具类。方便了随后的开发使用。
  • [Java] HashMap 的实现原理
    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。当我们往Hashmap中put元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上。需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)