集合概述
1、Java集合概览
Java集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection 接口(存放单一元素);另一个是 Map 接口(存放键值对)。对于 Collection接口,下面又有三个主要的子接口:List、Set和 Queue。
- List(对付顺序的好帮手):存储的元素是有序的、可重复的,可以插入多个null。List底层实现有数组、链表两种方式。
- Set(注重独一无二的性质):...无序的、不可重复的,只允许一个null。
- Queue(先进先出。如:实现排队功能的叫号机):按特定的排队规则来确定先后顺序,...有序的、可重复的。
- Vector:...有序的、可重复的,线程安全的(在进行修改操作时,会自动加锁处理),容量可变的(可以根据需要自行增长或缩小),可以通过迭代器、for-each循环等方式进行遍历,性能较差。
- Map(用key来搜索的专家):使用键值对(key-value)存储,类似于数学上的函数y=f(x),“x”代表key,“y”代表value,key是无序的、不可重复的,value是无序的、可重复的,每个键最多映射到一个值。
- Set、Map容器有基于哈希存储和红黑树两种方式实现。Set基于Map实现,Set里的元素值就是Map的键值。
Collections常见的实现类:ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap、PriorityQueue
2、如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map 接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
3、Java中线程安全的集合有哪些?
- Vector:就比Arraylist多了个同步化机制(它给几乎所有的 public 方法都加上了 synchronized 关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在 Vector 已被弃用)。
- Stack:栈,也是线程安全的,继承于Vector。
- Hashtable:HashTable 和 HashMap 类似,不同点是 HashTable 是线程安全的,但它给几乎所有 public 方法都加上了 synchronized 关键字,还有一个不同点是 HashTable 的K,V都不能是 null,但 HashMap 可以,所以它现在也因为性能原因被弃用了。
- Collections 包装方法:
- Vector 和 HashTable 被弃用后,他们被 ArrayList 和 HashMap 代替,但他们是线程不安全的,所以 Colletions 工具类中提供了相应的包装方法把他们包装成线程安全的集合。
Collections 针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步。List<E> synchronizedList = Collections.synchronizedList(new ArrayList<>()); Map<Object, Object> synchronizedMap = Collections.synchronizedMap(new HashMap<>(16)); Set<Object> synchronizedSet = Collections.synchronizedSet(new HashSet<>()); - ConcurrentHashMap:是HashMap的线程安全版本,内部采用分段锁机制实现,可以支持高并发的读写操作。
- CopyOnWriteArrayList:是ArrayList的线程安全版本,内部采用复制机制实现,在写操作时会先复制一个新的集合,然后在新集合上进行写操作,读操作仍然在旧集合上进行,可以保证读写之间的数据一致性。
- CopyOnWriteArraySet
- 其他并发实现类:有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到。
线程不安全:
- HashMap
- ArrayList
- LinkedList
- HashSet
- TreeSet
- TreeMap
4、Collection 和 Collections有什么区别?
-
Collcetion是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法(包括添加、删除、遍历、查找等)。实现该接口的类主要有List、Set、Queue,Collection常用的接口方法有:
- add(E e) :将元素e添加到集合中;
- remove(Object o) :从集合中删除对象o;
- contains(Object o) :判断集合中是否包含对象o;
- size() :返回集合中元素的个数;
- iterator() :返回集合中元素的迭代器。
-
Collections:java.util.Collections,是不属于java的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例化, 服务于java的Collection框架。
它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。Collections集合类的常用方法有:
- sort():对集合进行排序;
- shuffle():随机打乱集合中元素的顺序;
- reverse():反转集合中元素的顺序;
- binarySearch():使用二分查找算法在集合中查找指定元素并返回其索引;
- max():返回集合中最大的元素;
- min():返回集合中最小的元素;
- replaceAll():用新值替换集合中所有旧值;
- copy(List m,List n):将集合n中的元素全部复制到m中,并且覆盖相应索引的元素;
- fill(List list,Object o):用对象o替换集合list中的所有元素
5、Collections常用方法
| 方法 | 功能 |
|---|---|
| static <T> boolean addAll(Collection<? super T>c, T... elements) | 将所有指定元素添加到指定集合c中 |
| static void reverse(List list) | 反转指定List集合中元素的顺序 |
| static void shuffle(List list) | 对List集合中的元素进行随机排序 |
| static void sort(List list) | 根据元素的自然顺序对List集合中的元素进行排序 |
| static void swap(List list,int i,int j) | 将指定List集合中角标i处元素和j处元素交换 |
| static int binarySearch(List list,Object key) | 使用二分法搜索指定对象在List集合中的索引,查找的List集合中的元素必须是有序的 |
| static Object max(Collection col) | 根据元素的自然顺序,返回给定集合中最大的元素 |
| static Object min(Collection col) | 根据元素的自然顺序,返回给定元素中最小的元素 |
| static boolean replaceAll(List list,Object oldVal,Object newVal) | 用一个新值newVal替换List集合中所有的旧值oldVal |
6、Arrays.sort和Collections.sort?
-
Arrays.sort方法是用于对数组进行排序的方法,默认升序。
需要注意的是,Arrays.sort方法只能用于基本数据类型(如int、double等)和实现了Comparable接口的对象类型。如果要对自定义对象类型进行排序,需要实现该对象的Comparable接口或者提供一个Comparator比较器。
-
Collections.sort方法是用于对集合进行排序的方法,默认升序。
与Arrays.sort不同,Collections.sort可以用于任何对象类型,包括自定义对象类型。只需要确保对象类型实现了Comparable接口或者提供一个Comparator比较器。
总结:Arrays.sort和Collections.sort都是Java中常用的排序方法,但它们的使用场景和限制略有不同。Arrays.sort主要用于对基本数据类型和实现了Comparable接口的对象类型进行排序,而Collections.sort则主要用于对任意对象类型进行排序。在使用时,需要根据具体的数据结构和对象类型选择合适的方法。
List
1、Vector和Stack
List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 synchronized 关键字,就导致执行效率会比较低,所以现在已经很少用了。
更好的选择是并发包下的 CopyOnWriteArrayList。
Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop、peek 等方法),所以叫栈。
不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了。
2、ArrayList与Vector区别?
- 线程安全:Vector是线程安全的,而ArrayList不是。
- 扩容机制:Vector每次扩容会将容量增加一倍,而ArrayList每次扩容会将容量增加原来的一半。
- 性能:由于Vector需要考虑线程安全问题,所以相较于ArrayList来说,性能略微差一些。
- 应用场景:Vector适用于多线程环境下需要保证线程安全的场景,而ArrayList适用于单线程环境。
3、ArrayList了解吗?
- ArrayList是Java中的一种集合类,它实现了List接口。
- 它的底层是Object类型的(动态)数组,可以自动扩展数组的大小以容纳更多的元素。当数组存储的元素数超过容量时,ArrayList会自动增加存储容量,以此达到动态扩容的效果。
- ArrayList的主要优点在于,它可以进行快速的随机访问、元素的添加和删除操作,使得代码实现更加简单高效。此外,在遍历ArrayList时,也非常方便,可以使用for循环、迭代器等多种方式。
- 需要注意的是,由于ArrayList不是线程安全的,如果在多线程环境中使用需要采取措施来保证线程安全,比如同步访问、使用线程安全的集合类等。
4、Linkedlist了解吗?
- LinkedList是Java中的一种集合类,它实现了List接口。
- 它的底层是双向链表。它的节点包含了本节点的引用、前一个节点的引用和后一个节点的引用,这些引用允许节点连接起来,形成一个链表。
- LinkedList适用于需要进行频繁的添加、删除、移动操作的场景,例如可以用LinkedList实现栈和队列等数据结构。
- 和ArrayList相比,LinkedList在插入和删除元素时的效率更高,因为只需要修改相邻元素的引用,不需要移动元素。但在随机访问时效率较低,因为需要遍历整个链表才能找到指定元素。
在最坏情况下,查询、插入和删除元素的时间复杂度都是O(n),其中n是链表中元素的数量。因为在LinkedList中,要查找一个元素,需要从头节点开始遍历整个链表,直到找到目标元素或者到达链表末尾;要在链表中插入或删除一个元素,也需要遍历链表找到目标位置。因此,LinkedList的查询、插入和删除操作的效率相对较低,尤其是在大规模的数据操作中。
5、ArrayList与LinkedList的区别?
得分点:数据结构、访问效率
标准回答:
- 底层实现:ArrayList的实现是基于(Object)数组,LinkedList的实现是基于双向链表。
- 访问速度:对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。
- 插入和删除元素:对于插入和删除操作,LinkedList要优于ArrayList,在插入和删除元素时只需要修改前一个节点和后一个节点的指针即可,不需要进行数组元素的移动,所以效率相对较高。ArrayList插入、删除的时间复杂度为O(N),LinkedList插入、删除的时间复杂度为O(N)。
- 内存空间:LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
追问:ArrayList和LinkedList的常见使用场景?
- 如果需要频繁进行插入和删除操作,且不需要随机访问元素,那么LinkedList是更好的选择。比如像音乐播放器中的播放列表,需要频繁地添加、删除、移动歌曲。
- 如果需要频繁进行随机访问,那么ArrayList是更好的选择。比如像图书馆的图书目录,需要快速地根据书名、作者等信息查找对应的书籍。
- 如果需要同时进行插入/删除和随机访问操作,则需要根据具体情况进行权衡和选择。比如像在线游戏中的玩家列表,需要频繁地添加、删除、移动玩家,但同时需要根据玩家ID快速地查找对应的玩家。
6、场景:有一堆数据不知道大小,ArrayList存好还是LinkedList存好?
对于不知道数据大小的情况,基于对内存、访问方式、添加/删除的需求和开销等方面的综合考虑,我认为在一般情况下,选择ArrayList比LinkedList更合适。虽然LinkedList在添加或删除元素时更有效率,但是在实际应用中,我们通常会在数据集合内进行访问,也就是需要随机访问,这时ArrayList的效率更高。 另外,ArrayList在底层使用数组来存储数据,这个数组的长度是动态变化的,可以根据需要自动扩展和收缩,因此空间利用率更高。当存储数据元素较多时,LinkedList节点数量增多可能会导致指针增加,占用更多的内存。 当然,LinkedList也有其适用的场合,比如需要频繁插入或删除数据的操作,或者在链表的开头或结尾进行操作等情况。因此选择ArrayList还是LinkedList,还要具体问题具体分析,综合考虑使用场景和对性能的需求来做出权衡选择。
ArrayList扩容,虽然这个过程中需要重新分配和复制数据,但是由于扩容频率不会太高,因此不会对程序性能造成太大影响。另外,我们也可以在实例化时指定预期容量,这样可以避免不必要的扩容过程,提高程序的性能。
7、数据非常大,用ArrayList和linkedlist哪个空间浪费大?
当数据非常大时,使用LinkedList会浪费更多的空间,因为LinkedList在每个元素中都需要存储前后元素的引用,而且还需要为每个元素分配一个内存空间,这就导致了在元素数量很大时,LinkedList会浪费更多的内存空间。
相比之下,ArrayList使用数组实现,在创建时就分配了一段连续的内存空间,并且在数组空间不足时会自动扩容。虽然在扩容时需要重新分配内存空间并且有可能会浪费一些内存,但是相对于LinkedList而言,ArrayList的内存利用率更高。
因此,在数据量很大时,使用ArrayList的空间浪费要比LinkedList小。但是如果需要频繁进行插入/删除操作,LinkedList会比ArrayList表现更好,因为插入/删除操作时不需要移动其他元素。
需要根据具体的场景和需求,综合考虑选择使用哪种数据结构。
8、既然有数组,为什么还要用ArrayList和LinkedList?
以下是ArrayList和LinkedList对于数组的优势:
- 大小可变性:数组长度固定,无法修改,ArrayList和LinkedList可以在必要时动态增加或减少存储元素的容量,避免因长度限制而造成的不便。
- 插入和删除元素的效率:数组在插入和删除元素时可能需要移动其他元素的位置,降低效率。ArrayList在插入和删除元素时同样有这个问题,但LinkedList则能够在 O(1) 时间内在任何位置插入和删除元素。
- 内存利用率:数组在创建时必须预留一段连续的内存空间,可能会浪费一部分内存。而ArrayList和LinkedList可以根据实际需要动态分配内存,能够更灵活地利用内存空间。
- 线程安全性:数组在多线程环境下需要使用同步机制来保证线程安全,而ArrayList存在线程安全问题,但可以通过 Collections.synchronizedList() 或 Lock 接口来解决。LinkedList的节点是线程安全的,但由于它也实现了 List 接口,所以也没有被针对并发操作进行优化的方法。
9、ArrayList扩容机制
9.1 说说ArrayList的扩容机制?
当以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。当插入的元素个数大于当前容量时,就需要进行扩容了, ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右。
ArrayList的扩容方式是在底层数组已满时,创建一个新的数组,并将原来的元素复制到新的数组中。具体步骤如下:
- 计算新数组的大小,在原有容量的基础上扩大为原来数组容量的1.5倍(当容量不够时,还可以扩大为所需大小)。
- 创建新数组,将原来的数组元素复制到新数组中。使用System.arraycopy方法是在底层实现。
- 替换掉原来的数组引用,使ArrayList的底层数组引用指向新数组。
注意:ArrayList在扩容时需要进行数组复制,因此最好提前预估列表元素数量,避免频繁进行扩容操作,因为扩容操作是很耗时的。
ArrayList可以根据需要自动扩容与缩容:当ArrayList添加元素时,如果容量不足,就会自动扩容,扩容的大小通常是原来容量的1.5倍,然后将原数组中的元素复制到新数组中。当ArrayList删除元素时,如果元素个数少于容量的1/3,就会自动缩容,缩容的大小通常是原来容量的2/3,然后将原数组中的元素复制到新数组中。
9.2 Arraylist 扩容的数组长度为什么是原数组长度的 1.5 倍?
在ArrayList的扩容操作中,扩容后的新数组大小 = 原数组大小 * 1.5。这个倍数的选择是为了在不断添加元素时,尽可能减少扩容的次数,从而优化性能。而数学上,1.5倍也是比较合适的一个选择,因为它比1.0倍稍微大点,但比2.0倍要小点,因此可以达到一个比较好的平衡。当然,在特定场景下,可能需要自己定义更适合的扩容倍数。
9.3 Arraylist扩容,数组特别大会有什么问题?
(效率低,占内存)
当ArrayList扩容时,它需要创建一个新的数组,并将所有元素从旧数组复制到新数组中。如果数组特别大,这个过程可能会消耗大量的内存和时间,并且可能会导致内存溢出或性能问题。 此外,由于Java中数组的最大大小受限于JVM的内存限制,因此如果数组非常大,则可能无法在内存中创建。
9.4 ArrayList 是怎么添加/删除元素的
添加元素有两种方式:add方法和addAll方法。
- add:在列表末尾添加一个元素。如果当前列表的元素数量超过了底层数组的容量,则进行扩容操作。
- addAll:将集合中的所有元素添加到列表末尾。如果当前列表的元素数量加上集合大小超过了底层数组的容量,则进行扩容操作。
在删除元素有两种方式:remove方法和removeAll方法。
- remove:删除指定位置的元素,并返回该元素。删除后,后面的元素会自动向前移动。
- removeAll:从列表中删除所有在集合中出现的元素。删除时,同样会自动向前移动后面的元素。
9.5 Arraylist怎么删除奇数位元素?
可以通过循环遍历ArrayList,使用remove方法删除奇数位元素。具体步骤如下:
- 使用for循环遍历ArrayList,从第一个元素开始,每隔一个元素删除一个。
- 在循环中,使用remove方法删除当前位置的元素,即i位置的元素。
- 由于删除一个元素后,后面的元素会自动往前移,所以需要将i减1,保证下一次循环仍然是从奇数位开始。
示例代码如下:
for (int i = 0; i < arrayList.size(); i += 2) {
arrayList.remove(i);
i--;
}
9.6 为什么ConcurrentHashMap不允许插入null值?
(若插入会抛出空指针异常)
因为给ConcurrentHashMap中插入 null(空)值会存在歧义。我们可以假设ConcurrentHashMap允许插入 null(空) 值,那么,我们取值的时候会出现两种结果:
- 值没有在集合中,所以返回的结果就是 null (空);
- 值就是 null(空),所以返回的结果就是它原本的 null(空) 值。
HashMap允许插入 null 值,是因为HashMap的设计是给单线程使用的,所以如果取到 null 值,我们可以通过HashMap的 containsKey(key)方法来区分这个 null 值到底是插入值为空,还是本就没有才返回的空值。
而 ConcurrentHashMap 就不一样了,因为 ConcurrentHashMap 是在多线程场景下使用的,它的情况更加复杂。
10、List采用for each调用了remove方*** 抛出什么异常?为什么?
在使用for each循环时,如果同时调用了List的remove方法,会抛出ConcurrentModificationException异常(并发修改异常)。这是因为for each循环是基于迭代器实现的,而在调用List的remove方法时,会修改列表的结构,导致迭代器失效,从而抛出异常。 可以通过使用Iterator的remove方法来解决这个问题。
11、如何实现List集合去重?
List 去重有以下 3 种实现思路:
- 自定义方法去重,通过循环判断当前的元素是否存在多个,如果存在多个,则删除此重复项,循环整个集合最终得到的就是一个没有重复元素的 List;
- 使用 Set 集合去重,利用 Set 集合自身自带去重功能的特性,实现 List 的去重;
- 使用JDK8中Stream流的去重功能(推荐)。Stream 中包含了一个去重方法:distinct,可以直接实现集合的去重功能
自定义去重的实现方法有两种:
- 我们可以创建一个新集合,通过循环原集合判断循环的元素,是否已存在于新集合,如果不存在则插入,否则就忽略,这样循环完,最终得到的新集合就是一个没有重复元素的集合;
- 使用迭代器循环并判断当前元素首次出现的位置(indexOf)是否等于最后出现的位置(lastIndexOf),如果不等于则说明此元素为重复元素,删除当前元素即可,这样循环完就能得到一个没有重复元素的集合。
总结:自定义去重功能实现起来相对繁琐,而 Set 集合依靠其自带的去重特性,可以很方便的实现去重功能,并且可以使用 LinkedHashSet 在去重的同时又保证了元素所在位置不被更改。而最后一种去重的方法,是 JDK 8 中新增的,使用 Stream 中的 distinct 方法实现去重,它的优点是不但写法简单,而且无需创建新的集合,是实现去重功能的首选方法。
12、List 集合遍历的方式有哪些?
- Iterator接口 —— java集合中一员
- 包:
java.util.Iterator - 功能:主要用于迭代访问(遍历)
Collection中的元素(Iterator对象称为迭代器) - 方法:
public E next():返回迭代的下一个元素。public boolean hasNext():如果仍有元素可以迭代,则返回true
Iterator<String> it = list.iterator(); while(it.hasNext()){//判断是否有迭代元素 System.out.println(it.next());//输出迭代出的元素 } - 包:
- 普通for循环遍历(类似于数组遍历)
- 增强for
- 增强
for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。 - 它的内部原理其实是个
Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
for(String a : list){ System.out.println(a); } - 增强
- List集合自有迭代器
ListIterator<String> listli = list.listIterator(); while(listli.hasNext()){ System.out.println(listli.next()); } - Lambda
Lambda list.forEach( item -> System.out.println(item) );
13、ArrayList内存分配在什么位置?
(堆内存)
ArrayList是Java中的一个动态数组,内存分配通常是在堆内存中进行的。堆内存是一种由Java虚拟机(JVM)管理的内存池,用于存储动态分配的对象。当创建一个ArrayList对象时,JVM会在堆内存中分配一块连续的内存空间来存储数组元素。当ArrayList的元素数量增加时,JVM会自动扩展堆内存中的内存空间,以容纳更多的元素。因此,ArrayList能够动态地增长和收缩,使其在实现动态数据结构时非常有用。
堆空间结构:堆是一种动态分配的内存空间,用于存储动态分配的对象。它的空间结构可以分为三个部分:新生代、老年代和永久代(在JDK8及之后版本中已经被元空间(Metaspace)所取代)。
14、场景:存储对象类型为person的对象,然后里面有name属性,需求是按照name查找,用哪个存好?
如果你需要按照name属性进行查找数据,那么建议使用HashMap或TreeMap等基于哈希表或红黑树实现的数据结构来存储对象。这些数据结构会更有效地支持按照指定 key 进行快速查找元素。
当然,如果强制要求在ArrayList或LinkedList存储对象,并且需要按照name属性进行查找,那么建议使用ArrayList。因为ArrayList底层使用数组实现,可以更快地检索指定位置的元素。如果使用LinkedList,需要从头逐个遍历元素,直到找到指定元素,效率会相对较低。 另外,无论是使用ArrayList还是LinkedList,都可以自定义比较器(Comparator)来对对象排序,使得按照name属性进行排序后数据更快查找。在构造比较器时,只需要实现Comparator接口,根据name属性实现自定义的比较函数,然后调用集合的sort方法即可。 综上所述,如果需要按照name属性进行快速查找,请考虑使用Map类型的集合,并定义一个 name 到对象的映射关系。如果强制要求在ArrayList或LinkedList存储对象,那么建议使用ArrayList,并实现一个比较器来支持按照 name 属性排序。
优化方法——使用HashMap/TreeMap:可以考虑使用HashMap/TreeMap来存储person对象,以name作为key,person对象作为value。这样可以在常数时间内快速查找到指定name的person对象,从而提高查找效率。
15、stream遍历ArrayList
list.stream().forEach(System.out::println);
16、怎么提高ArrayList的增删效率?
要提高ArrayList的增删效率,可以考虑以下几点:
- 使用初始容量:在创建ArrayList时,可以使用预估的初始容量,通过ArrayList的构造函数指定。如果事先知道ArrayList中将要存储的元素数量,设置合适的初始容量可以避免频繁的扩容操作,提高效率。
- 批量操作:在进行大量的元素增删操作时,可以考虑使用ArrayList的批量操作方法,如addAll()和removeAll()。这些方法可以减少数组的拷贝和元素移动操作,提高效率。
- 使用LinkedList:如果对于频繁的插入和删除操作,可以考虑使用LinkedList而不是ArrayList。LinkedList在插入和删除元素时不需要进行数组的扩容和移动,因为它使用链表结构存储元素。
- 删除元素时考虑使用iterator:使用迭代器进行元素的删除操作比直接使用索引要高效。迭代器删除元素时不需要进行数组的拷贝和移动操作,因此效率更高。
- 使用subList()方法避免冗余拷贝:如果需要删除ArrayList中的一段连续元素,可以通过使用subList()方法获取子列表,然后调用clear()方法一次性删除子列表中的元素。这样可以避免不必要的数组拷贝和移动操作,提高效率。
- 考虑使用其他数据结构:根据具体需求,有时可以使用其他更适合的数据结构来实现特定的功能。例如,如果需要对元素进行高效的查找操作,可以选择使用HashSet或TreeSet等数据结构。
总的来说,要提高ArrayList的增删效率,关键是减少数组的拷贝和移动操作。通过合理设置初始容量、使用批量操作、选择合适的数据结构等方式,可以有效地提升ArrayList的性能。
17、如何使ArrayList变得线程安全
要使ArrayList变得线程安全,可以使用Java中的同步机制。
-
使用synchronized关键字来同步方法。例如,可以将ArrayList的add()方法、get()方法等需要保持线程安全的方法使用synchronized关键字进行同步,以确保多个线程不会同时访问该方法。
-
使用Collections.synchronizedList()方法来将ArrayList转换为一个线程安全的List。这个方法返回一个线程安全的List,它通过在每个方法上添加synchronized关键字来保证线程安全。但是,要注意的是,迭代器的操作不是线程安全的,因此在使用迭代器时需要进行额外的同步措施。
-
使用线程安全的数据结构来替代ArrayList,例如Vector和CopyOnWriteArrayList等。Vector也是基于数组实现的动态数组,它与ArrayList类似,但是所有的方法都是同步的,因此可以保证线程安全。CopyOnWriteArrayList是基于复制-写模式的线程安全类,它保证读操作的线程安全性,但是在写操作时会将整个List复制一份进行修改,因此写操作的开销较大。
综上所述,要使ArrayList变得线程安全,可以使用synchronized关键字、Collections.synchronizedList()方法、线程安全的数据结构等方式来实现。具体选择哪种方式需要根据具体场景和性能要求来决定。
Map
1、HashMap与HashTable/TreeMap/HashSet的区别
2、HashMap、HashTable和ConcurrentHashMap三者的区别?
- 线程安全: Hashtable 和 ConcurrentHashMap 是线程安全的集合类,而 HashMap 则是非线程安全的集合类。(凑时间可说:“因此,...”)
- 效率: 由于 Hashtable 和 ConcurrentHashMap 都具备线程安全的特性,这意味着它们的性能相对于 HashMap 要略低。但是,由于 ConcurrentHashMap 可以实现更高效的并发,因此在高并发场景下,它也相比 Hashtable 有更高的性能。
- Null值: HashMap和ConcurrentHashMap都允许一个null键和多个null值,而HashTable不允许。
- 迭代器(iterator): ConcurrentHashMap 的迭代器支持弱一致性,允许允许在操作期间进行修改,而 Hashtable 的迭代器是强一致性的,禁止修改。
- 存储方式: Hashtable 和 ConcurrentHashMap 对键值对进行存储时,都是采用分离链接法(Separate Chaining)来处理哈希冲突。而 HashMap 则采用开放地址法(Open Addressing)的方式来处理冲突,这种方式会更加耗费内存,但是可以减少链表的使用,从而使遍历更为高效。
- HashMap和ConcurrentHashMap都采用的是数组+链表/红黑树的数据结构,底层原理基本相同。而HashTable则采用了数组+链表的数据结构。Hash表的原理,就是根据Key计算它的HashCode值,再通过对数组长度取模得到该键值对在数组中的存储位置。
3、LinkedHashMap/LinkedList如何保证节点顺序?
LinkedHashMap:
*** 是一种有序的Map,它通过维护一个双向链表来*** 。在插入一个新节点时,LinkedHashMap会将该节点插入到链表的尾部,而在访问一个已经存在的节点时,LinkedHashMap会将该节点移动到链表的尾部,从而保证链表中节点的顺序与插入顺序或访问顺序一致。
因此,当遍历LinkedHashMap时,它会按照插入顺序或访问顺序返回元素,而不是按照键的哈希值或自然顺序返回元素。这种特性使得LinkedHashMap非常适合需要保证元素顺序的场景,比如LRU缓存、有序映射等。
LinkedList:
*** 是一种双向链表,它通过维护一个链表结构来*** 。每个节点都包含一个指向前驱节点的引用和一个指向后继节点的引用,这样就可以在链表中按照顺序访问节点。
在插入一个新节点时,LinkedList会将该节点插入到链表的尾部,而在访问一个已经存在的节点时,LinkedList会将该节点移动到链表的尾部,从而保证链表中节点的顺序与插入顺序或访问顺序一致。
因此,当遍历LinkedList时,它会按照插入顺序或访问顺序返回元素,而不是按照键的哈希值或自然顺序返回元素。这种特性使得LinkedList非常适合需要保证元素顺序的场景,比如栈、队列等。
4、HashMap的底层实现、扩容机制?
5、线程安全的Map有哪些?
线程安全的Map有:HashTable、SynchronizedMap、ConcurrentHashMap(推荐)
线程不安全的Map有:HashMap、TreeMap
6、HashMap是否线程安全?
HashMap是线程不安全的,其主要体现:
- 在jdk1.7中,在多线程环境下,扩容时会造成 环形链 或 数据丢失。
- 在jdk1.8中,在多线程环境下,会发生 数据覆盖 的情况。
7、HashMap线程不安全。是哪一步不安全?怎么解决不安全呢?
- 扩容操作:在HashMap中,当元素数量达到一定阈值时,需要对数组进行扩容操作。如果多个线程同时进行扩容,可能会导致数据覆盖或者元素丢失的问题。
- put操作:多个线程对同一个HashMap进行put操作时,可能会导致元素覆盖或者增加操作失败的问题。
- resize操作:HashMap中采用链表和红黑树的数据结构保存元素,当桶内元素数量超过一定阈值时,会将链表转成红黑树,或者反过来,这个过程就是resize操作。如果多个线程同时进行resize操作,可能会导致链表/红黑树结构出现问题。
为了解决HashMap的线程安全问题,可以采取以下几种方式:
- 使用ConcurrentHashMap:ConcurrentHashMap是Java中线程安全的HashMap实现类,它通过分段加锁的方式来保证线程安全性。
- 使用Collections.synchronizedMap():可以将HashMap转换为线程安全的Map,它会使用一个对象锁来保证对Map的操作是线程安全的。
- 使用线程安全的Map实现:例如Hashtable,虽然性能较差且容易出现死锁,但也可作为一种简单的线程安全解决方案。
- 使用锁(Lock):可以通过在多线程访问HashMap时使用锁来保证线程安全性。这种方式需要手动添加锁和解锁操作,相对较麻烦,但可以控制细粒度锁的范围以提高性能。
- 使用volatile关键字:如果只有一个线程写入HashMap,而其他线程只是读取HashMap,那么可以使用volatile关键字来实现线程安全。volatile关键字可以保证变量的可见性,从而保证其他线程可以读取到最新的HashMap值。
使用HashTable或者Collections.synchronizedMap()时,有个共同的问题:那就是性能问题。无论读操作还是写操作,它们都会给整个集合进行加锁,导致同一时间内其他的操作进入阻塞状态。
8、HashMap有几种遍历方法?推荐使用哪种?(map遍历也是类似)
- JDK8之前主要使用 EntrySet 和 KeySet 进行遍历。
- 在JDK8之后主要包含3种遍历方法:使用 Lambda 遍历、使用 Stream 单线程遍历、使用 Stream 多线程遍历
-
EntrySet 是早期hashmap遍历的主要方法:
HashMap<String, String> map = new HashMap() {{ put("Java", " Java Value."); ... }}; //循环遍历 for (Map.Entry<String, String> entry : map.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); } -
KeySet 的遍历方式是循环 Key 内容,再通过 map.get(key) 获取 Value 的值,具体实现如下:
... for (String key : map.keySet()) { //循环遍历 System.out.println(key + ":" + map.get(key)); }使用 KeySet 遍历,性能不如 EntrySet,因为 KeySet 其实循环了两遍集合,第一遍循环是循环 Key,而获取 Value 有需要使用 map.get(key),相当于又循环了一遍集合,所以 KeySet 循环不能建议使用,因为循环了两次,效率比较低。EntrySet 和 KeySet 除了以上直接循环外,我们还可以使用它们的迭代器进行循环。详见:面试官:HashMap有几种遍历方法?推荐使用哪种?
-
Lambda表达式的遍历:
map.forEach((key, value) -> { System.out.println(key + ":" + value); }); -
Stream单线程遍历(Stream 遍历是先得到 map 集合的 EntrySet,然后再执行 forEach 循环)
map.entrySet().stream().forEach((entry) -> { System.out.println(entry.getKey() + ":" + entry.getValue()); }); -
Stream多线程遍历(Stream 多线程的遍历方式和上一种遍历方式类似,只是多执行了一个 parallel 并发执行的方法,此方法会根据当前的硬件配置生成对应的线程数,然后再进行遍历操作)
map.entrySet().stream().parallel().forEach((entry) -> { System.out.println(entry.getKey() + ":" + entry.getValue()); });
JDK 8 之后的开发环境,推荐使用 Stream 的遍历方式,因为它足够简洁;而如果在遍历的过程中需要动态的删除元素,那么推荐使用迭代器的遍历方式;如果在遍历的时候,比较在意程序的执行效率,那么推荐使用 Stream 多线程遍历的方式,因为它足够快。
9、ConcurrentHashMap
9.1 JDK1.7 和 JDK1.8 的 ConcurrentHashMap 实现有什么不同?
- 线程安全实现方式 :
- JDK1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。
- JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
- Hash碰撞解决方法 :
- JDK1.7 采用拉链法
- JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
- 并发度 :
- JDK 1.7 最大并发度是 Segment 的个数,默认是 16。
- JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
9.2 ConcurrentHashMap是如何保证线程安全的?
1、ConcurrentHashMap在JDK 1.7中使用的数组 + 链表的结构,其中数组分为两类,大数组Segment 和 小数组 HashEntry,而加锁是通过给Segment添加ReentrantLock重入锁来保证线程安全的。
2、ConcurrentHashMap在JDK1.8中使用的是数组 + (单向)链表 + 红黑树的方式实现,它是通过 CAS 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。
(JDK1.8中)当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。ConcurrentHashMap采用链式寻址法来解决hash冲突。当hash冲突比较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(n)。
因此在JDK1.8中,引入了红黑树的机制。当数组长度大于64并且链表长度大于等于8的时候,单向链表就会转换为红黑树。另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于8,红黑树会退化成单向链表。
9.3 ConcurrentHashMap 和 Hashtable 哪个效率更高?
ConcurrentHashMap 效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线程安全。
而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采用 CAS+Synchronized 实现线程安全。
9.4 HashTable和CurrentHashMap在并发处理上的区别?
- 锁机制不同: HashTable使用synchronized关键字来保证线程安全,每次操作都要锁住整张表,这样在高并发环境下,线程竞争锁的速度变慢,从而导致锁等待的时间变长,性能则逐渐下降。ConcurrentHashMap则采用了分段锁技术,将整个哈希表分成众多小的段,每个段都由一把独立的锁来控制,多个线程可以同步访问不同的段,这样可以显著提高并发性,减少等待时间,从而提高整体性能。
- 数据一致性: 当多个线程对HashTable进行增删改操作时,需要首先获得锁才能进行修改,而ConcurrentHashMap则采用了多版本并发控制MVCC来保证数据一致性。它允许多个线程同时操作一个数据,每个线程访问到的数据是版本号最高的,这样当其他线程修改该数据时,当前线程不会受到影响,也不需要加锁等待,从而实现了高效的并发操作。
- 迭代器(Iterator): 在使用迭代器对HashTable进行遍历时,需要对整个哈希表进行锁定,这样就不能同时进行读和写操作,因此,如果读写并发性很高的情况下,就会导致性能下降。而ConcurrentHashMap则提供了一种Snapshot Iterator的迭代器,它可以返回一个数据的快照版本,使得即使其他线程对该数据发生改变,当前迭代器也不受影响,这样可以保证高效且安全的并发遍历。
ConcurrentHashMap因为采用了更为有效的并发控制技术,能够更好地支持高并发环境,保证数据一致性,提高性能,因此通常在多线程并发处理的环境中使用较多。
9.5 ConcurrentHashMap在性能方面做的优化
(1)在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。
(2)引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。
(3)当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。
(4)ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点:
- 当线程竞争不激烈时,直接采用CAS来实现元素个数的原子递增。
- 如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。
Set
1、HashSet、LinkedHashSet 和 TreeSet 三者的异同
- 底层数据结构:HashSet采用哈希表实现,LinkedHashSet采用哈希表和双向链表实现,TreeSet采用红黑树实现。
- 元素顺序:HashSet不保证元素的顺序,是无序的;LinkedHashSet可以保证元素插入的顺序;而TreeSet按照元素的自然顺序或自定义顺序排序。
- 效率:HashSet比较高效,由于采用哈希表实现,无论是插入、查找、删除元素的时间复杂度都是O(1),但是不保证元素顺序;LinkedHashSet和HashSet具有相同的查询和插入时间复杂度,但是插入性能略低于HashSet;TreeSet插入、查找和删除操作的时间复杂度都是O(logN)。
- 元素唯一性:HashSet和LinkedHashSet不允许重复元素;TreeSet中元素是唯一的,且已经实现了排序,不需要额外的排序操作。
需保证元素的唯一且有序,可以选择TreeSet;需保证元素唯一而不考虑顺序,可以选择HashSet;需保证元素唯一且需要了解元素的插入顺序,可以选择LinkedHashSet。
2、HashSet与HashMap的区别?
HashMap和HashSet都是基于哈希表的,它们的主要区别有:
可以把HashMap想象成一个映射表,它存储的是键值对,而HashSet是一个集合,它存储的是单个的值
- 存储方式:HashMap是一个映射表,它存储的是键值对,而HashSet是一个集合,它存储的是单个的值。
- 元素的唯一性:HashMap中的key是唯一的,而value可以重复;而HashSet中的元素是唯一的,不能重复。
- 对null的支持:HashMap可以存储一个null键和多个null值,而HashSet最多能存一个null值。
- 底层数据结构:HashMap是基于哈希表实现的,而HashSet是基于HashMap实现的,底层也是使用哈希表。
- 使用场景:HashMap适用于需要通过键值对来查找元素的场景,而HashSet适用于需要存储唯一元素的场景。
3、如何遍历Set?
- 使用for-each循环遍历:
Set set = new HashSet<>();
set.add("apple");
set.add("orange");
set.add("banana");
for(String str : set){
System.out.println(str);
}
- 使用Iterator遍历:
Set set = new HashSet<>();
set.add("apple");
set.add("orange");
set.add("banana");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
其中,使用Iterator遍历的好处是可以在遍历元素时同时进行删除操作,而使用for-each循环则不支持在遍历时进行元素的删除操作。
4、如何判断一个元素是否在Set中存在?
可以使用contains方法。
5、HashSet如何判断两个对象是否相等?
HashSet使用equals方法和hashCode方法来判断两个对象是否相等。 通常需要满足以下两个条件:
- 如果两个对象通过equals比较相等,那么它们返回的hashCode值也必须相等;
- 如果两个对象的hashCode值不相等,那么它们返回的equals比较结果也必须为false。
6、HashSet如何保证元素不重复?(如何去重)
我们只要了解了 HashSet 执行添加元素的流程,就能知道为什么 HashSet 能保证元素不重复了。HashSet 添加元素的执行流程是:
- 当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较
- 如果没有相符的 hashcode,HashSet 会假设对象没有重复出现,会将对象插入到相应的位置中。
- 但是如果发现有相同 hashcode 值的对象,这时会调用对象的 equals() 方法来检查对象是否真的相同,如果相同,则 HashSet 就不会让重复的对象加入到 HashSet 中,这样就保证了元素的不重复。
HashSet是基于哈希表实现的,它主要依赖于哈希码来判断元素是否重复。具体来说,HashSet会将每个元素的哈希码与当前已有的元素进行比较,如果哈希码相同,则再调用equals方法进行比较。如果两个元素的哈希码和equals判断结果都相等,则判断这两个元素相同,后添加的元素将不会被添加到集合中。
在HashSet中,hashCode()和equals()很重要,因为HashSet是通过hashCode()和equals()判断重复元素的。因此,在对自定义的对象进行HashSet去重时,需要重写它们的hashCode()和equals()方法,并保证它们在逻辑意义上相等的对象返回相同的hashCode()值和equals()返回true。
需要注意的是,在对HashSet进行去重时,如果元素的哈希码过于接近,可能会导致哈希冲突,从而影响HashSet的性能。为了避免哈希冲突,在选择哈希函数和哈希表大小时需要考虑元素的特征和数量。
更多内容详见:面试官:HashSet如何保证元素不重复?
7、HashSet 的 value 为什么用 Object 而不用 null,效率更高而且还省得创建对象了?
在Java中,HashSet是一种常用的集合实现,它用于存储不重复的元素,每个元素都可以为null。
在HashSet中,value是Object类型,而不是null,这是因为HashSet要依赖于哈希码来判断元素是否重复。如果value为null,无法得出它的哈希码,从而无法进行比较和判断。而使用Object类型,则可以通过hashCode()和equals()方法来判断元素是否相等,从而进行去重。
同时,使用Object类型可以避免创建额外的null对象,从而提高了程序的效率和性能。在创建对象时,可能会导致额外的内存开销和垃圾回收等问题,对系统的性能和稳定性都有影响。
值得注意的是,在对自定义的对象进行HashSet去重时,需要重写它们的hashCode()和equals()方法,并保证它们在逻辑意义上相等的对象返回相同的hashCode()值和equals()返回true。这样才能保证HashSet正确去重,并避免因哈希冲突导致的性能问题。
8、TreeSet如何保证元素有序?
TreeSet使用红黑树的数据结构来保证元素的有序性。
在Java中,TreeSet是一种基于红黑树的集合实现,它可以对元素进行自然排序或指定比较器排序。在TreeSet中,元素的排序是通过比较元素的大小来实现的。具体来说,TreeSet中的每个元素都必须实现Comparable接口或者使用指定的Comparator来进行比较。
在添加元素时,TreeSet会根据元素的大小将元素插入到对应的位置,以保持元素的有序性。在遍历集合时,会按照元素的自然顺序或指定的比较器顺序进行顺序访问。
由于红黑树是一种平衡二叉树,相比于普通的二叉树,它可以在O(log n)的时间内完成查找、插入、删除等操作,从而保证了TreeSet元素的有序性。
9、HashSet底层原理
HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
- 先获取元素的哈希值(hashcode方法)
- 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
- 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
- Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树)
分析HashSet的扩容和转成红黑树机制
- HashSet底层是HashMap,第一次添加时,table的数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor是0.75)=12
- 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
- Java8以后,如果一条链表中的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行数化(红黑树),否则仍然采用数组扩容机制
10、自己定义对象放到set里会去重吗?
自己定义的对象放到Set里不会去重。
要使用Set集合对自定义类对象进行去重,需要重写该自定义类对象的hashCode和equals方法;否则,Set集合无法实现去重。