文章目录
在java中,List和Set是集合框架中两个核心接口,都位于java.util包下,用于存储和操作数据集合。
List
特点
List是一个有序集合,它按照元素添加的顺序来存储元素,也就是说,你可以通过索引来访问元素,就像数组一样。
List允许存储重复的元素,即可以将同一个对象添加到List中多次
实现类
ArrayList
-
底层实现
基于动态数组实现。它有一个数组来存储元素,当数组空间不足时,会创建一个更大的数组,并将原数组中的元素复制到新数组中
自动扩容机制:默认容量为10,扩容因子为0.75,容量不足是按1.5倍增长
-
性能特点
- 随机访问速度快
通过索引,时间复杂度为O(1)
- 插入/删除元素效率低
在数组的末尾添加元素速度很快,时间复杂度是O(1)
如果在数组中间插入或删除,需要移动后续的元素来填补空位,时间复杂度为O(N)
- 随机访问速度快
-
使用场景
频繁查询,较少增删的场景
-
原理
-
ArrayList常用方法
// 创建一个空的 ArrayList,初始容量默认为 10 ArrayList<String> list = new ArrayList<>(); //创建一个具有指定初始容量的空 ArrayList ArrayList<String> list2 = new ArrayList<>(20); //创建一个包含指定集合元素的 ArrayList ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); //在列表末尾添加一个元素 list.add("D"); //在指定索引位置插入一个元素 list.add(1, "X"); //将指定集合中的所有元素添加到列表末尾 list.addAll(Arrays.asList("E", "F")); //将指定集合中的所有元素插入到指定索引位置 list.addAll(2, Arrays.asList("Y", "Z")); //获取指定索引位置的元素 String element = list.get(1); //返回指定元素在列表中的第一个匹配项的索引,如果不存在则返回 -1 int index = list.indexOf("B"); //返回指定元素在列表中的最后一个匹配项的索引,如果不存在则返回 -1 int lastIndex = list.lastIndexOf("B"); //删除指定索引位置的元素 list.remove(1); //删除列表中第一个匹配的元素 list.remove("A"); //清空列表中的所有元素 list.clear(); //替换指定索引位置的元素 list.set(1, "Z"); //返回列表中的元素数量 int size = list.size(); //返回一个迭代器,用于遍历列表 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } //返回从 fromIndex 到 toIndex(不包含)的子列表 List<String> sublist = list.subList(1, 3); //检查列表中是否包含指定元素 boolean contains = list.contains("A"); //将列表转换为数组 Object[] array = list.toArray(); //将列表转换为指定类型的数组 String[] array = list.toArray(new String[0]); //检查列表是否为空 boolean isEmpty = list.isEmpty();
Vector
- 底层实现
基于动态数组:类似于 ArrayList。它使用一个 Object 类型的数组 elementData 来存储元素
自动扩容机制:当数组容量不足时,Vector 会自动扩容。默认情况下,扩容策略是将容量增加到当前容量的两倍 - 线程安全
所有公共方法(如 add、get 等)都使用了 synchronized 关键字,确保在多线程环境下对集合的操作是线程安全的
- 性能特点
- 线程安全带来的开销
Vector 在单线程环境下的性能不如 ArrayList。每次操作都需要获取锁,这会导致性能下降
- 随机访问高效
底层是数组,Vector 支持通过索引快速访问元素,时间复杂度为 O(1)
- 扩容性能问题
扩容操作涉及数组的复制,当元素数量较多时,扩容会导致较大的性能开销
- 线程安全带来的开销
- 适用场景
- 多线程环境
- 动态数组需求
- 读多写少的场景
- Vector 的使用逐渐减少(通常推荐使用 ArrayList 或 ConcurrentHashMap)
- Vector常用方法
//创建一个空的 Vector,初始容量为 10 Vector<String> vector = new Vector<>(); //创建一个具有指定初始容量的空 Vector Vector<String> vector1 = new Vector<>(20); //创建一个具有指定初始容量和扩容增量的空 Vector Vector<String> vector2 = new Vector<>(10, 5); //创建一个包含指定集合元素的 Vector Vector<String> vector3 = new Vector<>(Arrays.asList("A", "B", "C")); //在 Vector 的末尾添加一个元素 vector.add("D"); //在指定索引位置插入一个元素 vector.add(1, "X"); //将指定集合中的所有元素添加到 Vector 的末尾 vector.addAll(Arrays.asList("E", "F")); //将指定集合中的所有元素插入到指定索引位置 vector.addAll(2, Arrays.asList("Y", "Z")); //获取指定索引位置的元素 String element = vector.get(1); //返回指定元素在 Vector 中的第一个匹配项的索引,如果不存在则返回 -1 int index = vector.indexOf("B"); //返回指定元素在 Vector 中的最后一个匹配项的索引,如果不存在则返回 -1 int lastIndex = vector.lastIndexOf("B"); //删除指定索引位置的元素 vector.remove(1); //删除 Vector 中第一个匹配的元素 vector.remove("A"); //清空 Vector 中的所有元素 vector.clear(); //替换指定索引位置的元素 vector.set(1, "Z"); //返回 Vector 中的元素数量 int size = vector.size(); //返回 Vector 的当前容量 int capacity = vector.capacity(); //确保 Vector 的容量至少为指定值 vector.ensureCapacity(30); //将 Vector 的容量调整为当前元素数量 vector.trimToSize(); //返回一个迭代器,用于遍历 Vector Iterator<String> iterator = vector.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } //检查 Vector 中是否包含指定元素 boolean contains = vector.contains("A"); //将 Vector 转换为数组 Object[] array = vector.toArray(); //检查 Vector 是否为空 boolean isEmpty = vector.isEmpty(); //
LinkedList
- 底层实现
** 基于双向链表实现**。每个元素都是一个节点,节点包括数据部分和指向前后节点的指针
跳转可以查看,自己实现一个链表 →java基础 之 实现一个链表 - 性能特点
- 随机访问速度慢
通过索引访问元素速度较慢,时间复杂度为O(N)。
因为需要从头借点开始沿着列表逐个节点遍历 - 插入/删除元素效率高
如果已经知道要操作的节点位置,在链表的头部、尾部或中间插入和删除元素速度很快,时间复杂度是O(1)。
因为只需要更改相关借点的指针即可
- 随机访问速度慢
- 适用场景
适用频繁在中间插入和删除元素的场景
- LinkedList常用方法
LinkedList<String> list = new LinkedList<>(); list.add("A"); list.add("B"); // 在指定索引位置插入一个元素 list.add(1, "C"); // 在索引1的位置插入"C" //在链表的头部添加一个元素 list.addFirst("X"); //在链表的尾部添加一个元素(与 add(E e) 相同) list.addLast("Y"); //清空链表中的所有元素 list.clear(); //检查链表中是否包含指定元素 boolean contains = list.contains("A"); //获取指定索引位置的元素 String element = list.get(1); //返回指定元素在链表中的第一个匹配项的索引,如果不存在则返回 -1 int index = list.indexOf("B"); //返回指定元素在链表中的最后一个匹配项的索引,如果不存在则返回 -1 int lastIndex = list.lastIndexOf("B"); //删除指定索引位置的元素 list.remove(1); //删除链表中第一个匹配的元素 list.remove("A"); //删除链表的第一个元素 list.removeFirst() //删除链表的最后一个元素 list.removeLast(); //替换指定索引位置的元素 list.set(1, "Z"); //返回链表中的元素数量 int size = list.size(); //将元素添加到链表的末尾(作为队列使用) list.offer("A"); //将元素添加到链表的头部(作为双端队列使用) list.offerFirst("X"); //将元素添加到链表的尾部(与 offer(E e) 相同) list.offerLast("Y"); //获取并移除链表的第一个元素(作为队列使用) String first = list.poll(); //获取并移除链表的第一个元素(与 poll() 相同) String first = list.pollFirst(); //获取并移除链表的最后一个元素 String last = list.pollLast(); //获取链表的第一个元素,但不移除它(作为队列使用) String first = list.peek(); //获取链表的第一个元素,但不移除它(与 peek() 相同) String first = list.peekFirst(); //获取链表的最后一个元素,但不移除它 String last = list.peekLast(); //返回一个迭代器,用于遍历链表 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } //返回一个逆序迭代器,用于从尾到头遍历链表 Iterator<String> descendingIterator = list.descendingIterator(); while (descendingIterator.hasNext()) { String element = descendingIterator.next(); System.out.println(element); } //返回从 fromIndex 到 toIndex(不包含)的子列表 List<String> sublist = list.subList(1, 3);
ArrayList与Vector的区别
- 线程安全性
- Vector是线程安全的
其方法(如add、get、remove等)是同步的(synchronized),这意味着在多线程环境中,多个线程可以安全地并发访问和修改 Vector,而不会导致数据不一致。
- ArrayList不是线程安全的
ArrayList 的方法没有同步机制,因此在多线程环境中,如果多个线程同时对 ArrayList 进行写操作,可能会导致数据不一致
- Vector是线程安全的
- 性能
- Vector在多线程环境中可以安全使用,但同步机制会带来额外的性能开销。所以单线程环境中,Vector的性能通常比ArrayList差
- ArrayList没有同步机制,在单线程环境中性能更高,更适合在单线程或线程安全不是问题的场景中使用。
- 扩容机制
- Vector默认情况下每次扩容为原来的两倍。可以通过构造函数指定扩容增量。
- ArrayList的扩容机制是每次扩容为原来的1.5倍。ArrayList不支持在构造函数中指定扩容增量,扩容机制是固定的。
- 同步机制
- Vector内部使用synchronized方法来实现线程安全
- ArrayList如果需要在多线程环境中使用,可以通过Collections.synchronizedList
将ArrayList设置为线程安全
List<String> syncArrayList = Collections.synchronizedList(new ArrayList<>());
小结
优点
- List是有序的,可以方便的通过索引进行元素的访问和操作,对于需要按照特定顺序处理数据的场景非常有用,比如处理一个有序的任务列表
- 提供了多种实现方式,可以根据实际需求选择合适的实现。
如果需要频繁随机访问元素,可以选择 ArrayList;
如果需要频繁在中间插入和删除元素,可以选择 LinkedList。
缺点
- 可能存储大量重复元素
因为允许重复元素,会导致集合中存储了大量重复的数据,在某些需要唯一性数据的场景下是不合适的。
- ArrayList 的动态扩容机制可能导致性能问题
当 ArrayList 的容量不足时,需要进行扩容操作,这个过程涉及到创建新数组和复制旧数组元素,可能会消耗较多时间。虽然这种情况不经常发生,但在处理大量数据时仍需要注意。
ArrayList | LinkedList | Vector | |
---|---|---|---|
底层原理 | 基于动态数组实现 | 基于双向链表实现 | 基于动态数据实现 |
线程安全 | 不安全 | 不安全 | 线程安全 |
频繁的插入和删除 | - | 适合 | - |
适用场景 | 适合随机访问 | 适合频繁插入和删除 | 基本不使用了 |
Set
特点
- 无序集合(部分实现是有序的)
Set 是一个无序集合,它不保证元素的存储顺序(HashSet)。不过,也有一些 Set 的实现(如 LinkedHashSet 和 TreeSet)会保持一定的顺序。
- 不允许重复元素
Set 中不允许存储重复的元素。当你尝试向 Set 中添加一个已经存在的元素时,添加操作会失败,Set 会自动去重
实现类
HashSet
- 底层实现
基于哈希表实现。它通过哈希函数将元素映射到一个桶(bucket)中。当添加元素时,会先计算元素的哈希值,然后根据哈希值确定元素存储的位置。
- 性能特点
- 添加、删除和查找效率高
在理想情况下(哈希函数分布均匀,没有大量冲突),添加、删除和查找元素的时间复杂度都是 O(1)。但如果出现大量哈希冲突,性能会下降。
- 添加、删除和查找效率高
- 适用场景
适用非线程安全场景
- 常用方法
//创建一个空的 HashSet HashSet<String> set = new HashSet<>(); //创建一个包含指定集合元素的 HashSet HashSet<String> set2 = new HashSet<>(Arrays.asList("A", "B", "C")); //向集合中添加一个元素 set.add("D"); //从集合中删除一个元素 set.remove("A"); //检查集合中是否包含指定元素 boolean contains = set.contains("B"); //清空集合中的所有元素 set.clear(); //返回集合中的元素数量 int size = set.size(); //返回一个迭代器,用于遍历集合 Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }
LinkedHashSet
- 底层实现
基于哈希表+双向链表实现。它在 HashSet 的基础上,通过双向链表维护元素的插入顺序。
- 性能特点
- 添加、删除和查找效率高
基本操作的时间复杂度和 HashSet 类似,也是 O(1)。同时,它还能保持元素的插入顺序。
- 添加、删除和查找效率高
- 适用场景
非线程安全场景
- 常用方法:
LinkedHashSet 继承了 HashSet 的所有方法,因此它与 HashSet 的方法完全相同
LinkedHashSet<String> linkedSet = new LinkedHashSet<>();
TreeSet
-
底层实现
基于红黑树实现。它是一个有序的 Set 实现,元素会按照自然顺序(或者指定的比较器顺序)进行排序。
-
性能特点
- 添加、删除和查找
时间复杂度都是 O(log n)。因为红黑树是一种平衡二叉树,每次操作都需要在树中进行搜索或调整,所以时间复杂度是 O(log n)。
- 添加、删除和查找
-
常用方法:
//创建一个空的 TreeSet,元素按自然顺序排序 TreeSet<String> treeSet = new TreeSet<>(); //创建一个空的 TreeSet,元素按指定比较器排序 // 创建一个自定义比较器,按降序排列 Comparator<Integer> descendingComparator = (a, b) -> b.compareTo(a); TreeSet<Integer> treeSet2 = new TreeSet<>(descendingComparator); //创建一个包含指定集合元素的 TreeSet,元素按自然顺序排序 TreeSet<String> treeSet3 = new TreeSet<>(Arrays.asList("C", "A", "B")); //向集合中添加一个元素 treeSet.add("D"); //从集合中删除一个元素 treeSet.remove("A"); //检查集合中是否包含指定元素 boolean contains = treeSet.contains("B"); //清空集合中的所有元素 treeSet.clear(); //返回集合中的元素数量 int size = treeSet.size(); //返回一个迭代器,用于遍历集合 Iterator<String> iterator = treeSet.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } //返回集合中的第一个元素(最小值) String first = treeSet.first(); //返回集合中的最后一个元素(最大值) String last = treeSet.last(); //headSet(E toElement):返回一个包含集合中小于 toElement 的所有元素的子集合 TreeSet<String> head = treeSet.headSet("C"); //tailSet(E fromElement):返回一个包含集合中大于等于 fromElement 的所有元素的子集合 TreeSet<String> tail = treeSet.tailSet("B"); //subSet(E fromElement, E toElement):返回一个包含集合中大于等于 fromElement 且小于 toElement 的所有元素的子集合 TreeSet<String> sub = treeSet.subSet("A", "C");
小结
优点
-
自动去重
Set 的最大优点是自动去重,这使得它在处理需要唯一性数据的场景时非常方便。例如,当你需要从一个数据源中提取不重复的记录时,使用 Set 可以轻松实现。
-
高效的查找性能
对于 HashSet 和 LinkedHashSet,由于基于哈希表实现,在理想情况下查找元素的速度非常快,时间复杂度是 O(1)。这使得它们在需要快速判断元素是否存在的场景下表现良好。
缺点
- 无序性
HashSet 是无序的,如果你需要按照一定的顺序处理数据,那么使用 HashSet 可能不合适。不过你可以选择 LinkedHashSet 或 TreeSet 来解决这个问题。
- TreeSet的性能开销
TreeSet 由于需要维护元素的排序,所以在添加、删除和查找操作时,时间复杂度是 O(log n),相比 HashSet 的 O(1)(理想情况)性能开销要大一些。
HashSet | LinkedHashSet | TreeSet | |
---|---|---|---|
定义 | |||
底层原理 | 基于哈希表 | 基于哈希表+链表 | 基于红黑书 |
是否有序 | 无序 | 保留插入顺序 | 按照自然顺序或自定义顺序排序 |
线程安全 | 不安全 | 不安全 | 不安全 |
时间复杂的 | O(1) | 略低于HashSet | O(log n) |
使用场景 | 存储一组不重复的用户 ID | 存储一组不重复的用户操作记录,保持操作顺序 | 存储一组不重复的分数,并按分数排序 |
总结
- 一、定义
- List是有序的集合,允许重复元素
- Set是无序的集合,不允许重复元素
- 二、元素顺序
- List是有序的,适合需要保留插入顺序的场景
- Set是无序的或者按照特定的规则排序的,
- 三、元素唯一
- List允许重复的元素
- Set不允许重复元素
- 四、快速查找元素
- List不适合,因为需要遍历
- Set适合(因为基于哈希值或树的查找)
- 五、适用场景
- List适合需要保留插入顺序或允许重复元素的场景
- Set适合需要去重或者快速查找的场景