集合Collection
集合
ArrayList
是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数 组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进 行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
RandomAccess接口:支持随机访问。
Cloneable接口:使用object.clone()方法,必须实现的接口,用来实拷贝功能。
Serializable接口:实现序列化
ArrayList 优缺点
ArrayListt和LinkerList对比
底层实现,访问,插入删除 ,空间利用率
LinkedList
LinkedList 有七种遍历:for循环,foreach循环,迭代器,⽤pollFirst()遍历,⽤pollLast()遍历,⽤removeFirst()遍历,⽤removeLast()遍历
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
1 public class LinkedList<E> 2 extends AbstractSequentialList<E> 3 implements List<E>, Deque<E>, Cloneable, java.io.Serializable 4 {}
所有方法及描述
返回值 | 方法及描述 |
---|---|
boolean |
add(E e) 将指定的元素追加到此列表的末尾。 |
void |
add(int index, E element) 在此列表中的指定位置插入指定的元素。 |
boolean |
addAll(Collection<? extends E> c) 按照指定集合的迭代器返回的顺序将指定集合中的所有元素追加到此列表的末尾。 |
boolean |
addAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 |
void |
addFirst(E e) 在该列表开头插入指定的元素。 |
void |
addLast(E e) 将指定的元素追加到此列表的末尾。 |
void |
clear() 从列表中删除所有元素。 |
Object |
clone() 返回此 LinkedList 的浅版本。 |
boolean |
contains(Object o) 如果此列表包含指定的元素,则返回 true 。 |
Iterator<E> |
descendingIterator() 以相反的顺序返回此deque中的元素的迭代器。 |
E |
element() 检索但不删除此列表的头(第一个元素)。 |
E |
get(int index) 返回此列表中指定位置的元素。 |
E |
getFirst() 返回此列表中的第一个元素。 |
E |
getLast() 返回此列表中的最后一个元素。 |
int |
indexOf(Object o) 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。 |
int |
lastIndexOf(Object o) 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。 |
ListIterator<E> |
listIterator(int index) 从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。 |
boolean |
offer(E e) 将指定的元素添加为此列表的尾部(最后一个元素)。 |
boolean |
offerFirst(E e) 在此列表的前面插入指定的元素。 |
boolean |
offerLast(E e) 在该列表的末尾插入指定的元素。 |
E |
peek() 检索但不删除此列表的头(第一个元素)。 |
E |
peekFirst() 检索但不删除此列表的第一个元素,如果此列表为空,则返回 null 。 |
E |
peekLast() 检索但不删除此列表的最后一个元素,如果此列表为空,则返回 null 。 |
E |
poll() 检索并删除此列表的头(第一个元素)。 |
E |
pollFirst() 检索并删除此列表的第一个元素,如果此列表为空,则返回 null 。 |
E |
pollLast() 检索并删除此列表的最后一个元素,如果此列表为空,则返回 null 。 |
E |
pop() 从此列表表示的堆栈中弹出一个元素。 |
void |
push(E e) 将元素推送到由此列表表示的堆栈上。 |
E |
remove() 检索并删除此列表的头(第一个元素)。 |
E |
remove(int index) 删除该列表中指定位置的元素。 |
boolean |
remove(Object o) 从列表中删除指定元素的第一个出现(如果存在)。 |
E |
removeFirst() 从此列表中删除并返回第一个元素。 |
boolean |
removeFirstOccurrence(Object o) 删除此列表中指定元素的第一个出现(从头到尾遍历列表时)。 |
E |
removeLast() 从此列表中删除并返回最后一个元素。 |
boolean |
removeLastOccurrence(Object o) 删除此列表中指定元素的最后一次出现(从头到尾遍历列表时)。 |
E |
set(int index, E element) 用指定的元素替换此列表中指定位置的元素。 |
int |
size() 返回此列表中的元素数。 |
Spliterator<E> |
spliterator() 在此列表中的元素上创建*late-binding和故障快速* Spliterator 。 |
Object[] |
toArray() 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 |
<T> T[] |
toArray(T[] a) 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 |
Iterator 和 ListIterator 有什么区别?
Iterator 可以遍历 Set 和 List 集合,⽽ ListIterator 只能遍历 List。
Iterator 只能单向遍历,⽽ ListIterator 可以双向遍历(向前/后遍历)。
Iterator 接⼝提供遍历任何 Collection 的接⼝。我们可以从⼀个 Collection 中使⽤迭代器⽅法来获取迭代器实例。迭代器取代了 Java 集
合框架中的 Enumeration,迭代器允许调⽤者在迭代过程中移除元素
CopyOnWriteArrayList
特点:写时复制机制、读写分离、写枷锁
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单等场景。
1、什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
适用场景:读多写少的场景。
2、CopyOnWriteArrayList的实现原理
在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。【以下源码基于jdk1.8】

