(引用网上集合图片图)
Collection
List | 存取有序,有索引,元素可以重复 |
---|---|
ArrayList | 底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素 |
Vector | 底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素 |
LinkedList | 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素 |
Set | 不保存重复的元素,不保证维护元素的次序 |
---|---|
HasSet | 底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素。hashCode()和equals()保证元素唯一性 |
LinkedHashSet | 底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。 |
TreeSet | 底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性 |
Map
Map | Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复 |
---|---|
HashMap | 底层数组+链表实现,可以存储null键和null值,线程不安全 |
TreeMap | 底层使用的数据结构是二叉树,无序(指元素顺序与添加顺序不一致) ; 可以对元素进行排序,使用自然排序和定制排序中的一种 。key不能重复,可以插入null键 |
HashTable | 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低 |
ConcurrentHashMap | 底层采用分段的数组+链表实现,线程安全,是HashTable的替代 。原因是Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 |
1、ArrayList 和 LinkedList 的区别?
ArrayList :底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素。
LinkedList :底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素。
ArrayList 的增删未必就是比 LinkedList 要慢
对于ArrayList 如果增删都是在末尾来操作【每次调用的都是 remove() 和 add()】,此时 ArrayList 就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。
如果删除操作的位置是在中间。由于 LinkedList 的消耗主要是在遍历上,ArrayList 的消耗主要是在移动和复制上(底层调用的是 arrayCopy() 方法,是 native 方法)。LinkedList 的遍历速度是要慢于 ArrayList 的复制移动速度的如果数据量有百万级的时,还是 ArrayList 要快。
2、ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?
判断list是否实现了该接口,从而选取特定的遍历方式;RandomAccess接口是如何让ArrayList进行快速随机访问的了
RandomAccess是一个空的接口,是一个标志接口。
通过Collections的binarySearch()方法我们可以看到里面有用到RandomAccess,它先判断集合是否实现了RandomAccess从而判断是使用indexedBinarySearch()方法还是iteratorBinarySearch()方法
ArrayList 用 for 循环遍历比 iterator 迭代器遍历快,LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。所以LinkedList没有实现RandomAccess 接口
为什么ArrayList建议使用for,LinkedList使用iterator
数组支持随机访问,但是链表不支持随机访问
for (int i = 0; i < list.size(); i++){
list.get(i);
}
LinkedList
不支持按照索引查找元素,因此使用get(i)
,会对链表进行遍历查找,效率低
ArrayList
由于它底层使用的对象数组实现,对象数组能够支持随机访问(按索引查找),效率高。
Iterator it = list.iterator();
while (it.hasNext()) {
int str = (int) it.next();
System.out.print(str);
}
对于LinkedList
来说 iterator
的next()
,相当于获取下一个链表节点元素,按顺序读取数据,效率高。
3、ArrayList 的扩容机制?
java中标准数组是定长的,在数组被创建之后,它们不能被加长或缩短。
ArrayList底层是基于定长数组实现的,是一个动态数组,支持自动扩容。
无参构造
默认扩容至数组大小为10
List<String> list = new ArrayList<String>();
有参构造
扩容至数组为指定参数大小
List<String> list = new ArrayList<String>(20);
add()方法
list.add("a");
//将指定元素添加到list的末尾
public boolean add(E e) {
//因为要添加元素,所以添加之后可能导致容量不够,所以需要在添加之前进行判断(扩容)
ensureCapacityInternal(size + 1); // Increments modCount!!(待会会介绍到fast-fail)
elementData[size++] = e;
return true;
}
当使用 add 方法的时候首先调用 ensureCapacityInternal 方法,传入 size+1 进去,检查是否需要扩充 elementData 数组的大小;
如果需要扩容,需要利用ArrayList扩容支持的核心方法grow()
方法
grow()
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去
当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。ArrayList扩容的长度是原长度的1.5倍
private void grow(int minCapacity) {
// 获取到ArrayList中elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
// 不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
4、Array 和 ArrayList 有何区别?什么时候更适合用 Array?
Array是Java中的数组,声明数组有三种方式
int[] a=new int[10];
int a[]=new int[10];
int a[]={1,2,3,4};
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的
Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
什么时候更适合使用 Array:
如果列表的大小已经指定,大部分情况下是存储和遍历它们;
对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢;
如果你要使用多维数组,使用 [ ][ ] 比 List> 更容易。
5、HashMap 的实现原理/底层数据结构?JDK1.7 和 JDK1.8
JDK1.7:Entry数组 + 链表
JDK1.8:Node 数组 + 链表/红黑树,当链表
上的元素个数超过 8 个并且数组长度 >= 64 时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到 O(logN)。Entry 和 Node 都包含 key、value、hash、next 属性。
HashMap的数据结构为数组+链表,以key,value的形式存储,通过调用put和get方法来存制和取值。
HashMap是Y轴方向是数组,X轴方向就是链表的存储方式。
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置
当程序执行 map.put(String,Obect)
方法 时,系统将调用String的 hashCode()
方法得到其 hashCode
值
1、当执行map.put("a","aa");
系统会先通过hashCode来获取元素存储位置。
2、如果对应下标的位置没有元素,则直接在Entry数组中对于存储位置创建一个Entry元素。
3、如果存在相同的hashcode,比如添加map.put("a1","aa1");
代表出现hash冲突
3.1、 使用equals()
方法判断他们key是否相同, 如果相同的话,直接覆盖原来数据
3.2、如果key不相同,就会按照链表顺序依次遍历entry元素,并且执行3.1
验证。
3.3、如果遍历链表都无法找到相同的key的数据,就会添加新数据值链表末尾节点。
PS: table[0] 的位置是专门用来存放,key=null的情况
6、HashMap 的 put 方法的执行过程?
put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作;
通过 hash 计算下标并检查 hash 是否冲突,也就是对应的下标是否已存在元素。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
如果对应下标的位置没有元素,则直接创建一个。
如果有元素,说明 hash 冲突了,则再次进行 3 种判断。
- 判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新e的value,也就是替换操作。
- 如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。
- 如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。
对维护着迭代器的modCount 变量加一
最后判断,如果当前数组的长度已经大于阀值了。则重新hash数组。
当元素向HashMap容器中添加元素的时候,会判断当前元素的个数,如果当前元素的个数大于等于阈(yu)值时,即当前数组table的长度*加载因子加载因子=0.75
就要进行自动扩容。
7、HashMap 的 get 方法的执行过程?
table[0] 的位置是专门用来存放,key=null的情况。
第一步:判断key==null,如何map也为空的话,直接返回null,否则返回table[0]元素的value值
第二步:调用hashCode(key)获取key在table数组中存储位置。
第三步: 判断table数组对应的存储位置上是否存在Entry元素,如果不存在直接返回null
第四步:如果table对应的存储位置上存在Entry元素,如上图Entry[0],判断Entry[0].key是否和key相等,如果不相等,依次遍历Entry[0]下的链表的entry元素,直到找到满足key.equals(e.key)
条件的,返回entry.value值。
第五步:如果都不满足条件,直接返回null
8、HashMap 的 resize 方法的执行过程?
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容
最消耗性能:原数组中的数据必须重新计算其在新数组中的位置,并放进去
有两种情况会调用 resize 方法:
- 第一次调用 HashMap 的 put 方法时,会调用 resize 方法对 table 数组进行初始化,如果不传入指定值,默认大小为 16。
- 当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念fa值,念yu值四声)—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容后的数组容量应该为原数组的两倍,并且这里的数组大小必须是2的幂
9、HashMap 的 size 为什么必须是 2 的整数次方?
如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找
HashMap中的链表出现越少,性能才会越好,也就是减少hash冲突的出现
以JDK1.8为例,查看下面一段代码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算数组索引下标的位置
tab[i = (n - 1) & hash]
&为二进制中的与运算,即:两位同时为“1”,结果才为“1”,否则为0。如1&0=0; 1&1=1
通过计算发现如果hashMap 的数组长度都是2的n次幂 ,那么对于这个数再减去1,转换成二进制的话,就肯定是最高位为0,其他位全是1 的数
我们测试下
1、数字8减去1转换成二进制是0111
第一个key的hashcode值:10101001 ,通过10101001 & 0111 计算,结果:101000001
第一个key的hashcode值:11101000 ,通过11101000 & 0111 计算,结果:101000000
2、数字16减去1转换成二进制是01111
3、数字32减去1转换成二进制是011111
如果是非 2 的整数次方的数
当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算 的时候,会出现重复的数据,
因为不为2的n次幂 的话,对应的二进制数肯定有一位为0 , 这样不管你的hashCode 值对应的该位,是0还是1 。
最终得到的该位上的数肯定是0,这带来的问题就是HashMap上的数组元素分布不均匀,而数组上的某些位置,永远也用不到。
这将带来的问题就是你的HashMap 数组的利用率太低,并且链表可能因为上边的(n - 1) & hash 运算结果碰撞率过高,导致链表太深。(当然jdk 1.8已经在链表数据超过8个以后转换成了红黑树的操作,但那样也很容易造成它们之间的转换时机的提前到来),所以说HashMap的长度一定是2的次幂,否则会出现性能问题。
10、HashMap 多线程死循环问题?
HashMap不是线程安全的,在被多线程共享操作时,会有问题.
主要是多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环.
解决办法:
1、用Collections工具类的synchronizedMap()方法达到线程安全的目的。但由于synchronized是串行执行,在访问量很大的情况下效率很低,不推荐使用
2、推荐使用ConcurrentHashMap
11、HashMap 的 get 方法能否判断某个元素是否在 map 中?
HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,因为 get 返回 null 有可能是不包含该 key,也有可能该 key 对应的 value 为 null。因为 HashMap 中允许 key 为 null,也允许 value 为 null。
12、HashMap 与 HashTable 的区别是什么?
1、HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
2、HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable 遇到 null,直接返回 NullPointerException。
3、Hashtable 是线程安全的,而 HashMap 不是线程安全的,但是我们也可以通过 Collections.synchronizedMap(hashMap),使其实现同步。
HashTable的补充:
HashTable 和 HashMap 的实现原理几乎一样,差别无非是
-
HashTable 不允许 key 和 value 为 null;
-
HashTable 是线程安全的。但是 HashTable 线程安全的策略实现代价却太大了,简单粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
13、HashMap 与 ConcurrentHashMap 的区别是什么?
HashMap 不是线程安全的,而 ConcurrentHashMap 是线程安全的。
ConcurrentHashMap 采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段 segment,而且每个小的片段 segment 上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段 segment,然后再在这个片段上面进行插入,而且这里还需要获取 segment 锁,这样做明显减小了锁的粒度。
14、 HashTable 和 ConcurrentHashMap 的区别?
HashTable 和 ConcurrentHashMap 相比,效率低。 Hashtable 之所以效率低主要是使用了 synchronized 关键字对 put 等操作进行加锁,而 synchronized 关键字加锁是对整张 Hash 表的,即每次锁住整张表让线程独占,致使效率低下,而 ConcurrentHashMap 在对象中保存了一个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行 put 操作时首先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可,因此, ConcurrentHashMap 在多线程并发编程中可是实现多线程 put操作。
15、ConcurrentHashMap 的实现原理是什么?
数据结构
JDK 7:中 ConcurrentHashMap 采用了数组 + Segment + 分段锁的方式实现。
JDK 8:中 ConcurrentHashMap 参考了 JDK 8 HashMap 的实现,采用了数组 + 链表 + 红黑树的实现方式来设计,内部大量采用 CAS 操作。
ConcurrentHashMap 采用了非常精妙的"分段锁"策略,ConcurrentHashMap 的主干是个 Segment 数组。
Segment 继承了 ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在 ConcurrentHashMap,一个 Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,并发环境下,对于不同 Segment 的数据进行操作是不用考虑锁竞争的。就按默认的 ConcurrentLevel 为 16 来讲,理论上就允许 16 个线程并发执行。
所以,对于同一个 Segment 的操作才需考虑线程同步,不同的 Segment 则无需考虑。Segment 类似于 HashMap,一个 Segment 维护着一个HashEntry 数组:
HashEntry 是目前我们提到的最小的逻辑处理单元了。一个 ConcurrentHashMap 维护一个 Segment 数组,一个 Segment 维护一个 HashEntry 数组。因此,ConcurrentHashMap 定位一个元素的过程需要进行两次 Hash 操作。第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部。
16、HashSet 的实现原理?
底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素。hashCode()和equals()保证元素唯一性
HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的值已经存在时返回 false。
17、HashSet 怎么保证元素不重复的?
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
元素值作为的是 map 的 key,map 的 value 则是 PRESENT 变量,这个变量只作为放入 map 时的一个占位符而存在,所以没什么实际用处。其实,这时候答案已经出来了:HashMap 的 key 是不能重复的,而这里HashSet 的元素又是作为了 map 的 key,当然也不能重复了。
18、LinkedHashMap 的实现原理?
底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
LinkedHashMap 也是基于 HashMap 实现的,LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用
,从而在哈希表的基础上又构成了双向链接列表,来实现按插入顺序或访问顺序排序。
LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入顺序,则为 false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "e");
for (Iterator<String> iterator = map.values().iterator(); iterator
.hasNext();) {
String name = (String) iterator.next();
System.out.print(name);
}
}
abce,按照加入的顺序打印
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "e");
map.get("1");
map.get("2");
for (Iterator<String> iterator = map.values().iterator(); iterator
.hasNext();) {
String name = (String) iterator.next();
System.out.print(name);
}
}
打印结果为:ceab
这就是基于访问的顺序,get一个元素后,这个元素被加到最后(使用了LRU 最近最少被使用的调度算法)
19、Iterator 怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。Java 中的 Iterator 功能比较简单,并且只能单向移动:
使用方法 iterator() 要求容器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个元素。注意:iterator() 方法是 java.lang.Iterable 接口,被 Collection 继承。
使用 next() 获得序列中的下一个元素。
使用 hasNext() 检查序列中是否还有元素。
使用 remove() 将迭代器新返回的元素删除。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorDemo {
public static void main(String[] args) {
List<String> lst = new ArrayList<String>();
lst.add("aaa");
lst.add("bbb");
lst.add("ccc");
Iterator<String> iterator = lst.iterator();
//iterator.hasNext()如果存在元素的话返回true
while(iterator.hasNext()) {
//iterator.next()返回迭代的下一个元素
System.out.println(iterator.next());
}
}
}
20、Iterator 和 ListIterator 有什么区别?
Iterator | |
---|---|
hasNext() | 如果迭代器指向位置后面还有元素,则返回 true,否则返回false |
next() | 返回集合中Iterator指向位置后面的元素 |
remove() | 删除集合中Iterator指向位置后面的元素 |
ListIterator | |
---|---|
add(E e) | 将指定的元素插入列表,插入位置为迭代器当前位置之前 |
hasNext() | 以正向遍历列表时,如果列表迭代器后面还有元素,则返回 true,否则返回false |
hasPrevious() | 如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回false |
next() | 返回列表中ListIterator指向位置后面的元素 |
nextIndex() | 返回列表中ListIterator所需位置后面元素的索引 |
previous() | 返回列表中ListIterator指向位置前面的元素 |
previousIndex() | 返回列表中ListIterator所需位置前面元素的索引 |
remove() | 对迭代器使用hasNext()方法时,删除ListIterator指向位置后面的元素;当对迭代器使用hasPrevious()方法时,删除ListIterator指向位置前面的元素 |
set(E e) | 从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e |
相同点
都是迭代器,当需要对集合中元素进行遍历不需要干涉其遍历过程时,这两种迭代器都可以使用。
不同点
1、使用范围不同,Iterator可以应用于所有的集合,Set、List和Map和这些集合的子类型。而ListIterator只能用于List及其子类型。
2、ListIterator有add方法,可以向List中添加对象,而Iterator不能。
3、ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator不可以。
4、ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
5、都可实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。
参考:https://blog.csdn.net/longshengguoji/article/details/41551491
21、Iterator 和 Enumeration 接口的区别?
1、Enumeration速度是Iterator的2倍,同时占用更少的内存。
2、Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。
3、当其他线程修改正在被iterator遍历的集合,会抛出 ConcurrentModificationException 异常。这其实就是 fail-fast 机制。
4、Iterator允许调用者删除底层集合里面的元素,而通过Enumeration,我们只能读取集合的数据,不能对数据进行修改。
5、Iterator 支持 fail-fast 机制,而 Enumeration 不支持 ,因此比 Enumeration 更安全
package java.util;
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
22、fail-fast 与 fail-safe 有什么区别?
快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
java.util.ArrayList
java.util.LinkedList
java.util.Vector
java.util.Stack
安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
如:ConcurrentHashMap
参考:https://www.cnblogs.com/yonyong/p/9323244.html
23、Collection 和 Collections 有什么区别?
Collection:是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素。它的直接继承接口有 List,Set 和 Queue。
Collections:是不属于 Java 的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例化, 服务于 Java 的 Collection 框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。
参考:https://blog.csdn.net/pcwl1206/article/details/100977230