Java面试题(二)集合

本文深入解析Java集合框架,包括ArrayList、LinkedList、HashMap等核心数据结构的实现原理,探讨其内部机制如扩容、遍历及线程安全性,对比不同集合类的特点与适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(引用网上集合图片图)

在这里插入图片描述

Collection

List存取有序,有索引,元素可以重复
ArrayList底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
Vector底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
LinkedList底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
Set不保存重复的元素,不保证维护元素的次序
HasSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素。hashCode()和equals()保证元素唯一性
LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性

Map

MapMap里保存着两组数据: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来说 iteratornext(),相当于获取下一个链表节点元素,按顺序读取数据,效率高。

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 方法:

  1. 第一次调用 HashMap 的 put 方法时,会调用 resize 方法对 table 数组进行初始化,如果不传入指定值,默认大小为 16。
  2. 当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念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 的实现原理几乎一样,差别无非是

  1. HashTable 不允许 key 和 value 为 null;

  2. 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值