【 Java八股文面试 | Java集合 】
摘要:本文聚焦 Java 八股文面试的高频考点 ——Java 集合,内容源自小林 coding,在原文基础上进行了精简提炼,同时以加粗形式突出核心要点,助力快速抓牢面试关键信息,高效备战面试。
概念
数组与集合区别?
数组和集合的区别:
- 数组是固定长度的数据结构,一旦创建长度就无法改变;而集合是动态长度的数据结构,可以根据需要动态增加或减少元素。
- 数组可以包含基本数据类型和对象,而集合只能包含对象。
- 数组可以直接通过下标访问元素,而集合需要通过迭代器或其他方法访问元素。
用过哪些集合?
我用过的一些 Java 集合类:
- ArrayList: 动态数组,支持动态增长。
- LinkedList: 双向链表,支持快速的插入和删除操作。
- HashMap: 基于哈希表的Map实现,存储键值对,通过键快速查找值。
- HashSet: 基于HashMap实现的Set集合,用于存储唯一元素。
- TreeMap: 基于红黑树实现的有序Map集合,可以按照键的顺序进行排序。
说说Java中的集合?

List是有序的Collection,使用此接口能够精确的控制每个元素的插入位置,用户能根据索引访问List中元素。常用的实现List的类有LinkedList,ArrayList。
- ArrayList:是容量可变的列表,其底层使用数组实现。当几何扩容时,会创建更大的数组,并把原数组复制到新数组。ArrayList支持对元素的快速随机访问,但插入与删除速度很慢。
- LinkedList:是双向链表,与ArrayList相比,其插入和删除速度更快,但随机访问速度更慢。
Set中的元素是无序,不重复的。常用的实现有HashSet,LinkedHashSet和TreeSet。
- HashSet:通过HashMap实现,HashMap的Key即HashSet存储的元素,所有Key都是用相同的Value,Value是一个名为PRESENT的Object类型常量。使用Key保证元素唯一性,但不保证有序性。由于HashSet是HashMap实现的,因此线程不安全。
- LinkedHashSet:继承自HashSet,通过LinkedHashMap实现,使用双向链表维护元素插入顺序。
- TreeSet:通过TreeMap实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Map 是一个键值对集合,存储键、值和之间的映射。Key 无序,唯一;value 不要求有序,允许重复。Map 没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应的值对象。主要实现有HashMap、HashTable、ConcurrentHashMap
- HashMap:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突),JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
- HashTable:数组+链表组成的,数组是 HashTable 的主体,链表用于解决哈希冲突
- TreeMap:红黑树(自平衡的排序二叉树)
- ConcurrentHashMap:Node数组+链表+红黑树实现,线程安全的(jdk1.8以前Segment锁,1.8以后volatile + CAS / synchronized)
Collections和Collection的区别
- Collection是Java集合框架中的一个接口,它是所有集合类的基础接口。它定义了一组通用的操作和方法,如添加、删除等,用于操作和管理一组对象。Collection接口有许多实现类,如List、Set等。
- Collections是Java提供的一个工具类,位于java.util包中。它提供了一系列静态方法,这些方法可以对实现了Collection接口的集合进行操作,Collections类中的方法包括排序、查找等。
集合遍历的方法有哪些?
在Java中,集合的遍历方法主要有以下几种:
- 普通 for 循环: 可以使用带有索引的普通 for 循环来遍历 List。
- 增强 for 循环(for-each循环): 用于循环访问数组或集合中的元素。
- Iterator 迭代器: 可以使用迭代器来遍历集合,特别适用于需要删除元素的情况。
- ListIterator 列表迭代器: ListIterator是迭代器的子类,可双向访问列表并在迭代时修改元素。
- 使用 forEach 方法: Java 8引入了 forEach 方法,可以对集合进行快速遍历。
- **Stream API:**若需要先对集合元素进行复杂处理(如过滤、转换等),再遍历结果,则适合用 Stream API 串联操作,最后通过流的
forEach输出
List

