数组:一片物理上连续的大小确定的储存空间。
好处:根据下标快速的查找和修改里面的内容。
缺点:大小确定,无法修改。添加新的元素或者删除元素比较麻烦。
数组的静态初始化
//数组实现方式一: //数据类型 数组名称[] = {值, 值,…} String str[] = {"移动端","Android","iOS"}; System.out.println(str.length);//三个元素 for (int i = 0; i运行结果是一样的。
"E:AndroidAndroid StudioOjrebinjava.exe"... 3 移动端 Android iOS如果我想再添加两个新元素(Java、Kotlin),那么使用数组就不合适了,这时候顺序表就出现了。
1.2 顺序表顺序表:以数组的形式保存的线性表,物理上连续、逻辑上连续、大小可以动态增加。(如:ArrayList)
顺序表用的频率远远高于数据。用肯定都会用,咱们看看源码。
public class ArrayListextends AbstractList implements List , RandomAccess, Cloneable, java.io.Serializable { transient Object[] elementData;//存储 ArrayList 元素的数组缓冲区。 private int size;//ArrayList 的大小 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public E get(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); return (E) elementData[index]; } public E set(int index, E element) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; } public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } } 然后你会发现还是用数组进行存储的,只不过把对数组的 *** 作处理了,而不需要我们自己处理。
原来的数据:
新增数据:
而顺序表的新增和删除元素都需要大量移动元素等 *** 作,此时链表就产生了。
1.3 链表链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。
新增
这里将元素1的指针由指向元素2改为指向新增元素4,然后再将元素4的指针指向元素2即可。
删除元素
将元素1的指针直接指向元素3即可。
1.4 ArrayList 和 linkedList 对比ArrayList(顺序表):
优点:查找快
缺点:增删慢
linkedList(链表)
优点:增删快
缺点:查找慢
那么问题来了在什么情况下使用 ArrayList?在什么情况下使用 linkedList? 为什么?
那么可不可以将 顺序表和链表的优点结合起来呢?——Hash表
1.5 Hash表散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希表其实本质上就是一个数组。可以根据一个key值来直接访问数据,查找速度快。
二、HashMapHashMap采用Node(implements Map.Entry)数组来存储key-value对,每一个键值对组成了一个Node实体,Node类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Node实体。
注意:jdk1.8 HashMap中的Entry不见了,取而代之的是Node,但是Node也实现了Map.Entry接口,和之前的Entry类似。
Node类
static class Nodeimplements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { ... } } 从上面看出 hash、key、value 代表本节点数据,next表示指向下一个节点的数据。
JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8) 时,将链表转换为红黑树,这样大大减少了查找时间。
2.0 小试牛刀class HashMapTest { public static void main(String[] args) { //创建HashMap HashMaphashMap = new HashMap<>(); //添加数据(key-value) hashMap.put("name","帅次"); hashMap.put("age","20"); hashMap.put("subject","Android"); hashMap.put(null,"空Key"); System.out.println("HashMap1:"+hashMap.toString());//{null=空Key, subject=Android, name=帅次, age=20} //添加数据(覆盖已有key) hashMap.put("age","26"); System.out.println("HashMap2:"+hashMap.toString());//{null=空Key, subject=Android, name=帅次, age=26} //根据key,获取value System.out.println("Key-null:"+hashMap.get(null));//空Key System.out.println("Key-subject:"+hashMap.get("subject"));//Android //根据key删除元素 System.out.println("删除subject对应:"+hashMap.remove("subject"));//删除subject对应:Android //根据key-value删除元素 System.out.println("删除age:"+hashMap.remove("age","20"));//删除age对应20:false System.out.println(hashMap.toString());//{null=空Key, name=帅次, age=26} System.out.println("删除age:"+hashMap.remove("age","26"));//删除age对应20:true System.out.println("HashMap3:"+hashMap.toString());//{null=空Key, name=帅次} //方法一:1、获得key-value的Set集合,2、循环(推荐) Set > entrySet= hashMap.entrySet(); for (Map.Entry strEntry : entrySet) { System.out.println("遍历Set集合:"+strEntry.getKey()+"-"+strEntry.getValue()); } //方法二:1、获取key的Set集合,2、循环 Set strKeySet= hashMap.keySet(); for (String s : strKeySet) { //先获取key,通过key获取value System.out.println("遍历KeySet集合:"+s+"-"+hashMap.get(s)); } //方法三:1、获得key-value的Set集合,2、获取iterator 3、循环 Set > itEntrySet= hashMap.entrySet(); Iterator iterator = itEntrySet.iterator(); while (iterator.hasNext()){ Map.Entry map= (Map.Entry)iterator.next(); System.out.println("遍历Iterator:"+map.getKey()+"-"+map.getValue()); } //其他还有不少方法可以遍历数据 } } 运行结果
"E:Android...." HashMap1:{null=空Key, subject=Android, name=帅次, age=20} HashMap2:{null=空Key, subject=Android, name=帅次, age=26} Key-null:空Key Key-subject:Android 删除subject对应:Android 删除age:false {null=空Key, name=帅次, age=26} 删除age:true HashMap3:{null=空Key, name=帅次} 遍历Set集合:null-空Key 遍历Set集合:name-帅次 遍历KeySet集合:null-空Key 遍历KeySet集合:name-帅次 遍历Iterator:null-空Key 遍历Iterator:name-帅次 Process finished with exit code 02.1 构造方法static final float DEFAULT_LOAD_FACTOR = 0.75f; final float loadFactor; 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); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }HashMap有4个构造方法,主要初始化了三个参数:
initialCapacity:初始容量(默认16)。
loadFactor 加载因子(默认0.75)。
threshold 阈值:hashMap所能容纳的最大价值对数量,如果超过则需要扩容,计算方式:threshold=initialCapacity*loadFactor。
加载因子:在默认情况下,数组大小为16,那么当HashMap中元素个数超过 16*0.75=12 的时候,就把数组的大小扩展为 16*2=32 ,即扩大一倍,然后重新计算每个元素在数组中的位置。当然这个值是可以自己设定的,但是不推荐修改这个值。
咱们比较常用的还是无参构造方法。HashMap 创建好了,那么咱们开始存储数据吧。
2.2 put()添加数据:
key可以为null
key唯一,如果key存在则覆盖value内容
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }这里在调用 putVal() 方法前根据key进行了hash计算。
2.2.1 hash(key)static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } public int hashCode() { return identityHashCode(this); } static int identityHashCode(Object obj) { int lockWord = obj.shadow$_monitor_; final int lockWordStateMask = 0xC0000000; // Top 2 bits. final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash). final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits. if ((lockWord & lockWordStateMask) == lockWordStateHash) { return lockWord & lockWordHashMask; } return identityHashCodeNative(obj); }
当 key == null 时返回0,也就是说 key 可以设置为 null。
从identityHashCode()方法看出key可以是任意类型,都可以变成int类型的hashCode。
h >>> 16 是什么?
h是hashCode。h >>> 16是用来取出h的高16,(>>>是无符号右移)
0000 0100 1011 0011 1101 1111 1110 0001 >>> 4 0000 0000 0100 1011 0011 1101 1111 1110 >>> 16 0000 0000 0000 0000 0000 0100 1011 0011上面两个例子 一个>>>4 一个>>>16,可用于对比。
这里根据Key返回了一个Hash值。拿到哈希值咱们继续看 putVal() 方法。
2.2.2 putVal()transient Node2.2.3 resize() 扩容[] table; final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node [] tab; Node p; int n, i; //判断table是否初始化 if ((tab = table) == null || (n = tab.length) == 0) //调用 resize() 方法,进行初始化并赋值 n = (tab = resize()).length; //通过hash获取下标,如果数据为null if ((p = tab[i = (n - 1) & hash]) == null) //tab[i]下标没有值,创建新的Node并赋值。 tab[i] = newNode(hash, key, value, null); else { //tab[i] 下标的有数据,发生碰撞 Node e; K k; //判断tab[i]的hash值传入的hash值相当,tab[i]的的key值和传入的key值相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //如果是原有数据直接替换即可 e = p; else if (p instanceof TreeNode)//判断数据结构为红黑树 e = ((TreeNode )p).putTreeval(this, tab, hash, key, value); else {//数据结构是链表 for (int binCount = 0; ; ++binCount) { //p的下一个节点为null,表示p就是最后一个节点 if ((e = p.next) == null) { //创建Node并插入链表的尾部 p.next = newNode(hash, key, value, null); //当元素>=8-1,链表转为树(红黑树)结构 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果key在链表中已经存在,则退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //更新p指向下一个节点,继续遍历 p = e; } } //如果key在链表中已经存在,则修改其原先key的value值,并且返回老的value值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//替换旧值时会调用的方法(默认实现为空) return oldValue; } } ++modCount;//修改次数 //根据map值判断是否要对map的大小扩容 if (++size > threshold) resize(); afterNodeInsertion(evict);//插入成功时会调用的方法(默认实现为空) return null; } static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 final Node[] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //table已经初始化,且容量 > 0 if (oldCap > 0) { //MAXIMUM_CAPACITY=1<<30=2的30次幂=1073741824 if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的容量已近达到最大值,则不再扩容,阈值直接设置为最大值 //Integer.MAX_VALUE=(1 << 31)-1=2的31次幂-1=2147483648-1 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 扩大两倍 } else if (oldThr > 0) // 初始化容器=threshold newCap = oldThr; else { //threshold 和 table 皆未初始化情况,此处即为首次进行初始化 //第一次进入什么都没有所以要初始化容器(16) newCap = DEFAULT_INITIAL_CAPACITY; //12 = 0.75*16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //newThr 为 0 时,计算阈值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //更新阈值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node [] newTab = (Node [])new Node[newCap]; //更新table数据 table = newTab; //如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; //将指定下标数据不为null if ((e = oldTab[j]) != null) { //将指定下标数据置空 oldTab[j] = null; //指定下标数据只有一个 if (e.next == null) //直接将数据存放到新计算的hash值下标 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode)//红黑树 //将树箱中的节点拆分为下树箱和上树箱,如果现在太小(<=6),则数据结构取消红黑树改为链表。 ((TreeNode )e).split(this, newTab, j, oldCap); else { //链表 //重新计算hash值,根据新的下标重新分组 Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 因此,resize()方法 初始化了容器,并给 table 赋值,返回Node
2.2.4 putTreeval()[]。 final TreeNode2.3 get()putTreeval(HashMap map, Node [] tab, int h, K k, V v) { Class> kc = null; boolean searched = false; //获取根节点 TreeNode root = (parent != null) ? root() : this; //循环所有的节点,如果key冲突,返回原key对应的对象,如果不存在key冲突,则直接存放对象,并返回null对象 for (TreeNode p = root;;) { int dir, ph; K pk; //dir:决定节点的位置 if ((ph = p.hash) > h)//当前节点的哈希值大于存放对象的哈希值 dir = -1; else if (ph < h)//当前节点的哈希值小于存放对象的哈希值 dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点等于存放对象的哈希值,且key相同,则返回当前节点 return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { //如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点 if (!searched) { TreeNode q, ch; searched = true; if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } dir = tieBreakOrder(k, pk); } TreeNode xp = p; //如果p的左节点或右节点为空,证明已经找到存放位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { Node xpn = xp.next; //创建新节点 TreeNode x = map.newTreeNode(h, k, v, xpn); //根据dir设置存放位置 if (dir <= 0) xp.left = x; else xp.right = x; xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode )xpn).prev = x; //balanceInsertion树化结构,设置红黑节点,是否需要旋转 //moveRootToFront重置根节点 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } 获取数据
这方法返回结果:
节点(Node);
null
该 key 对应的数据就是 null;
HashMap 中不存在该 key
public V get(Object key) { Nodee; //hash(key):根据key的hashCode计算hash值,这个跟put中的的一样。 return (e = getNode(hash(key), key)) == null ? null : e.value; } 这个方法就是通过 getNode() 方法来获取节点,如果节点为null则返回null,如果节点存在则返回key对应的value。
2.3.1 getNode()final Node2.3.2 getTreeNode()getNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; //根据hash值获取table中节点存放位置,并获取第一个元素。 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //如果第一个节点是我们要找的Key,则直接返回 if (first.hash == hash && // ((k = first.key) == key || (key != null && key.equals(k)))) return first; //获取下一个节点的信息 if ((e = first.next) != null) { //判断数据结构是红黑树,则通过getTreeNode()获取节点并返回 if (first instanceof TreeNode) return ((TreeNode )first).getTreeNode(hash, key); //否则循环整个链表找到节点并返回 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } static final class TreeNode2.3.3 find()extends linkedHashMap.linkedHashMapEntry { TreeNode parent; // 红黑树节点 //调用树的find()函数 final TreeNode getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null); } final TreeNode2.4 remove()find(int h, Object k, Class> kc) { TreeNode p = this; do { int ph, dir; K pk; TreeNode pl = p.left, pr = p.right, q; if ((ph = p.hash) > h)//当前节点的哈希值大于给定哈希值,进入左节点 p = pl; else if (ph < h)//当前节点小于给定哈希值,进入右节点 p = pr; else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点等于给定哈希值,且key相同,则返回当前节点 return p; else if (pl == null)//如果左节点为null,则进入右节点 p = pr; else if (pr == null)//如果右节点为null,则进入左节点 p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)//如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点 p = (dir < 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null)//如果在右结点中找到该关键字,直接返回当前节点 return q; else//进入左节点 p = pl; } while (p != null); return null; } public V remove(Object key) { Nodee; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final NoderemoveNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node [] tab; Node p; int n, index; //判断table是否存在、数据元素大于>0、index位置元素是否存在 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; //判断p和传入的数据一致 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //获取p的数据 node = p; else if ((e = p.next) != null) {//和p数据不一致,获取p下一个节点数据 //判断p的数据结构是否为红黑树 if (p instanceof TreeNode) //红黑树,调用find找到node节点 node = ((TreeNode )p).getTreeNode(hash, key); else { //链表 do { //循环找到跟e相同的数据 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //判断获取到的数据 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //根据红黑树删除数据 if (node instanceof TreeNode) ((TreeNode )node).removeTreeNode(this, tab, movable); else if (node == p)//node=p,然后指针指向p节点后一个节点,达到删除node的目的 tab[index] = node.next; else//此时node=p.next,所以node就被干掉了 p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } 这个方法其实跟上面的 put/get 方法类似,都是先获取哈希值,然后根据哈希值和Key找到这个节点,进行删除,自己探索乐趣多多。
2.5 clear()public void clear() { Node[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } } 这个简单粗暴,直接遍历table,然后将table的值设置为null。
其实HashMap常用的方法也就这么几个,你了解了吗?
三、问答小知识 3.1 为什么Key总喜欢用String、Integer从HashMap的语法上来讲,一切对象都可以作为Key值。如:Integer、Long、String、Object等。但是在实际工作中,最常用的使用String作为Key值。
它们都是final修饰的类,不可变性,保证key的不可更改性,不会存在获取hash值不同的情况。
它们内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范。
它们具有自己独立的特性,它们都放在常量区(快速判断是否相等)。
3.2 HashMap在什么情况下扩容?在上面的put方法中有这么一段内容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... if (++size > threshold) resize(); ... }当map的元素大于threshold = capacity(当前Map的大小) * load factor(默认0.75).
调用 resize() 进行扩容
static final int MAXIMUM_CAPACITY = 1 << 30; final Node[] resize() { ... if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } ... 3.3 为什么扩容是2的n次幂?>>:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。如将oldCap向左移动1位。
限制:最大不能超过2的30次幂
HashMap为了存取高效,要尽量较少碰撞。就是要尽量把数据分配均匀,每个链表长度大致相同。
例如:hashMap.get("name").hashCode()=773564。
773564 转为二进制:10111100110110111100
取模运算
取模运算:明显当hashmap大小不为2的n次幂的时候,哈希值的碰撞就多了起来。
tab[(n - 1) & hash])hash算法中,为了使元素分布的更加均匀,很多都会使用取模运算,在hashMap中并没有使用hash%n这样进行取模运算,而是使用(n - 1) & hash进行代替。原因是在计算机中,&的效率要远高于%;需要注意的是,只有容量为2的n次幂的时候,(n - 1) & hash 才能等效hash%n,这也是hashMap 初始化初始容量时必须是2的n次幂的原因。
3.4 为什么要先高16位异或低16位再取模运算?由于hashcode 和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。
3.5 HashMap 中的 key 为 定义的实体类类型, 则需实现哪些方法?//未重写equals()、hashCode()方法 System.out.println("未重写equals()、hashCode()等方法"); HashMapuserMap = new HashMap<>(); User userQin = new User("嬴政",20); userMap.put(userQin,"秦始皇"); //当你定义的MapUser和put时的数据未发生改变,通过Map.get可以获取到数据 System.out.println(userMap.get(userQin));//秦始皇 System.out.println("Age20:"+userQin.hashCode());//Age20:1531333864 //当MapUser.age发生改变后,通过Map.get无法获取数据 userQin.age=25; System.out.println("Age25:"+userQin.hashCode());//Age25:1531333864 System.out.println(userMap.get(userQin));//秦始皇 userMap.put(new User("刘邦",20),"汉高祖"); System.out.println(userMap.get(new User("刘邦",20)));//null //重写了equals()、hashCode()方法 System.out.println("重写了equals()、hashCode()方法"); HashMap hmUserMap = new HashMap<>(); HMUser hmUserLi = new HMUser("李世民",20); hmUserMap.put(hmUserLi,"唐太宗"); //当你定义的MapUser和put时的数据未发生改变,通过Map.get可以获取到数据 System.out.println(hmUserMap.get(hmUserLi));//唐太宗 System.out.println("Age20:"+hmUserLi.hashCode());//Age20:807921772 //当MapUser.age发生改变后,通过Map.get无法获取数据 hmUserLi.age=25; System.out.println("Age25:"+hmUserLi.hashCode());//Age25:807921777 System.out.println(hmUserMap.get(hmUserLi));//null hmUserMap.put(new HMUser("朱元璋",20),"明太祖"); System.out.println(hmUserMap.get(new HMUser("朱元璋",20)));//明太祖
未重写equals()、hashCode()方法时:
同一个对象属性发生改变后hashCode值还相同,可以从Map.get中获取到值。
不同对象属性相同,无法从Map.get中获取到值。
重写equals()、hashCode()方法时:
同一个对象属性发生改变后hashCode也发生变化相同,无法从Map.get中获取到值。
不同对象属性相同,可以从Map.get中获取到值。
3.6 为什么 HashMap 是非线程安全
HashMap 写入时:假如 A 线程和 B 线程同时进行插入 *** 作,计算出了相同的哈希值对应了相同的数组位置,A 先写入一个节点,B 也写入同一节点,那B的写入 *** 作就会覆盖 A 的写入 *** 作>造成 A 的写入数据丢失。
HashMap 扩容时:会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。当多个线程同时进来,检测到总数量超过门限值的时候就会同时调用 resize *** 作,各自生成新的数组,最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。
反正挺多可能会出现线程安全问题,总之 HashMap 是非线程安全的,有并发问题时,建议使用 ConcrrentHashMap。
ConcrrentHashMap:
底层采用分段的数组+链表实现,线程安全。
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍(读 *** 作不加锁,由于Node的value变量是 volatile 的,也能保证读取到最新的值)。
ConcurrentHashMap允许多个修改 *** 作并发进行,其关键在于使用了锁分离技术。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段, *** 作完毕后,又按顺序释放所有段的锁。
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用 *** 作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)