Java面试宝典-Java集合01
目录
Java面试宝典-Java集合01
1、Java中常用的集合有哪些?
2、Collection 和 Collections 有什么区别?
3、为什么集合类没有实现 Cloneable 和 Serializable 接口?
4、数组和集合有什么本质区别?
5、数组和集合如何选择?
6、list与Set区别
7、HashMap 和 Hashtable 有什么区别?
8、concurrentHashMap和HashTable有什么区别
9、HashMap 的工作原理是什么?
10、Hashmap什么时候进行扩容?
11、说一下 HashMap 的实现原理?
12、为什么HashMap使用红黑树而不使用AVL树
13、Java中的ConcurrentHashMap中为什么不能存储null?
14、Java8开始ConcurrentHashMap,为什么舍弃分段锁?
15、ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
16、set有哪些实现类?
17、说一下HashSet的实现原理
18、Set是如何保证元素不重复的?
19、HashMap和HashSet的区别
20、TreeSet常用方法有哪些?
1、Java中常用的集合有哪些?
Java中常用的集合有:
List
接口的实现类:ArrayList
:基于动态数组实现,支持随机访问。LinkedList
:基于双向链表实现,适合插入和删除操作。Vector
:和ArrayList
类似,但是线程安全,性能较差。
Set
接口的实现类:HashSet
:基于哈希表实现,不保证元素的顺序。LinkedHashSet
:hash表和链表实现,保持元素插入的顺序。TreeSet
:基于红黑树实现,保持元素的排序。
Map
接口的实现类:HashMap
:基于哈希表实现,允许null
键和值。LinkedHashMap
:hash表和链表实现,保持键值对的插入顺序。TreeMap
:基于红黑树结构实现,可排序。Hashtable
:和HashMap
类似,但是线程安全,性能较差。
Queue
接口的实现类:ArrayDeque
:基于数组实现的双端队列。PriorityQueue
:基于堆实现,用于排序。
以下是一个简单的示例代码,展示了如何创建和使用集合:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
public class CollectionExample {
public static void main(String[] args) {
// Lists
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
// Sets
Set<String> hashSet = new HashSet<>();
Set<String> linkedHashSet = new LinkedHashSet<>();
Set<String> treeSet = new TreeSet<>();
// Maps
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
// Queues
Queue<String> arrayDeque = new ArrayDeque<>();
Queue<String> priorityQueue = new PriorityQueue<>();
// 示例操作
arrayList.add("Hello");
linkedList.add("World");
hashSet.add("Java");
treeSet.add("Collection");
hashMap.put("Example", 1);
linkedHashMap.put("Usage", 2);
arrayDeque.offer("ArrayList");
priorityQueue.offer("PriorityQueue");
// 打印集合内容
System.out.println("ArrayList: " + arrayList);
System.out.println("LinkedList: " + linkedList);
System.out.println("HashSet: " + hashSet);
System.out.println("TreeSet: " + treeSet);
System.out.println("HashMap: " + hashMap);
System.out.println("LinkedHashMap: " + linkedHashMap);
System.out.println("ArrayDeque: " + arrayDeque);
System.out.println("PriorityQueue: " + priorityQueue);
}
}
2、Collection 和 Collections 有什么区别?
(1)Collection是最基本的集合接口,Collection派生了两个子接口list和set,分别定义了两种不同的存储方式。
(2)Collections是一个包装类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)。此类不能实例化,就像一个工具类,服务于Collection框架。
角色与功能:
Collection 是一个接口,它是Java集合框架的根接口,定义了集合的基本操作和行为,如添加、删除、遍历等。它包括List、Set、Queue等集合类的父接口。这意味着Collection本身不直接实现任何功能,而是通过其子接口和实现类来提供具体的操作。
Collections 是一个工具类,位于java.util包中,提供了一系列静态方法,用于对集合进行各种操作。这些方法通常用于对集合进行一些常见的操作,如排序、查找、替换等。Collections类不能被实例化,因为它是一个工具类,服务于Java的Collection框架。
使用场景:
Collection 主要用于存储和操作一组对象。通过实现Collection接口的类(如ArrayList、LinkedList等),可以定义各种不同的数据结构。
Collections 类中的静态方法主要用于对集合类进行操作,例如排序、查找、替换、复制、打乱顺序等。这些方法都是静态的,可以直接通过类名调用,而不需要创建Collections类的实例。
扩展性:
Collection 接口在Java类库中有很多具体的实现,用户可以根据需要选择不同的实现类。
Collections 类则没有具体的实现,它是一个工具类,提供了一些实用的静态方法。
综上所述,Collection是一个接口,定义了集合的基本操作;而Collections是一个工具类,提供了对集合进行操作的静态方法。在实际使用中,应根据需求选择合适的类或接口
3、为什么集合类没有实现 Cloneable 和 Serializable 接口?
集合类没有实现 Cloneable 和 Serializable 接口的原因有以下几点:
1. 多态性:集合类是一个容器,可以容纳任意类型的对象,而 Cloneable 和 Serializable 接口是针对具体对象的。如果集合类实现了这两个接口,就会限制集合类只能存储实现了这两个接口的对象,失去了多态性。
2. 元素生命周期:集合类的元素通常具有不同的生命周期,可能是瞬时对象,也可能是持久化对象,不适合统一实现 Cloneable 和 Serializable 接口。
3. 序列化和克隆的语义:Cloneable 接口用于实现对象的克隆,而 Serializable 接口用于实现对象的序列化。集合类的元素可能具有不同的克隆和序列化语义,无法统一实现这两个接口。
因此,集合类没有默认实现 Cloneable 和 Serializable 接口,如果需要使用克隆和序列化功能,可以通过自定义类来实现这两个接口,并将集合类作为成员变量来实现相应的功能。
4、数组和集合有什么本质区别?
数组的大小是固定的,一旦创建后就不能改变,而集合的大小是动态的,可以根据需要扩展和缩减。数组可以存储基本数据类型和引用数据类型,而集合只能存储引用数据类型(对象)。
数组通常用于存储同一类型的数据,而集合可以存储不同类型的数据。
数组的长度是不可变的,而集合的长度是可变的,这意味着可以在运行时向集合中添加或删除元素。
5、数组和集合如何选择?
如果数据的大小是固定的,那么数组可能是一个更好的选择,因为它提供了固定大小的存储空间。相反,如果数据的大小可能会发生变化,那么集合可能更合适。
如果需要存储基本数据类型,那么只能使用数组,如果需要存储不同类型的数据,集合可能更适合。数组在访问速度上通常比集合更快,因为它们可以通过索引直接访问元素。
集合提供了许多有用的方法,如add、remove、contains等,这些方法使得数据的操作更加方便。如果需要使用这些方法,那么集合可能是更好的选择。
6、list与Set区别
(1)List简介
实际上有两种List:一种是基本的ArrayList,其优点在于随机访问元素,另一种是LinkedList,它并不是为快速随机访问设计的,而是快速的插入或删除。
① ArrayList:由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。
② LinkedList :对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。
还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。
(2)Set简介
Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上Set就是Collection,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。Set不保存重复的元素(至于如何判断元素相同则较为负责)
① Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与
Collection有完全一样的接口。Set接口不保证维护元素的次序。
② HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
③ TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
(3)list与Set区别
① List,Set都是继承自Collection接口
② List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但
是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循
环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
③ Set和List对比:
1. Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
2. List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ListVsSetExample {
public static void main(String[] args) {
// 使用List
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Apple"); // 可以重复
System.out.println("List: " + list); // 输出 [Apple, Banana, Apple]
// 使用Set
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 不可以重复,因为Set不会包含重复元素
System.out.println("Set: " + set); // 输出 [Banana, Apple],顺序可能不同
}
}
在这个例子中,当我们向List和Set中添加相同的元素"Apple"时,List允许重复,而Set不允许重复,因此第二个"Apple"在添加到Set时并未生效。
7、HashMap 和 Hashtable 有什么区别?
1. Hashtable不允许键或值为null,否则会抛出NullPointerException异常。而HashMap可以存储key和value为null的元素;
2. Hashtable继承自Dictionary类,HashMap继承自AbstractMap类并实现了Map接口;
3. Hashtable在创建时必须指定容量大小,且默认大小为11。而HashMap可以在创建时不指定容量大小,系统会自动分配初始容量,并
采用2倍扩容机制;
4. 迭代器 Iterator 对 Hashtable 是安全的,而 Iterator 对 HashMap 不是安全的,因为迭代器被设计为工作于一个快照上,如果在迭代
过程中其他线程修改了 HashMap,则会抛出并发修改异常;
5. Hashtable是线程安全的,而HashMap是非线程安全的。Hashtable通过在每个方法前加上synchronized关键字来保证线程安全性,而
HashMap则没有实现这种机制。
8、concurrentHashMap和HashTable有什么区别
HashMap和Hashtable在Java中的主要区别包括线程安全性、对null的支持、继承的类、性能以及使用场景。
线程安全性:
HashMap是非线程安全的,这意味着在多线程环境下使用它可能会导致数据不一致或其他并发问题。如果需要在多线程环境中使用,则需要额外的同步措施。
Hashtable是线程安全的,它通过内部同步来确保在并发访问时的数据一致性。然而,这也意味着它在单线程环境下的性能可能稍低于HashMap。
对null的支持:
HashMap允许null键(key)和null值(value)。
Hashtable不允许null键和null值。如果试图在Hashtable中插入null键或null值,将会抛出NullPointerException。
继承的类:
HashMap继承自AbstractMap类,并实现了Map接口。
Hashtable继承自Dictionary类,并实现了Map接口。由于Hashtable是Java早期版本的一部分,它继承了Dictionary类,但现代的Java程序更倾向于使用Map接口及其实现。
性能:
由于Hashtable是线程安全的,它在某些情况下的性能可能不如HashMap。然而,这取决于具体的使用场景和并发需求。HashMap在单线程环境下的性能通常更好,因为它没有内部同步的开销。
使用场景:
如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。在多线程环境下若使用HashMap,需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。
Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好,提供了更高的并发性能。
下面是一个简单的代码示例来说明HashMap和Hashtable的区别:
import java.util.HashMap;
import java.util.Hashtable;
public class HashMapVsHashtable {
public static void main(String[] args) {
// 创建一个HashMap实例
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("one", 1);
hashMap.put("two", 2);
System.out.println("HashMap size: " + hashMap.size());
// 创建一个Hashtable实例(注意:由于Hashtable不允许null键和值,这里会抛出NullPointerException)
// Hashtable<String, Integer> hashtable = new Hashtable<>(); // 这行代码会出错,因为Hashtable不允许null键和值
// 正确的方式是使用已知的键和值创建Hashtable实例,例如:
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("one", 1); // 注意这里不能使用null键或值,否则会抛出NullPointerException(虽然在这个例子中没有使用null)
System.out.println("Hashtable size: " + hashtable.size()); // 注意:由于Hashtable是线程安全的,它在多线程环境下的性能可能不如HashMap
}
}
9、HashMap 的工作原理是什么?
(1)存储
当向HashMap中添加一个键值对时,首先会计算键(Key)的哈希值,这个哈希值将决定该键值对在内部数组中的索引位置。然后,该键值对会被存储在对应索引的链表中。如果两个不同的键拥有相同的哈希值,它们会被存储在同一个索引位置,这种现象称为哈希冲突。为了解决冲突,HashMap会在链表中维护这些具有相同哈希值的键值对。
(2)查找
当需要获取某个特定键对应的值时,HashMap会再次计算该键的哈希值,并沿着对应索引的链表查找匹配的键。一旦找到,就返回对应的值。
(3)扩容
随着HashMap中元素的增加,为了防止性能下降,当链表的长度超过一定阈值时,HashMap会进行自动扩容。这个过程涉及到创建一个新的、更大的数组,并将旧数组中的所有元素重新映射到新数组的索引上。这个过程也被称为rehashing。
(4)数据结构
HashMap的内部结构是一个Entry数组,每个Entry包含一个key-value键值对。这样设计的目的是为了高效地存储和检索数据。
10、Hashmap什么时候进行扩容?
在初始化HashMap时,需要指定其初始容量和负载因子。负载因子是一个介于0到1之间的浮点数,默认值为0.75。当HashMap中的元素数量达到当前容量乘以负载因子时,即满足capacity * loadFactor条件时,就会触发扩容操作。
在扩容过程中,HashMap会创建一个新的数组,这个新数组的容量是原来容量的两倍。然后,它会重新计算每个键值对的哈希值,并将这些键值对重新映射到新数组的对应位置上。这个过程可能会涉及到一些性能开销,因为它需要重新计算哈希值和重新分配元素。
由于每次扩容都需要重新计算哈希值并重新分配元素,这会带来一定的性能开销。因此,我们应该尽量避免让HashMap频繁地进行扩容,以提高性能。
11、说一下 HashMap 的实现原理?
HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。
其中TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。
HashMap在JDK1.7中采用数组+链表的存储结构。
HashMap采取Entry数组来存储key-value,每一个键值对组成了一个Entry实体,Entry类时机上是一个单向的链表结构,它具有next指针,
指向下一个Entry实体,以此来解决Hash冲突的问题。
HashMap实现一个内部类Entry,重要的属性有hash、key、value、next。
JDK1.8中采用数据+链表+红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
12、为什么HashMap使用红黑树而不使用AVL树
HashMap在Java 8中引入了红黑树的用途是为了解决散列表性能问题。在哈希表中,当碰撞越来越严重时,查找效率会急剧下降。这种情况下,如果使用红黑树作为哈希桶中的数据结构,可以保持在最坏情况下的时间复杂度为O(log n),相比于链表的O(n),显著提高了查找效率。
AVL树是另一种自平衡二叉查找树,它通过旋转保持树的平衡,从而保证最坏情况下的时间复杂度。但是,在实际应用中,HashMap中的哈希桶通常包含的节点数量非常多,而且这些节点往往是在最坏的散列情况下插入的。在这种情况下,AVL树的旋转操作可能会导致性能下降,因为它需要维护从插入到平衡被破坏的所有节点的平衡,这在插入操作中是不必要的。
红黑树在插入和删除操作时通常只需要几次颜色变换和最多两次树旋转即可保持平衡,这种特性使得它在HashMap中作为哈希桶的数据结构更加高效。而且,红黑树的统计性能要优于AVL树,因此在Java 8中HashMap采用了红黑树作为哈希桶的实现。
13、Java中的ConcurrentHashMap中为什么不能存储null?
与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种更细的加锁机制来实现更大程度的共享,这种机制成为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
在多线程环境下,ConcurrentHashMap通过分段锁技术实现高并发访问。当多个线程尝试同时访问数据时,通过对数据结构的不同部分加锁,保证了数据的一致性和完整性。若允许null值存在,将增加在判断元素是否存在时的复杂度,因为需要区分key或value是否为null的情况,这在多线程操作时容易引起混淆和数据不一致性。
ConcurrentHashMap不能存储null值的主要原因包括:线程安全性维护、快速失败机制(FAIl-Fast)、以及返回值的歧义。
一、线程安全性维护
ConcurrentHashMap采用了分段锁的技术,通过对数据结构的一部分加锁而不是对整个数据结构加锁,大幅度提高了并发访问的效率。在这种机制下,如果允许null值,那么每次获取值时都必须进行额外的检查来避免空指针异常,这将降低整体的访问性能。而且,在并发修改过程中,null值的存在可能导致数据不一致,因为线程在检查null值之前和之后的状态可能会发生变化。
二、快速失败机制(Fail-Fast)
ConcurrentHashMap实现了快速失败机制,这意味着在迭代过程中,如果检测到结构性修改,则会立即抛出ConcurrentModificationException异常。这是为了避免不一致行为和潜在的错误。允许null值存在将使这一机制难以正确实现,因为null可以被视为一个特殊的值,而对null的插入或删除可能不会被视为结构性变化,导致机制失效。
三、返回值的歧义
在ConcurrentHashMap中,返回null通常意味着某个键没有被映射到任何值。如果允许存储null值,那么在调用例如get()方法时,返回null既可以表示键不存在,也可以表示键映射到了null值,这种歧义会使API使用变得复杂且容易出错。
14、Java8开始ConcurrentHashMap,为什么舍弃分段锁?
ConcurrentHashMap的原理是引用了内部的 Segment ( ReentrantLock ) 分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作 同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。 但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+CAS。 Java 8 中的 ConcurrentHashMap 放弃了分段锁,而是引入了 CAS 操作,即 Compare and Swap,利用原子性的操作和无锁编程的思想, 来实现并发写入:采用一种乐观锁的方式,通过比较当前值与期望值是否相等,来决定是否更新。这种方式避免了对整个数据结构加锁,提 高了并发写入时的性能和效率。
15、ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
在Java 8中的`ConcurrentHashMap`在实现上并没有完全使用`synchronized`,而是采用了一种更为精细的锁机制,称为CAS(Compare and Swap)操作,来提高并发性能。这是一种乐观锁策略,用于确保多个线程在没有明显争用的情况下可以同时进行操作,而不需要像`synchronized`一样阻塞线程。
然而`ReentrantLock`是一种悲观锁,它在某个线程获取锁的时候,会阻塞其他线程的访问,即使其他线程并不会导致竞争。这种情况下,`ReentrantLock`可能会导致性能下降,因为它引入了额外的线程切换和锁竞争。
而Java 8中的`ConcurrentHashMap`中使用CAS操作,允许多个线程同时尝试进行更新操作,只有当线程之间发生竞争时,才会有一个线程成功,其他线程会重新尝试。这种方式避免了线程之间的争用,提高了并发性能。此外,`ConcurrentHashMap`中的分段锁(Segment Locking)机制进一步减小了锁的粒度,允许多个线程在不同的分段上进行操作,从而降低了锁竞争的可能性。
16、set有哪些实现类?
Set集合的主要实现类有HashSet、LinkedHashSet、TreeSet、AbstractSet
(1)HashSet
1. 基于散列表实现的Set集合,内部的存储结构是哈希表,是线程不安全的
2. 它的底层数据结构是HashMap,因此它拥有快速的存取速度,是用的最多的实现类;
3. HashSet不保证元素的迭代顺序,也不保证该顺序会随着时间的推移保持不变;
4. 允许使用null元素;
(2)TreeSet
1. 基于红黑树(Red-Black tree)或者AVL树等自平衡二叉查找树实现的Set集合;
2. TreeSet可以确保集合元素处于排序状态;
3. 不允许插入null元素
TreeSet对元素进行排序的方式:
1. 元素自身具备比较功能,需要实现Comparable接口,并覆盖compareTo方法;
2. 元素自身不具备比较功能,需要实现Comparator接口,并覆盖compare方法。
(3)链接散列集LinkedHashSet
1. LinkedHashSet结合了哈希表和链表两种数据结构,具有可预知的迭代顺序;
2. 它继承自HashSet,但是又添加了一个双向链表来维持插入的顺序;
3. LinkedHashSet的元素迭代顺序是它们被插入的顺序,或者最近访问的顺序。
HashSet和LinkedHashSet内部使用哈希表来存储元素,当多个元素经过哈希函数计算后产生同一个索引位置时,就会产生哈希冲突。为了解决哈希冲突,HashSet和LinkedHashSet使用链式散列技术,即在哈希表每个索引位置上维护一个链表,将所有哈希值相同的元素存放在同一个链表中,从而实现快速查找和添加元素。
(4)AbstractSet
这是一个抽象类,它为创建新的set实现提供了一个框架。它本身不能直接实例化,但可以通过继承并实现其抽象方法来创建自定义的set实现类。
17、说一下HashSet的实现原理
HashSet底层使用的是数组加链表或者红黑树的数据结构。在JDK1.8之前,主要是数组加链表的方式,而在JDK1.8及以后的版本中,为了优化性能,引入了红黑树。
HashSet的实现原理基于HashMap,其内部维护了一个HashMap实例。HashSet的add(), remove()和contains()方法都是直接委托给HashMap的相应方法来实现的。
具体来说,HashSet的add()方法将元素作为key存入HashMap,HashMap的value则是一个固定的Object对象。由于HashMap的key是不允许重复的,因此HashSet的add()方法也就确保了元素的唯一性。
当我们向HashSet中添加一个元素时,首先会计算该元素的哈希值,然后根据哈希值确定元素在内部HashMap中的存储位置。如果该位置为空,则直接存储;如果不为空,则需要通过链表或红黑树来处理冲突。
在查找元素时,也是通过计算哈希值来确定位置,然后在对应的链表或红黑树中进行搜索。
HashSet的性能可以通过合理设置初始容量和负载因子来提高。一个较大的初始容量可以减少扩容操作的频率,而合适的负载因子可以平衡空间利用率和查找效率。
18、Set是如何保证元素不重复的?
HashSet内部实际上是通过HashMap来实现的。当向HashSet中添加一个元素时,它会调用该元素的hashCode()方法来计算其哈希值,然后根据这个哈希值决定元素在HashMap中的存储位置。
如果有两个元素具有相同的哈希值,那么它们会被视为同一个位置的候选者。为了区分这些具有相同哈希值的元素,HashSet还会使用equals()方法来比较它们是否相等。只有当元素在HashMap中不存在时,它才会被添加到集合中。如果已经存在,则不会重复添加,从而保证了Set集合中元素的唯一性。
19、HashMap和HashSet的区别
HashMap和HashSet的主要区别在于它们的设计目的、数据结构、存储方式和性能特点。
设计目的不同。HashMap用于存储键值对,每个键都是唯一的,并且与一个值相关联。当你需要通过一个键来查找对应的值时,HashMap非常有用。而HashSet用于存储一组唯一的对象,它是一个集合,只存储对象,不存储键值对。当你只关心一个元素是否存在于集合中,而不关心其具体位置或顺序时,HashSet非常有用。
数据结构不同。HashMap基于哈希表实现,内部使用数组加链表(或红黑树)的结构来存储键值对。而HashSet实际上是基于HashMap实现的,它内部维护了一个HashMap实例,所有元素都存储在HashMap的键部分,而值部分则使用了一个虚拟的对象(如PRESENT)。由于这种实现方式,HashSet只能保证元素的唯一性,但不能保证元素的顺序。
存储方式不同。HashMap存储的是键值对,键和值可以是不同类型的对象,键用于检索值,每个键都必须是唯一的。而HashSet只存储元素本身,不存储任何与之关联的值,因此只能保证元素的唯一性。
关于性能特点,HashMap和HashSet都提供了快速的查找、插入和删除操作,时间复杂度通常是O(1),但在最坏的情况下(如哈希冲突严重时)可能会退化到O(n)。由于HashSet底层基于HashMap实现,其性能也受到HashMap性能的影响。
使用场景的不同。HashMap适用于需要通过键值对来存储和访问元素的场景,如缓存、配置参数的存储等。而HashSet适用于只需要存储元素值并且需要保证元素的唯一性的场景,如去除重复元素、快速查找元素是否存在等。
20、TreeSet常用方法有哪些?
TreeSet是Java集合框架中的一个类,基于红黑树实现,提供了排序和集合操作的功能。 TreeSet的特点包括有序性和不重复性,默认情况下元素按升序排列。它内部使用红黑树数据结构来存储元素,确保基本操作的时间复杂度为O(log n)12。
TreeSet的常用方法包括:
1. add(Object obj):将一个对象添加到TreeSet中;
2. remove(Object obj):从TreeSet中移除一个对象;
3. pollFirst():返回TreeSet中的第一个对象,如果TreeSet为空则返回null;
4. pollLast():返回TreeSet中的最后一个对象,如果TreeSet为空则返回null;
5. size():返回TreeSet中元素的个数;
6. isEmpty():判断TreeSet是否为空;
7. contains(Object obj):判断一个对象是否在TreeSet中;
8. addAll(Collection<? extends E> c):将一个Collection对象中的元素添加到TreeSet中;
9. removeAll(Collection<? extends E> c):从TreeSet中移除一个Collection对象中的元素;
10. retainAll(Collection<? extends E> c):保留一个Collection对象中的元素,并将它们添加到TreeSet中;