常见的List集合(非线程安全):
ArrayList基于动态数组实现,它允许快速的随机访问,即通过索引访问元素的时间复杂度为 O (1)。在添加和删除元素时,如果操作位置不是列表末尾,可能需要移动大量元素,性能相对较低。适用于需要频繁随机访问元素,而对插入和删除操作性能要求不高的场景,如数据的查询和展示等。LinkedList基于双向链表实现,在插入和删除元素时,只需修改链表的指针,不需要移动大量元素,时间复杂度为 O (1)。但随机访问元素时,需要从链表头或链表尾开始遍历,时间复杂度为 O (n)。适用于需要频繁进行插入和删除操作的场景,如队列、栈等数据结构的实现,以及需要在列表中间频繁插入和删除元素的情况。
常见的List集合(线程安全):
Vector和ArrayList类似,基于数组实现。Vector中的方法大多是同步的(sychronized),这使得它在多线程环境下可以保证数据的一致性,但在单线程环境下,由于同步带来的开销,性能会略低于ArrayList。CopyOnWriteArrayList在对列表进行修改(如添加、删除元素)时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然在原数组上进行,这样可以保证读操作不会被写操作阻塞,实现了读写分离,提高了并发性能。适用于读操作远远多于写操作的并发场景,如事件监听列表等,在这种场景下可以避免大量的锁竞争,提高系统的性能和响应速度。
这几种实现具体在什么场景下应该用哪种?
- Vector 和 ArrayList 作为动态数组,适合随机访问的场合,插入和删除元素性能差。
- 而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。
CopyOnWriteArrayList在需要线程安全时使用
Arraylist和LinkedList的区别?
ArrayList和LinkedList都是Java中常见的集合类,它们都实现了List接口。
- 底层数据结构不同:ArrayList使用数组实现。LinkedList使用链表实现。
- 插入和删除操作的效率不同:ArrayList在尾部的插入和删除操作效率较高。LinkedList在任意位置的插入和删除操作效率都较高。
- 随机访问的效率不同:ArrayList支持通过索引进行快速随机访问,时间复杂度为O(1)。LinkedList需要从头或尾开始遍历链表,时间复杂度为O(n)。
- 空间占用:ArrayList在创建时需要分配一段连续的内存空间,因此占用较大空间。LinkedList每个节点只需要存储元素和指针,因此相对较小。
- 使用场景:ArrayList适用于需要频繁访问集合元素的场景,LinkedList适用于频繁进行插入和删除操作的场景。
- 线程安全:这两个集合都不是线程安全的。
ArrayList线程安全吗?把ArrayList变成线程安全有哪些方法?
不是线程安全的,ArrayList变成线程安全的方式有:
- 使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List:
List<String> synchronizedList = Collections.synchronizedList(arrayList);- 使用CopyOnWriteArrayList类代替ArrayList,它是一个线程安全的List实现:
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);- 使用Vector类代替ArrayList,Vector是线程安全的List实现:
Vector<String> vector = new Vector<>(arrayList);ArrayList的扩容机制说一下
ArrayList在添加元素时,如果当前元素个数已经达到了内部数组的容量上限,就会触发扩容操作。ArrayList的扩容操作主要包括以下几个步骤:
- 计算新的容量:一般情况下,新的容量会扩大为原容量的1.5倍。
- 创建新的数组:根据计算得到的新容量,创建一个新的更大的数组。
- 将元素复制:将原来数组中的元素逐个复制到新数组中。
- 更新引用:将ArrayList内部指向原数组的引用指向新数组。
- 完成扩容:扩容完成后,可以继续添加新元素。
ArrayList的扩容操作涉及到数组的复制和内存的重新分配,所以在频繁添加大量元素时,扩容操作可能会影响性能。为了减少扩容带来的性能损耗,可以在初始化ArrayList时预分配足够大的容量,避免频繁触发扩容操作。
之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数
int newCapacity = oldCapacity + (oldCapacity >> 1);List<>里面填基本数据类型为什么会报错?
List<> 等泛型集合类要求填充的必须是引用类型(对象类型),而不能直接使用基本数据类型(如 int、char、double 等),否则会编译报错。解决办法是,使用对应的包装类。
这么设计的原因是泛型的类型擦除机制:Java 泛型在编译后会被擦除为 Object 类型,而 Object 只能接收引用类型,不能接收基本数据类型。
Map