1 /** 2 * Appends the specified element to the end of this list. 3 * 4 * @param e element to be appended to this list 5 * @return {@code true} (as specified by {@link Collection#add}) 6 */ 7 public boolean add(E e) { 8 final ReentrantLock lock = this.lock; 9 lock.lock(); 10 try { 11 Object[] elements = getArray(); 12 int len = elements.length; 13 Object[] newElements = Arrays.copyOf(elements, len + 1); 14 newElements[len] = e; 15 setArray(newElements); 16 return true; 17 } finally { 18 lock.unlock(); 19 } 20 } 21 22 /** 23 * Removes the element at the specified position in this list. 24 * Shifts any subsequent elements to the left (subtracts one from their 25 * indices). Returns the element that was removed from the list. 26 * 27 * @throws IndexOutOfBoundsException {@inheritDoc} 28 */ 29 public E remove(int index) { 30 final ReentrantLock lock = this.lock; 31 lock.lock(); 32 try { 33 Object[] elements = getArray(); 34 int len = elements.length; 35 E oldValue = get(elements, index); 36 int numMoved = len - index - 1; 37 if (numMoved == 0) 38 setArray(Arrays.copyOf(elements, len - 1)); 39 else { 40 Object[] newElements = new Object[len - 1]; 41 System.arraycopy(elements, 0, newElements, 0, index); 42 System.arraycopy(elements, index + 1, newElements, index, 43 numMoved); 44 setArray(newElements); 45 } 46 return oldValue; 47 } finally { 48 lock.unlock(); 49 } 50 } 51 52 /** 53 * Replaces the element at the specified position in this list with the 54 * specified element. 55 * 56 * @throws IndexOutOfBoundsException {@inheritDoc} 57 */ 58 public E set(int index, E element) { 59 final ReentrantLock lock = this.lock; 60 lock.lock(); 61 try { 62 Object[] elements = getArray(); 63 E oldValue = get(elements, index); 64 65 if (oldValue != element) { 66 int len = elements.length; 67 Object[] newElements = Arrays.copyOf(elements, len); 68 newElements[index] = element; 69 setArray(newElements); 70 } else { 71 // Not quite a no-op; ensures volatile write semantics 72 setArray(elements); 73 } 74 return oldValue; 75 } finally { 76 lock.unlock(); 77 } 78 }
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为开始读的那一刻已经确定了读的对象是旧对象。
3、CopyOnWrite的缺点
(1)内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
(2)数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧容器中的数据
CopyOnWriteArrayList读取时不加锁只是写入和删除时加锁,所以一个线程X读取的时候另一个线程Y可能执行remove操作。remove操作首先要获取独占锁,然后进行写时复制操作,就是复制一份当前的array数组,然后在复制的新数组里面删除线程X通过get访问的元素,比如:1。删除完成后让array指向这个新的数组。
在线程x执行get操作的时候并不是直接通过全局array访问数组元素而是通过方法的形参a访问的,a指向的地址和array指向的地址在调用get方法的那一刻是一样的,都指向了堆内存的数组对象。之后改变array指向的地址并不影响get的访问,因为在调用get方法的那一刻形参a指向的内存地址就已经确定了,不会改变。所以读的仍然是旧数组。然是旧数组的元素】
Set
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复(相等)。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断 的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
注意:两个List中元素值一样,在set中也是相等的。
1 Set set=new HashSet<ArrayList<Integer>>(); 2 3 ArrayList<Integer> list0=new ArrayList<>(); 4 ArrayList<Integer> list1=new ArrayList<>(); 5 for (int i = 0; i < 4; i++) { 6 list0.add(i); 7 } 8 for (int i = 0; i < 4; i++) { 9 list1.add(i); 10 } 11 set.add(list0); 12 set.add(list1); //set中只存放了list0,list1没存放 13
位置不同会当作两个不同list如下:
1 Set set=new HashSet<ArrayList<Integer>>(); 2 3 ArrayList<Integer> list0=new ArrayList<>(); 4 ArrayList<Integer> list1=new ArrayList<>(); 5 for (int i = 0; i < 4; i++) { 6 list0.add(i); 7 } 8 for (int i = 3; i >= 0; i--) { 9 list1.add(i); 10 } 11 set.add(list0); 12 set.add(list1); // list1和list0都能存放
HashSet(Hash 表)
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不 同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的 hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是 同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相 同的元素放在一个哈希桶中)。也就是哈希一样的存一列。如图 1 表示 hashCode 值不相同的情 况;图 2 表示 hashCode 值相同,但 equals 不相同的情况。
TreeSet(二叉树)
1. TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
2. Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。
3. 在覆写 compareTo()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序 。
4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整 数、零或正整数。
LinkedHashSet
LinkedHashSet(HashSet+LinkedHashMap)
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。 LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法 操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并 通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操 作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
Map
概述:
将键映射到值的对象
一个映射不能包含重复的键
每个键最多只能映射到一个值
Map接口和Collection接口的不同
Map是双列的,Collection是单列的
Map的键唯一,Collection的子体系Set是唯一的
Map集合的数据结构针对键有效,跟值无关;Collection集合的数据结构是针对元素有
HashMap(数组+链表+红黑树)
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快 的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记 录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导 致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。
HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍
HashMap 的实现原理?
HashMap概述: HashMap是**基于哈希表的Map接⼝的⾮同步实现**。此实现提供所有可选的映射操作,并允许使⽤null值和null键。此类
不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语⾔中,最基本的结构就是两种,⼀个是数组,另外⼀个是模拟指针(引⽤),所有的数据结构都可以
⽤这两个基本结构来构造的,HashMap也不例外。HashMap实际上是⼀个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
当我们往Hashmap中put元素时,利⽤**key的hashCode()**重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前
的key-value放⼊链表中
获取时,直接找到hash值对应的下标,在进⼀步判断key是否相同,从⽽找到对应值。
理解了以上过程就不难明⽩HashMap是如何解决hash冲突的问题,核⼼就是使⽤了**数组的存储⽅式**,然后将冲突的key的对象放⼊链
表中,⼀旦发现冲突就在**链表**中做进⼀步的对⽐。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过⼋个之后,该链表会转为红⿊树来提⾼查询效率,从原来的
O(n)到O(logn)
JDK1.8主要解决或优化了⼀下问题:
1. resize 扩容优化,每次扩容都是*2
2. 引⼊了红⿊树,⽬的是避免单条链表过长⽽影响查询效率,红⿊树算法请参考
3. 解决了多线程死循环问题,但仍是⾮线程安全的,多线程时可能会造成数据丢失问题。
为啥每次扩容都是2*n?
原因:**添加元素做位运算的时候,可以让元素放入hash表时减少冲突,充分的散列。**
第一个截图是向HashMap中添加元素putVal()方法的部分源码,可以看出,向集合中添加元素时,会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置;而第二个截图是HashMap扩容时调用resize()方法中的部分源码,可以看出会新建一个tab,然后遍历旧的tab,将旧的元素进过e.hash & (newCap - 1)的计算添加进新的tab中,也就是(n - 1) & hash的计算方法,其中n是集合的容量,hash是添加的元素进过hash函数计算出来的hash值。
HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是**按位与**的计算,这是位运算,计算机能直接运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111*111这样形式的,**这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞,下面举例进行说明**。
为什么HashMap中String、Integer这样的包装类适合作为K?
答:String、Integer等包装类的特性能够**保证Hash值的不可更改性和计算准确性**,能够有效的减少Hash碰撞的⼏率
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
内部已重写了equals()、hashCode()等⽅法,遵守了HashMap内部的规范(不清楚可以去上⾯看看putValue的过程),不容易出
现Hash值计算错误的情况;
HashMap为什么不直接使⽤hashCode()处理后的哈希值直接作为table的下标?
答:hashCode()⽅法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,⽽HashMap的容量范围是在
16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最⼤值的,并且设备上也难以提供这么多的存储空间,从⽽导致通过
hashCode()计算出的哈希值可能不在数组⼤⼩范围内,进⽽⽆法匹配存储位置;
那怎么解决呢?
1. HashMap⾃⼰实现了⾃⼰的hash()⽅法,通过两次扰动使得它⾃⼰的哈希值⾼低位⾃⾏进⾏异或运算,降低哈希碰撞概率也使得数
据分布更平均;
2. 在保证数组长度为2的幂次⽅的时候,使⽤hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的⽅式进⾏存储,这样
⼀来是⽐取余操作更加有效率,⼆来也是因为只有当数组长度为2的幂次⽅时,h&(length-1)才等价于h%length,三来解决了“哈希
值与数组⼤⼩范围不匹配”的问题;
HashMap 的长度为什么是2的幂次⽅
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红⿊树长度⼤致相同。这个实现就是把数据存到
哪个链表/红⿊树中的算法。
这个算法应该如何设计呢?
我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作
(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运
算效率,这就解释了 HashMap 的长度为什么是2的幂次⽅。
JAVA8 实现
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑 树 组成。 根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的 具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决 于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后, 会将此链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
HashMap 和 ConcurrentHashMap 的区别
ConcurrentHashMap数组+链表/红⿊⼆叉树,然后在单独一个数组位置⽤lock锁进⾏保护,相对于HashTable的
synchronized锁的粒度更精细了⼀些,并发性能更好,⽽HashMap没有锁机制,不是线程安全的。(JDK1.8之后
ConcurrentHashMap启⽤了⼀种全新的⽅式实现,利⽤CAS算法(全称是compare and swap(比较并且交换))。)
其实cas算法实现过程比较简单,就是维护了3个变量:当前内存值V、旧的预期值A、即将更新的值B。通过while循环不断获取当前内存中的数值V,如果V等于A,就把V赋值为B;整个比较并交换的操作是原子操作。
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.8 采⽤的数据结构跟HashMap1.8的结构
⼀样,**数组+链表/红⿊⼆叉树**。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是
HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要): ① JDK1.8 的时候已经摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表/红⿊树的数
据结构来实现,并发控制使⽤ **synchronized 和 CAS** 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化
过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;②
Hashtable(同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可
能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
Java8 实现 (引入了红黑树)
JDK1.8版本:做了2点修改,见下图:
取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.
put()方法,位置非null加锁 cas操作失败加锁,null不加锁
get() 可见性
扩容
size方法
**对比:**
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
JDK1.8的实现**降低锁的粒度**,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而 JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的**数据结构变得更加简单**,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用**红黑树来优化链表**,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
JDK1.8使用内置锁**synchronized**来代替重入锁ReentrantLock synchronized(数组下标位置的头节点):锁头节点
HashTable(线程安全)
Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类, 并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap, 因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全 的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
TreeMap(可排序)
TreeMap 实现 SortedMap 接口,键的数据结构是红黑树,可保证键的排序和唯一性。能够把它保存的记录根据键排序,默认是按键值的升序排序, 也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。 如果使用排序的映射,建议使用 TreeMap。 在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的 Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。
LinkHashMap(记录插入顺序)
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
map遍历
1 下面是遍历Map的四种方法: 2 3 public static void main(String[] args) { 4 5 6 Map<String, String> map = new HashMap<String, String>(); 7 map.put("1", "value1"); 8 map.put("2", "value2"); 9 map.put("3", "value3"); 10 11 //第一种:普遍使用,二次取值 12 System.out.println("通过Map.keySet遍历key和value:"); 13 for (String key : map.keySet()) { 14 System.out.println("key= "+ key + " and value= " + map.get(key)); 15 } 16 17 //第二种 entrySet迭代器 18 System.out.println("通过Map.entrySet使用iterator遍历key和value:"); 19 Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); 20 while (it.hasNext()) { 21 Map.Entry<String, String> entry = it.next(); 22 System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); 23 } 24 25 //第三种:推荐,尤其是容量大时 entrySet foreach 26 System.out.println("通过Map.entrySet遍历key和value"); 27 for (Map.Entry<String, String> entry : map.entrySet()) { 28 System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); 29 } 30 31 //第四种 32 System.out.println("通过Map.values()遍历所有的value,但不能遍历key"); 33 for (String v : map.values()) { 34 System.out.println("value= " + v); 35 } 36 }
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术