java集合
介绍java中的集合框架 +2
Java 集合, 也叫作容器,主要是由两大接口派生而来:
一个是 Collection接口,主要用于存放单一元素;
另一个是 Map 接口,主要用于存放键值对。
对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。
collection接口有哪些实现类,list,set,map底下的常用集合,以及性能
- List
- ArrayList:基于数组实现,随机访问速度快,适合随机读写操作,但在删除、插入等操作时效率相对较低。
- LinkedList:基于双向链表实现,插入、删除操作效率较高,但随机访问效率较低。
- Set
- HashSet:基于哈希表实现,不允许元素重复,插入、删除操作效率较高,但元素的顺序不是固定的。
- TreeSet:基于红黑树实现,元素有序,但效率相对较低。
- Map
- HashMap:基于哈希表实现,键值对无序,插入、删除操作效率较高,但元素的顺序不是固定的。
- TreeMap:基于红黑树实现,键值对有序,但效率相对较低。
List/Set
集合是否能同时遍历和修改?这里回答了会抛出异常 可以用迭代器遍历
在使用迭代器遍历集合时,不要直接使用集合提供的 add/remove 方法修改集合,会引发并发修改异常(ConcurrentModificationException)。如果需要修改集合,应该使用迭代器的 remove 方法。
Java 8 中的 Stream API 提供了一种可以同时遍历和修改集合的方式。可以通过 filter、map、reduce 等方法对集合元素进行处理,最终生成一个新的集合。这种方式不会直接修改原有集合,因此不会引发并发修改异常。
List
arraylist和数组有什么区别
- 大小可变性:ArrayList 的大小是可以动态变化的,而数组一旦被创建,大小就是固定的,无法改变。
- 级别不同:ArrayList 是集合类的一种,提供了许多方便的操作方法,而数组是一种数据结构。
- 对象类型:ArrayList 可以存储任意类型的对象,而数组只能存储相同类型的数据。
ArrayList和LinkedList的区别 +3
(1)数据结构不同
- ArrayList基于数组实现
- LinkedList基于双向链表实现
(2) 多数情况下,ArrayList更利于查找,LinkedList更利于增删
- ArrayList基于数组实现,get(int index)可以直接通过数组下标获取,时间复杂度是O(1);LinkedList基于链表实现,get(int index)需要遍历链表,时间复杂度是O(n);当然,get(E element)这种查找,两种集合都需要遍历,时间复杂度都是O(n)。
- ArrayList增删如果是数组末尾的位置,直接插入或者删除就可以了,但是如果插入中间的位置,就需要把插入位置后的元素都向前或者向后移动,甚至还有可能触发扩容;双向链表的插入和删除只需要改变前驱节点、后继节点和插入节点的指向就行了,不需要移动元素。
**(3)**是否支持随机访问
- ArrayList基于数组,所以它可以根据下标查找,支持随机访问,当然,它也实现了RandmoAccess 接口,这个接口只是用来标识是否支持随机访问。
- LinkedList基于链表,所以它没法根据序号直接获取元素,它没有实现RandmoAccess 接口,标记不支持随机访问。
**(4)**内存占用,ArrayList基于数组,是一块连续的内存空间,LinkedList基于链表,内存空间不连续,它们在空间占用上都有一些额外的消耗:
- ArrayList是预先定义好的数组,可能会有空的内存空间,存在一定空间浪费
- LinkedList每个节点,需要存储前驱和后继,所以每个节点会占用更多的空间
ArrayList的扩容机制了解吗?
插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。
ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。
你讲讲ArrayList的几种遍历方式?有什么问题
-
for循环遍历:使用普通的for循环可以遍历ArrayList,代码如下:
cssCopy codefor(int i=0; i<list.size(); i++) { // 访问list.get(i) } -
foreach循环遍历:使用foreach语法糖可以遍历ArrayList,代码如下:
scssCopy codefor(Object obj: list) { // 访问obj } -
迭代器遍历:使用迭代器可以遍历ArrayList,代码如下:
vbnetCopy codeIterator iterator = list.iterator(); while(iterator.hasNext()) { Object obj = iterator.next(); // 访问obj }
其中,第一种for循环遍历是最常见的,但在删除元素时需要注意,如果直接使用普通的for循环进行删除操作,则会出现数组下标越界的问题;第二种foreach循环和第三种迭代器遍历则不会有这个问题。但是,在遍历过程中如果对集合进行增删操作,都会引起ConcurrentModificationException异常,因此需要使用Iterator的remove()方法进行删除。
Map
.hashmap的put流程
10.concurrenthashmap1.8后的优化
遍历集合过程中可以删除元素么?怎么解决的
为什么采用头插法?
头插法和尾插法是两种常见的链表插入方式。相比于尾插法,头插法在插入节点时不需要遍历整个链表找到最后一个节点,因此插入操作的时间复杂度为 O(1)。而尾插法则需要遍历整个链表,时间复杂度为 O(n)。因此,对于频繁插入的场景,采用头插法能够更加高效。而对于频繁删除的场景,则可以采用尾插法。
HashMap中JDK1.6/1.7的区别(√)
JDK8中HashMap的底层实现相比于JDK7做了哪些优化?
-
数据结构:数组 + 链表改成了数组 + 链表或红黑树
原因:发生 hash 冲突,元素会存入链表,链表过长转为红黑树,将时间复杂度由O(n)降为O(logn) -
链表插入方式:链表的插入方式从头插法改成了尾插法
简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后。
原因:因为 1.7 头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环。 -
扩容rehash:扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,不需要重新通过哈希函数计算位置,新的位置不变或索引 + 新增容量大小。
原因:提高扩容的效率,更快地扩容。 -
扩容时机:在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;
-
散列函数:1.7 做了四次移位和四次异或,jdk1.8只做一次。
原因:做 4 次的话,边际效用也不大,改为一次,提升效率。
红黑树的特点?HashMap中为什么使用红黑树?
红黑树是一种自平衡二叉查找树,具有以下特点:
- 每个节点要么是黑色,要么是红色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,空节点)是黑色。
- 如果一个节点是红色,则它的两个子节点都是黑色。
- 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
HashMap中使用红黑树是为了解决哈希冲突导致链表过长的问题。在JDK8之前,HashMap的底层实现是数组+链表。当哈希冲突比较严重时,会导致链表过长,从而影响查询效率。为了解决这个问题,JDK8中使用了红黑树来代替链表,当链表长度超过一定阈值时(默认为8),链表就会转换为红黑树。这样可以保证HashMap的查询、插入和删除操作都可以在O(log n)的时间内完成,大大提高了HashMap的性能。
平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,平衡二叉树保持平衡的效率更低。
hashmap底层实现
JDK1.7的数据结构是数组+链表
JDK1.8的数据结构是数组+链表+红黑树
- 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8&数组大小>=64,链表转为红黑树
- 如果红黑树节点个数<6 ,转为链表
HashMap线程安全吗,为什么不安全 +2
非线程安全,其底层实现使用了数组和链表或红黑树的结构,
在多线程环境下,如果两个线程同时进行插入或者删除操作,可能会导致链表或红黑树的结构被破坏,进而引发一系列错误。
举个例子,当两个线程同时调用put方法,且要插入的元素的哈希值相同,那么它们就可能会同时在数组的同一个位置上插入元素,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。
因此,如果需要在多线程环境下使用HashMap,需要采取一些措施来保证其线程安全性,比如使用ConcurrentHashMap或者在代码中使用synchronized关键字等。
ConcurrentHashMap怎么保证线程安全的?
concurrentHashMap 的底层实现 +6
ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAS+synchronized实现。
从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承于ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。
分段锁具体底层怎么实现的
后他问hashmap还有什么值得注意的地方
- 线程不安全:HashMap不是线程安全的,多线程环境下需要考虑并发访问的问题。
- 初始容量和负载因子:HashMap的构造方法中可以指定初始容量和负载因子。如果不指定,默认初始容量是16,负载因子是0.75。初始容量指的是HashMap中桶的数量,负载因子是指HashMap在达到容量阈值之前可以允许的最大负载因子。当HashMap中元素的数量超过容量阈值(初始容量 * 负载因子)时,HashMap会进行扩容。
- 键的哈希值和equals方法:HashMap中键的哈希值和equals方法决定了键值对的存储和查找方式。在使用自定义对象作为HashMap的键时,需要注意实现对象的hashCode方法和equals方法,保证hashCode方法返回的哈希值相同的对象在equals方法比较时也应该相等。
- 链表转化为红黑树:当HashMap中的链表长度达到8时,链表就会转化为红黑树,以提高查找的效率。但是,当红黑树中的节点数量小于6时,红黑树会转化回链表。这个转化的过程会造成一定的性能损失。
- 迭代器的快速失败:在迭代HashMap时,如果在迭代过程中HashMap被修改了,就会抛出ConcurrentModificationException异常,从而保证了迭代器的快速失败。
Map的哪种遍历方式效率比较高?
在使用 Map 进行遍历时,推荐使用 entrySet() 方法返回的集合进行遍历,因为 entrySet() 方法返回的是 Map.Entry<K,V> 的集合,包含了键值对中的键和值,因此在遍历时不需要通过键再去获取值,可以提高效率。下面是使用 entrySet() 进行遍历的示例代码:
goCopy codeMap<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 使用 entrySet() 方法遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
另外,如果只需要遍历 Map 中的键或者值,也可以使用 keySet() 或者 values() 方法返回的集合进行遍历,但需要注意,在遍历过程中需要通过键或值去获取对应的值或键,可能会影响效率。
为什么hashmap后面链表>8会自动转换成红黑树
当元素数量较多时,链表的查询效率会下降,而红黑树的查询效率要更高一些,这样就能更快地查找元素。另外,当桶中元素数量小于 8 个时,使用链表结构可以更快地进行操作,避免了建立红黑树的开销。因此,HashMap 采用了这种自适应的存储方式,能够在不同的元素数量范围内采用最优的存储方式。
Set
HashSet底层
HashSet 底层就是基于 HashMap 实现的。
HashSet的add方法,直接调用HashMap的put方法,将添加的元素作为key,new一个Object作为value,直接调用HashMap的put方法,它会根据返回值是否为空来判断是否插入元素成功。
JAVA中常用的数据结构说几种
- 数组(Array):一种线性结构,用于存储一组相同数据类型的元素。
- 链表(Linked List):一种非线性结构,由节点组成,每个节点包含一个数据项和指向下一个节点的指针。
- 栈(Stack):一种线性结构,具有后进先出的特点。
- 队列(Queue):一种线性结构,具有先进先出的特点。
- 堆(Heap):一种完全二叉树结构,具有最大值或最小值总是在堆的根节点的特点。
- 树(Tree):一种非线性结构,由节点组成,每个节点最多有两个子节点。
- 图(Graph):一种非线性结构,由节点和边组成,用于表示不同节点之间的关系。
- 散列表(Hash Table):一种以键值对形式存储数据的数据结构,通过哈希函数计算键的索引,实现高效的数据查找和插入。
对于解决哈希冲突来说,常见的方法
那Java中用来解决hash冲突的方法是什么,底层的实现细节(关于为什么是8,我说的是因为泊松分布,面试官说其实按魔法数理解会合适一些)
对于解决哈希冲突,常见的方法包括:
- 链地址法(Separate Chaining):每个桶都存储一个链表,哈希值相同的元素会被放到同一个桶中,如果发生哈希冲突,则将新元素插入到链表中。这是一种简单的方法,但是需要维护链表,而且当链表过长时,会影响查询效率。
- 开放地址法(Open Addressing):当发生哈希冲突时,通过某种规则在散列表中寻找下一个空槽来存储元素,具体规则包括:线性探测(Linear Probing)、二次探测(Quadratic Probing)和双重哈希(Double Hashing)等。这种方法不需要维护链表,而且可以充分利用空间,但是当散列表元素较多时,探测时间会增加。
- 建立公共溢出区:当哈希冲突发生时,将元素存储到一个公共溢出区中,通过一个指针链表将所有溢出的元素连接起来。这种方法相对简单,但需要一个额外的空间来存储溢出元素。
在 Java 中,HashMap 采用的是链地址法,当链表长度大于一定阈值时,会将链表转化为红黑树。同时,JDK 1.8 中也增加了一种新的解决哈希冲突的方法,称为尝试插入当前元素,如果插入失败就重新计算 hash 值并重新插入的“链表 + 红黑树 + 原地扩容”算法,进一步提高了 HashMap 的性能。