常见的Map集合(非线程安全):
HashMap是基于哈希表实现的Map,它根据键的哈希值来存储和获取键值对,JDK 1.8中是用数组+链表+红黑树来实现的。HashMap是非线程安全的。LinkedHashMap继承自HashMap,它在HashMap的基础上,使用双向链表维护了键值对的插入顺序或访问顺序,使得迭代顺序与插入顺序或访问顺序一致。由于它继承自HashMap,在多线程并发访问时,同样会出现与HashMap类似的线程安全问题。TreeMap是基于红黑树实现的Map,它可以对键进行排序,默认按照自然顺序排序,也可以通过指定的比较器进行排序。
常见的Map集合(线程安全):
Hashtable是早期 Java 提供的线程安全的Map实现,它的实现方式与HashMap类似,但是在每个可能修改Hashtable状态的方法上加上synchronized关键字。ConcurrentHashMap在 JDK 1.8 以前采用了分段锁等技术来提高并发性能。在ConcurrentHashMap中,将数据分成多个段(Segment),每个段都有自己的锁。在进行插入、删除等操作时,只需要获取相应段的锁,而不是整个Map的锁,这样可以允许多个线程同时访问不同的段,提高了并发访问的效率。在 JDK 1.8 以后是通过 volatile + CAS 或者 synchronized 来保证线程安全的。
如何对map进行快速遍历?
- 使用for-each循环和entrySet()方法:常见遍历方式,它可以同时获取
Map中的键和值。
for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());}
- 使用for-each循环和keySet()方法:如果只需要遍历
Map中的键,可以使用keySet()方法。
for (String key : map.keySet()) {System.out.println("Key: " + key + ", Value: " + map.get(key));}
- 使用迭代器:通过获取Map的entrySet()或keySet()的迭代器,也可以实现对Map的遍历。
- 使用 Lambda 表达式和forEach()方法:可以使用 Lambda 表达式和
forEach()方法来遍历Map。
map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value))
- 使用Stream API:
Stream API遍历Map,可以将Map转换为流,然后进行各种操作。
HashMap实现原理介绍一下?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。

所以在JDK 1.8版本的时候做了优化,当一个链表的长度超过8,且数组长度大于644的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。

了解的哈希冲突解决方法有哪些?
- 链接法:使用链表或其他数据结构来存储冲突的键值对,将它们链接在同一个哈希桶中。
- 开放寻址法:在哈希表中找到另一个可用的位置来存储冲突的键值对,而不是存储在链表中。常见的开放寻址方法包括线性探测、二次探测和双重散列。
- 再哈希法:当发生冲突时,使用另一个哈希函数再次计算键的哈希值,直到找到一个空槽来存储键值对。
- 哈希桶扩容:当哈希冲突过多时,可以动态地扩大哈希桶的数量,重新分配键值对,以减少冲突的概率。
hashmap的put过程介绍一下

