集合的特点主要有如下两点:
Map接口和Collection接口是所有集合框架的父接口:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiSLecDg-1662715587798)(D:\Environment\TyporaPicturesWorkspace\image-20220906172323283.png)]
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set两大接口:
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet。
Map:是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、
ConcurrentHashMap
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表
HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
TreeMap: 红黑树(自平衡的排序二叉树)
Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。
hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。
ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)
…
解决办法:
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
因为所有Collection接继承了Iterator迭代器。
Iterator 使用代码如下:
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
Iterator 的特点:是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
一种最常见的错误代码如下:
for(Integer i : list){
list.remove(i)
}
运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用
foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。
遍历方式有以下几种:
for 循环遍历:基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。迭代器遍历:Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。foreach 循环遍历:foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。ArrayList的优点如下:
ArrayList 的缺点如下:
ArrayList 比较适合顺序添加、随机访问的场景。
数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。
代码示例:
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();
// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。性能:ArrayList 在性能方面要优于 Vector。扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问
Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方
法将其转换成线程安全的容器后再使用。例如像下面这样:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
ArrayList 中的数组定义如下:
private transient Object[] elementData;
再看一下 ArrayList 的定义:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用 是说不希望 elementData 数组被序列化,重写了 writeObject 实现:
private void writeObject(java.io.ObjectOutputStream s) throws
java.io.IOException{
*// Write out element count, and any hidden stuff*
int expectedModCount = modCount;
s.defaultWriteObject();
*// Write out array length*
s.writeInt(elementData.length);
*// Write out all elements in the proper order.*
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后
的文件大小。
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及Set和List对比:
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统
一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层
HashMap 的相关方法来完成,HashSet 不允许重复的值。
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合
equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,
并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(
HashMap 比较key是否相等是先比较hashcode 再比较equals )。
以下是HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
| HashMap | HashSet |
|---|---|
| 实现了Map接口 | 实现Set接口 |
| 存储键值对 | 仅存储对象 |
| 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
| HashMap使用键(Key)计算 Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
| HashMap相对 HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈
希值。
链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查
等功能。
链表大致分为单链表和双向链表
单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指
针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lLxh2S8f-1662715587799)(D:\Environment\TyporaPicturesWorkspace\image-20220909161005329.png)]
不能随机查找,必须从第一个开始遍历,查找效率低
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) 。
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组
中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ekfuRDZL-1662715587801)(D:\Environment\TyporaPicturesWorkspace\image-20220909161749233.png)]
HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)
时,将链表转化为红黑树,以减少搜索时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIAGd4OA-1662715587802)(D:\Environment\TyporaPicturesWorkspace\image-20220909161812018.png)]
JDK1.7 VS JDK1.8 比较
JDK1.8主要解决或优化了一下问题:
| 不同 | JDK1.7 | JDK1.8 |
|---|---|---|
| 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 初始化方式 | 单独函数: inflateTable() | 直接集成到了扩容函数 resize() 中 |
| hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
| 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度< 8;存放单链表;冲突 & 链表长度 > 8;树化并存放红黑树 |
| 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
| 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
二叉树简单来说就是 每一个节上可以关联俩个子节点:
a
/ \
b c
/ \ / \
d e f g
/ \ / \ / \ / \
h i j k l m n o
当我们put的时候,首先计算 key 的 hash 值,这里调用了 hash 方法, hash 方法实际是让
key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变,
所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰
撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) &
hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者
综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中
用了复杂度 O(logn)的树结构来提升碰撞下的性能。
putVal方法执行流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dae3y6ov-1662715587803)(D:\Environment\TyporaPicturesWorkspace\image-20220909162945794.png)]
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤①:tab为空则创建
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数
组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 步骤③:节点key存在,直接覆盖value
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// 步骤④:判断该链为红黑树
// hash值不相等,即key不相等;为红黑树结点
// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
//判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
//判断链表的长度是否达到转化红黑树的临界值,临界值为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表结构转树形结构
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个
值
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 步骤⑥:超过最大容量就扩容
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向
⑥,如果table[i]不为空,转向③;
判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的
是hashCode以及equals;
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值
对,否则转向5;
遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操
作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩
容
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知
道什么是哈希才行;
Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度
的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希
碰撞)。
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放
地址法可以解决哈希冲突:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WHGg4DIX-1662715587804)(D:\Environment\TyporaPicturesWorkspace\image-20220909163902422.png)]
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽
位。
但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的
低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,
进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的
hash()函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只
进行了1次位运算和1次异或运算(2次扰动);
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减
少Hash碰撞的几率都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况内部已重写了 equals() 、 hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以
去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
答:重写 hashCode() 和 equals() 方法
重写 hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中重写 equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用答: hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空
间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最
大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可
能不在数组大小范围内,进而无法匹配存储位置;
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树
长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是
2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的
前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效
率,这就解释了 HashMap 的长度为什么是2的幂次方。
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置
的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的
目的;
线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛初始容量大小和每次扩充容量大小的不同 :底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一
个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向
HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8
采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式:
在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) : 使用 synchronized 来保证线程安全,效率非常低下。当一个线程
访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加
元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
1、HashTable:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6Kcukjr-1662715587804)(D:\Environment\TyporaPicturesWorkspace\image-20220909170010083.png)]
2、 JDK1.7的ConcurrentHashMap:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhneQNcu-1662715587805)(D:\Environment\TyporaPicturesWorkspace\image-20220909170029677.png)]
3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GNMTmD5m-1662715587806)(D:\Environment\TyporaPicturesWorkspace\image-20220909170046338.png)]
答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同
步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行
时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的
compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映
射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。
Collections 工具类的 sort 方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当
我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排
序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个
Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的
Collections.sort().