List、Set、Map等常用集合类的特点和用法。
常用集合类(List、Set、Map 等)是 Java 中提供的数据结构,用于存储和操作一组数据。以下是它们的特点和用法:
- List(列表):
- 特点:有序集合,允许重复元素。
- 用法:常用的实现类有 ArrayList 和 LinkedList。可通过索引访问元素,支持添加、删除和修改操作。适用于需要维护元素顺序且可能包含重复元素的场景。
ArrayList:
- 特点:基于数组实现的动态数组,支持快速随机访问。
- 用法:适用于频繁访问和更新元素的场景,但在插入和删除元素时效率较低。
LinkedList:
- 特点:基于链表实现的双向链表,支持快速插入和删除操作。
- 用法:适用于频繁插入和删除元素的场景,但在随机访问元素时效率较低。
- Set(集合):
- 特点:无序集合,不允许重复元素。
- 用法:常用的实现类有 HashSet 和 TreeSet。不保持元素插入顺序,不允许重复元素的存在。适用于需要去重的场景。
HashSet:
- 特点:基于哈希表实现的无序集合,不允许重复元素。
- 用法:适用于需要快速查找和去重的场景,但不保持元素的插入顺序。
TreeSet:
- 特点:基于红黑树实现的有序集合,不允许重复元素。
- 用法:适用于需要按照自然排序或自定义排序方式对元素进行排序的场景。
- Map(映射):
- 特点:键值对的集合,键和值可以都是任意对象,键不允许重复。
- 用法:常用的实现类有 HashMap 和 TreeMap。通过键来获取值,键是唯一的,适用于需要根据键查找值的场景。
HashMap:
- 特点:基于哈希表实现的键值对集合,键和值可以为 null。
- 用法:适用于快速查找和存储键值对的场景,根据键来获取值,不保持插入顺序。
TreeMap:
- 特点:基于红黑树实现的有序映射,键不允许为 null。
- 用法:适用于需要按照自然排序或自定义排序方式对键值对进行排序的场景。
1.List、Set 和 Map 之间有什么区别?它们的常用实现类有哪些?
List、Set和Map是Java集合框架中的三个核心接口,它们之间的区别如下:
List:
list是有序可重复的集合接口,可以按照顺序或者指定的顺序访问和操作元素,还可以通过索引访问元素
常用实现类:ArrayList、LinkedList
Set:
set是无序不重复的集合接口,不能通过索引访问元素。
常用实现类:HashSet、TreeSet
Map:
map是键值对映射的集合接口,每个键只能对应一个值。键值均可以为空
常用实现类:HashMap、ConcurrentHashMap
2.ArrayList 和 LinkedList 的区别是什么?它们适用于不同的场景吗?
ArrayList:底层是数组,查询快增删慢,可以通过索引随机访问,时间复杂度是O(1),
LinkedList: 底层是链表,查询慢增删块,不能使用索引访问,需要从头结点或者尾结点开始遍历,时间复杂度为O(n)
ArrayList适用于需要快速随机访问元素的场景,例如需要频繁地根据索引读取或修改元素的情况。
LinkedList适用于需要频繁进行插入和删除操作的场景,特别是在中间或开头进行插入和删除操作比较多的情况。
对于大部分常见的情况,ArrayList的性能要优于LinkedList,因为数组的访问速度更快。
但在某些特定的场景下,LinkedList可能会更适合,例如需要频繁进行插入和删除操作,并且对于随机访问的性能要求较低的情况。
3.HashSet 和 TreeSet 的区别是什么?它们如何保证元素的唯一性?
HashSet的底层是hash表且是无序不重复的集合接口
TreeSet的底层是红黑树,是有序不重复的集合接口
为了保证元素的唯一性,HashSet和TreeSet在判断元素是否重复时,依赖于元素的equals方法(和哈希码)
在选择HashSet和TreeSet时,需要根据具体的需求进行选择:
如果只关心元素的唯一性,而不关心元素的顺序,可以选择HashSet,它的插入、删除和查找操作的性能较好。
如果需要对元素进行排序,可以选择TreeSet,它会根据元素的顺序进行存储,但由于需要维护红黑树的平衡性,插入、删除和查找操作的性能稍低于HashSet。
总结:HashSet和TreeSet都可以保证元素的唯一性,HashSet适用于无序需求,而TreeSet适用于有序需求。
4.HashMap 和 HashTable 的区别是什么?它们的线程安全性如何?
HashMap是非线程安全的,存储的键和值都可以为null
而HashTable是线程安全的,存储的键和值都不可以为null
HashMap的性能通常比HashTable更好。
5.ConcurrentHashMap 是如何实现线程安全的?它与 HashMap 的区别是什么?
实现线程安全的几个关键点:
ConcurrentHashMap内部使用了分段式的锁,将整个数据结构分成一些独立的部分,并称为“段”并对不同的段进行了锁的力度的控制。还使用了volatile关键字来确保可见性,确保当一个线程修改了某个段的内容后,其他线程可以立即看到修改的结果。还使用并发安全的数据结构(hashEntry数组和链表)与线程安全的迭代器
与HashMap相比,ConcurrentHashMap的区别如下
:
ConcurrentHashMap是线程安全的,可以在多线程环境下进行并发操作,而HashMap是非线程安全的。在并发环境下,ConcurrentHashMap的性能通常比HashMap好,因为它通过分段锁的机制允许多个线程同时进行读操作,提高了并发性能。
线程安全的集合
1.常见的集合中的线程安全集合
List
1.Vector
原理是为其所有需要线程安全的方法都添加了synchronize关键字,锁住了整个对象(使用的互斥锁)
2.CopyOnWriteArrayList
在多线程中,读操作跟普通的ArrayList没有区别,写操作会上锁,上锁后将数据复制一份,再将数据写入,避免数据覆盖而造成的数据问题(使用读写锁)
Set
1.CopyOnWriteArraySet
底层使用的是CopyOnWriteArrayList
Queue
1.ConcurrentListedQueue
线程安全,读写效率高的队列,高并发情况下性能最好
其使用CAS比较交换算法来实现线程安全,其添加对象时涉及三个核心参数(V,E,N)
V:当前需要更新的变量,E:预期值,N:新值
只有当V=E时,才会将V=N,否则表示已经被别的线程更新,取消当前操作
2.BlockingQueue
https://blog.csdn.net/sjemYele/article/details/121004818ArrayBlockingQueue
LinkedBlockingQueue
2.Map中线程安全的
concurrentHashMap
是一个支持高并发更新与查询的哈希表(基于HashMap)。
在保证安全的前提下,进行检索不需要锁定。
HashTable
原理是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象(使用的锁是互斥锁)
集合的遍历、排序、查找等操作。
遍历
1.如何遍历集合框架中的元素?有哪些遍历方式?
1.使用迭代器(Iterator):通过调用集合的iterator()方法获取迭代器对象,然后使用while循环和next()方法逐个访问元素,直到遍历完所有元素。
List<String> list = new ArrayList<>();
// 添加元素到列表中
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
// 处理元素
}
2.使用增强型for循环(foreach):适用于数组和实现了Iterable接口的集合类,可以直接通过for循环遍历元素,不需要显式使用迭代器。
List<String> list = new ArrayList<>();
// 添加元素到列表中
for (String element : list) {
// 处理元素
}
3.**使用Lambda表达式和Stream API:**从Java 8开始,引入了Lambda表达式和Stream API,可以通过Stream的forEach()方法对集合进行遍历,并结合Lambda表达式进行元素处理。
List<String> list = new ArrayList<>();
// 添加元素到列表中
list.stream().forEach(element -> {
// 处理元素
});
4.使用普通的for循环:适用于数组和实现了RandomAccess接口的集合类,通过下标访问元素进行遍历。
List<String> list = new ArrayList<>();
// 添加元素到列表中
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
// 处理元素
}
排序、查找
集合类提供了丰富的排序、查找和其他操作方法,以下是一些常见的操作:
排序操作:
- List:使用
Collections.sort(list)
方法对列表进行自然排序,或者使用实现了Comparable
接口的自定义对象进行排序。也可以使用Collections.sort(list, comparator)
方法根据自定义比较器进行排序。 - TreeSet:元素会自动按照自然顺序或者自定义比较器的顺序进行排序。
查找操作:
- List:可以使用
list.indexOf(element)
方法查找元素在列表中的索引,或者使用list.contains(element)
方法判断元素是否存在。 - Set:可以使用
set.contains(element)
方法判断元素是否存在。
其他操作:
- 添加元素:使用
add(element)
方法将元素添加到集合中。 - 删除元素:使用
remove(element)
方法从集合中删除指定元素。 - 遍历元素:可以使用迭代器或增强型循环来遍历集合中的元素。
- 获取集合大小:使用
size()
方法获取集合中元素的个数。
对于需要进行自定义排序或比较的场景,可以实现 Comparable
接口或创建自定义比较器(实现 Comparator
接口)来指定排序规则。
示例:
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(3);
list.add(8);
Collections.sort(list); // 对列表进行自然排序
System.out.println(list); // 输出:[3, 5, 8]
List<String> strings = new ArrayList<>();
strings.add("banana");
strings.add("apple");
strings.add("cherry");
Collections.sort(strings, Collections.reverseOrder()); // 对列表进行逆序排序
System.out.println(strings); // 输出:[cherry, banana, apple]
Set<String> set = new TreeSet<>();
set.add("banana");
set.add("apple");
set.add("cherry");
System.out.println(set); // 输出:[apple, banana, cherry]
System.out.println(set.contains("apple")); // 输出:true
System.out.println(set.contains("orange")); // 输出:false
这些操作可以根据具体需求选择适合的集合类和方法,灵活地进行数据操作和处理。
ArrayList做初始化和扩容的时候是什么样的过程
当创建一个 ArrayList 实例时,会分配一定大小的内部数组作为存储容器。默认情况下,ArrayList 的初始容量为 10,但也可以通过指定初始容量的构造函数来自定义容量大小。当向 ArrayList 添加元素时,会首先检查当前元素个数是否已达到内部数组的容量上限。如果已达到容量上限,需要进行扩容操作。如果需要扩容时,ArrayList 会创建一个新的内部数组,并将原有的元素拷贝到新数组中,采用的容量增长方式为:新容器 = 原容量+原容量/2;在扩容过程中,ArrayList 会使用 System.arraycopy() 方法将原有数组中的元素复制到新的数组中。在扩容后,ArrayList 会更新内部的引用,指向新的内部数组。这样,之前的旧数组将被垃圾回收,释放内存空间。
Hashmap的get和put方法的实现?
HashMap 是基于哈希表实现的键值对存储结构,其中的 get() 和 put() 方法用于获取和插入键值对。下面是它们的简要实现过程:
-
get() 方法的实现:
- 首先,根据传入的键对象使用哈希函数计算出哈希值。
- 哈希值用于确定键值对在内部数组中的索引位置。
- 根据索引位置,在数组中找到对应的存储桶(bucket)。
- 若存储桶为空,返回 null,表示没有找到对应的键值对。
- 若存储桶不为空,则遍历存储桶中的链表或红黑树(Java 8+),查找与传入键对象相等的键值对。
- 若找到匹配的键值对,则返回对应的值;否则返回 null。
-
put() 方法的实现:
- 首先,根据传入的键对象使用哈希函数计算出哈希值。
- 哈希值用于确定键值对在内部数组中的索引位置。
- 根据索引位置,在数组中找到对应的存储桶。
- 若存储桶为空,直接将键值对插入存储桶中,并增加 HashMap 的大小计数器。
- 若存储桶不为空,则遍历存储桶中的链表或红黑树,检查键对象是否已存在。
- 若存在相同的键对象,则更新对应的值。
- 若不存在相同的键对象,则将键值对添加到链表或红黑树中。
- 如果链表或红黑树的长度超过了阈值(8),则将链表转换为红黑树,提高查找效率。
- 若 HashMap 的大小达到了容量的阈值(负载因子 * 数组长度),则进行扩容操作,重新计算哈希值和存储位置。
需要注意的是,HashMap 采用数组+链表(或红黑树)的方式来解决哈希冲突,即不同的键对象可能会计算得到相同的哈希值。因此,在查找和插入过程中,需要遍历链表或红黑树来确保准确的键值对匹配。Java 8 引入了红黑树的优化,使得在一些场景下查找效率更高。
以上是对 HashMap 的 get() 和 put() 方法的简要实现过程的描述,实际的实现可能会有更多的细节和优化。