HashMap HashMap的put()方法用于向HashMap中添加键值对,当调用HashMap的put()方法时,会按照以下详细流程执行(JDK8 1.8版本):
第一步:根据要添加的键的哈希码计算在数组中的位置(索引)。
第二步:检查该位置是否为空(即没有键值对存在)
- 如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。
第三步:如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同
- 如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。
第四步:如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:
如果键值对集合是链表结构,从链表的头部开始逐个比较键的哈希码和equals()方法,直到找到相同的键或达到链表末尾。
- 如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
- 如果没有找到相同的键,则将新的键值对添加到链表的头部。
如果键值对集合是红黑树结构,在红黑树中使用哈希码和equals()方法进行查找。根据键的哈希码,定位到红黑树中的某个节点,然后逐个比较键,直到找到相同的键或达到红黑树末尾。
- 如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
- 如果没有找到相同的键,则将新的键值对添加到红黑树中。
第五步:检查链表长度是否达到阈值(默认为8):
- 如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树,以提高查询效率。
第六步:检查负载因子是否超过阈值(默认为0.75):
- 如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。
第七步:扩容操作:
- 创建一个新的两倍大小的数组。
- 将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。
- 更新HashMap的数组引用和阈值参数。
第八步:完成添加操作。
此外,HashMap是非线程安全的,如果在多线程环境下使用,需要采取额外的同步措施或使用线程安全的ConcurrentHashMap。
HashMap的put(key,val)和get(key)过程
- 存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量。
- 获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
hashmap 调用get方法一定安全吗?
不是,调用 get 方法有几点需要注意的地方:
- 空指针异常:如果你尝试用
null作为键调用get方法,而HashMap没有被初始化(即为null),那么会抛出空指针异常。不过,如果HashMap已经初始化,使用null作为键是允许的,因为HashMap支持null键。 - 线程安全:
HashMap本身不是线程安全的。如果需要在多线程环境中使用类似HashMap的数据结构,可以考虑使用ConcurrentHashMap。
HashMap一般用什么做Key?为啥String适合做Key呢?
用 string 做 key,因为 String对象是不可变的,一旦创建就不能被修改,这确保了Key的稳定性。如果Key是可变的,可能会导致hashCode和equals方法的不一致,进而影响HashMap的正确性。
为什么HashMap要用红黑树而不是平衡二叉树?
- 平衡二叉树追求的是一种 “完全平衡” 状态:任何结点的左右子树的高度差不会超过 1,优势是树的结点是很平均分配的。这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
- 红黑树不追求这种完全平衡状态,而是追求一种 “弱平衡” 状态:整个树最长路径不会超过最短路径的 2 倍。优势是虽然牺牲了一部分查找的性能效率,但是能够换取一部分维持树平衡状态的成本。与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。
hashmap key可以为null吗?
可以为 null。
- hashMap中使用hash()方法来计算key的哈希值,当key为空时,直接另key的哈希值为0,不走key.hashCode()方法;

- hashMap虽然支持key和value为null,但是null作为key只能有一个,null作为value可以有多个;
- 因为hashMap中,如果key值一样,那么会覆盖相同key值的value为最新,所以key为null只能有一个。
重写HashMap的equal和hashcode方法需要注意什么?
hashCode() 重写原则:
- 若
a.equals(b)为true,则a.hashCode()必须等于b.hashCode()(核心!相等的对象必须有相等的哈希值)。 - 若
a.equals(b)为false,a.hashCode()和b.hashCode()可以相等(但尽量不同,以减少哈希冲突)
重写HashMap的equal方法不当会出现什么问题?
HashMap在比较元素时,会先通过hashCode进行比较,相同的情况下再通过equals进行比较。
所以 equals相等的两个对象,hashCode一定相等。hashCode相等的两个对象,equals不一定相等(比如散列冲突的情况)
重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回false,这样的一个后果会导致在hashmap等类中存储多个一模一样的对象,导致出现覆盖存储的数据的问题,这与hashmap只能有唯一的key的规范不符合。
HashMap的扩容机制介绍一下
hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤:
- 第1步是对哈希表长度的扩展**(2倍)**
- 第2步是将旧哈希表中的数据放到新的哈希表中。
因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
如我们从16(默认大小)扩展为32时,具体的变化如下所示:

因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
Hashmap和Hashtable有什么不一样的?
- 效率方面:HashMap效率高,HashTable效率低。
- 线程安全方面:HashMap线程不安全,HashTable线程安全。
- 底层数据结构:HashMap底层是数组+链表+红黑树,HashTable底层是数组+链表。
- 扩容机制:
- HashMap默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为原来的两倍。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
- HashTable默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
ConcurrentHashMap怎么实现的?
JDK 1.7 ConcurrentHashMap
在 JDK 1.7 中底层是底层结构是 Segment[] 数组(默认长度 16)+链表,每个 Segment 元素都是一个独立的 “分段锁”(继承自 ReentrantLock),并各自管理一个 HashEntry[] 数组(存储该分段下的键值对),HashEntry 数组的每个元素都是链表的头节点

JDK 1.7 ConcurrentHashMap 分段锁技术将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
JDK 1.8 ConcurrentHashMap
在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而 JDK 1.8 则使用了数组 + 链表/红黑树的方式优化了 ConcurrentHashMap 的实现,具体实现结构如下:

JDK 1.8 ConcurrentHashMap JDK 1.8 ConcurrentHashMap 主要通过 volatile + CAS 或者 synchronized 来实现的线程安全的。
添加元素时首先会判断容器是否为空:
如果为空则使用 volatile 加 CAS 来初始化**Node[] 数组**
如果容器不为空,定位元素位置并处理。
分两种情况处理:
- 目标位置为空:直接通过 CAS 操作 将新节点放入该位置(无需加锁,高效)。
- 目标位置不为空(存在哈希冲突,已有链表或红黑树):
- 用 synchronized 锁定该位置的头节点(只锁链表 / 红黑树,而非数组,粒度更细)。
- 遍历该位置的链表 / 红黑树,判断是否存在相同的键:若存在则替换值,否则新增节点。
- 操作完成后,判断链表长度是否超过阈值,若超过则转为红黑树(优化查询性能)。
如果把上面的执行用一句话归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,操作性能提高。
而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。
hashtable 和concurrentHashMap有什么区别
底层数据结构:
- jdk7前ConcurrentHashMap底层采用分段的数组+链表实现,jdk8后采用数组+链表/红黑树;
- HashTable采用的是数组+链表,数组是主体,链表是解决hash冲突存在的。
实现线程安全的方式:
- jdk8以前,ConcurrentHashMap采用分段锁,对整个数组进行了分段分割,每一把锁只锁容器里的一部分数据,多线程访问不同数据段里的数据,就不会存在锁竞争,提高了并发访问;jdk8以后,直接采用数组+链表/红黑树,并发控制使用CAS和synchronized操作,更加提高了速度。
- HashTable:所有的方法都加了锁来保证线程安全,但是效率非常的低下,当一个线程访问同步方法,另一个线程也访问的时候,就会陷入阻塞或者轮询的状态
说一下HashMap和Hashtable、ConcurrentMap的区别
- HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
- HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
- ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。
Set
Set集合有什么特点?如何实现key无重复的?
- set集合特点:Set集合中的元素是唯一的,不会出现重复的元素。
- set实现原理:Set集合通过内部的数据结构(如哈希表、红黑树等)来实现key的无重复。当向Set集合中插入元素时,会先根据元素的hashCode值来确定元素的存储位置,然后再通过equals方法来判断是否已经存在相同的元素,如果存在则不会再次插入,保证了元素的唯一性。
有序的Set是什么?记录插入顺序的集合是什么?
- 有序的 Set 是TreeSet和LinkedHashSet。TreeSet是基于红黑树实现,保证元素的自然顺序。LinkedHashSet是基于双重链表和哈希表来实现元素的有序存储,保证元素添加的自然顺序
- 记录插入顺序的集合通常指的是LinkedHashSet,它不仅保证元素的唯一性,还可以保持元素的插入顺序。当需要在Set集合中记录元素的插入顺序时,可以选择使用LinkedHashSet来实现
