一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅知识面试题,也是你 Java 知识点的扫盲贴。
回答基于以下几个原因。缓存哈希码String内部缓存了其哈希码,第一次调用hashCode()时计算哈希码,之后就直接返回缓存的值。这就意味着当我们使用String作为HashMap的key时,计算哈希码的开销只发生一次,这提高了使用String作为键时的性能。/**Cachethehashcodeforthestring*/privateinthash;//Defaultto0String是不变得Java中的String是不可变的,意味着一旦创建,String对象的内容就不能被改变。当我们使用一个对象作为HashMap的key时,其哈希码必须保持不变,否则就会找不到键值对了,而String是不变的,其哈希码也不会改变,这保证了key的一致性。更多阅读<<<<<String为什么要被设计成不可变的?String是如何实现不可变的?
COW,Copy-On-Write,即写时复制,是在多线程环境下实现线程安全的技术。它的核心思想是当线程尝试修改数据时,不直接在原有的数据上进行修改操作,而是先将原有数据复制一份,在副本上进行修改,修改完成后再将原数据的引用指向新修改过的副本。这样,读操作可以在不加锁的情况下并发进行,因为它们访问的是“不变”的集合。由于读操作无需加锁,所以大大提高了读并发的性能。写采用加锁和复制的技术,保证了线程安全。COW在每次写操作时都要复制整个数据,所以会加大内存的开销。COW采用牺牲写操作的性能和增加内存开销来保证线程安全和提高读操作性能,所以COW一般适用于读操作大大多于写操作的并发场景。在Java采用COW的技术有CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWriteArrayList底层原理是什么?
HashMap的迭代方式一般有如下几种:使用for-each循环遍历keySet()使用for-each循环遍历entrySet()使用Iterator遍历keySet()使用Iterator遍历entrySet()使用Lambda表达式使用StreamsAPI单线程使用StreamsAPI多线程详情基础数据privateMap<Integer,String>hashMap=newHashMap<>(){{put(1,"死磕");put(2,"死磕Java");put(3,"死磕Spring");put(4,"死磕Redis");put(5,"死磕JavaNIO");put(6,"死磕Netty");put(7,"死磕Java新特性&qu
回答HashMap是线程不安全的,主要体现在如下几个方面(Java8)。数据覆盖在调用put()方法时,如果两个线程同时调用put()方法,且两个线程在同一个桶位置插入数据,这个一个线程的数据可能会被另一个线程给覆盖掉。线程A执行完代码①,然后挂起。线程B执行完①、②后,继续执行下面代码。线程A继续执行②,这个时候线程A的数据会覆盖掉线程B的数据数据丢失数据丢失可能会体现在多个地方,这里大明哥就介绍两个地方。一、扩容过程中的元素迁移导致的数据丢失在迁移元素到新的数组中时,原始数组中的链表会重新打散,拆分为两个链表,低位链表保留在原始索引位置,高位链表要迁移到新的索引位置。这个过程要遍历旧链表中的每一个节点进行重分配。如果此时有其他线程尝试插入新元素,而resize()操作尚未完成,可能会导致在resize()过程中新插入的数据被遗漏。put()部分源码resize()部分源码比如线程A在执
回答当调用put()向HashMap中添加元素时,若当前数据量超过容量阈值(当前容量与加载因子的乘积)时,HashMap调用进行扩容操作。扩容操作分为2步。第一步:扩容,新容量=旧容量*2,同时新建一个tab数组,数组大小等于新容量大小第二步:节点迁移。整个迁移过程,HashMap没有采取重新计算节点的hash值来确定节点在新数组中的常规的方式,而是采取如下另辟蹊径的方式:用节点的hash值与旧容量值进行与运算(&),等于0则保持在原来的位置不变(即低位链表),不等于0则迁移到tab[i+oldCap]的位置(高位链表)。如果是红黑树,也是先构建高低两个链表,再来判断链表是否需要转换为红黑树,当链表节点数大于6时则继续保持红黑树结构。详情调用put()向HashMap中添加元素时,若当前数据量超过容量阈值(当前容量与加载因子的乘积)时,HashMap调用进行扩容操作:publicV
HashMap的负载因子与HashMap的扩容机制密切相关,它是衡量哈希表在其容量自动增加之前可以达到多满的一个度量值。若当前容器的容量,达到了我们设定的最大值(容量*负载因子)时,容器就要执行自动扩容操作。HashMap的负载因子默认值为0.75,这个值是基于空间和时间成本之间的一种折中选择。若设置的负载因子大于0.75,好处就是空闲利用率更加高了,能够容纳更多元素了,但是它会增加hash冲突的概率,导致效率会降低。同时,容量更多,说明扩容的代价会更大。若设置的负载因子小于0.75,意味着可以减少hash冲突的概率,但是它会导致HashMap的空间利用率更低了,会增加扩容操作的频率,从而在插入新元素时导致性能下降。同时,在HashMap的源码中也有这样一段描述:总得来说,负载因子是0.75的时候,HashMap空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的
回答HashMap的容量被设计为2^n,主要有如下几个优势:位运算效率:与使用取模(%)操作相比,使用位运算来计算索引位置更加高效。当容量是2^n时,计算索引的公式可以从(hash%capacity)简化为(hash&(capacity-1)),这个操作仅涉及位与运算,比取模操作更快。元素分布更加均匀:利用哈希码的低位直接作为数组索引,确保了元素能够被均匀分布在整个HashMap中,从而在理想情况下减少了哈希冲突,提高了HashMap的整体性能。扩容性能更佳:HashMap的初始容量是2^n,扩容也是以2倍的形式进行扩容,这样在进行扩容重新分布元素时,我们只需要对参与计算的最高位进行检测,如果为1就向高位移动2^(n-1)位,为0就保持不动。无需重新计算所有key的hash值再来重新分布。详解HashMap的默认容量是1<<4,也就是2^4,也就是16。当我们指定容量大
回答HashMap实现Map接口,底层是基于哈希表实现的,是以key-value键值对的形式存在。哈希表是一个数组和链表结合的数据结构,数组用于快速访问,链表用于解决哈希冲突(链地址法),但这是jdk1.8之前的。在JDK1.8之后,当链表长度大于阈值(默认8)并且当前数组的长度大于64时,链表会转换为红黑树,这样做的目的是提高查询效率。详解HashMap的底层数据结构如下:table数组:HashMap的核心是一个动态数组,每个数组的位置称为“桶”(Bucket)。每个桶负责存储键值对(Node)。链表:当不同的key通过哈希函数得到相同的数组索引时,会发生哈希冲突。HashMap通过链表将冲突的键值对连接起来,每个桶的头节点存储在数组的对应位置。红黑树:从Java8开始,为了优化在高哈希冲突下的性能,提高查询性能,当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树。Node:每个
回答在使用hash表时,hash冲突是一个非常常见的问题,该问题出现的主要原因是两个不同的输入值,通过hash函数计算得到了相同的hash值,尝试存储在hash表的同一个位置。解决hash冲突主要有如下几种方式:链地址法:解决hash冲突最经典的方法。它是通过将具有相同hash值的所有元素存储在同一个索引位置的链表中来解决冲突的。开放定址法:在hash表的数组本身中找到一个空闲的槽位来存储冲突的元素。它的核心思想是:当发生hash冲突时,按照某种探测技术来探测下一个空闲的槽位。再hash法:依赖多个hash函数来寻找空闲槽位,其思想是:当第一个hash函数h1(x)导致冲突时,系统将尝试第二个hash函数h2(x),如果仍然冲突,将继续尝试第三个hash函数h3(x),依此类推,直到找到一个空闲槽位为止。详解链地址法链地址法是解决hash冲突最经典方法,Java中的HashMap使用的就是
HashMap和HashTable都是Java集合中用于存储键值对的数据结构,他们的主要区别有如下几个。一、同步性HashMap是非同步的,多线程环境下是线程不安全的。这使得它在单线程环境下运行相比HashTable性能高一些。如果需要在多线程环境中使用HashMap,我们可以通过Collections.synchronizedMap()方法来包装HashMap使之同步,或者使用ConcurrentHashMap。HashTable是同步的,这意味着它是线程安全的,它是所有公共方法都使用了同步锁,这确保了HashTable在并发环境下的线程安全。但这也意味着它在性能上比非同步的HashMap性能会慢点。二、空值处理HashMap允许一条记录的key和多条记录的value为null。HashTable不允许key或value为null,否则会抛出NullPointerException。三、
ArrayList、LinkedList和Vector都是实现List接口的集合。他们在内部实现和功能特性上面存在一些差异。一、内部实现差异ArrayList:底层是使用动态数组实现的。这说明ArrayList支持快速的随机访问和快速的遍历,但是对于中间的插入和删除操作可能会比较慢,因为这涉及到数组的复制和移动。LinkedList:底层采用双向链表实现。这使得它在添加和删除元素时特别高效,但是对随机的访问则效率就比较低了,因为需要从头节点开始遍历。Vector:底层也是采用动态数组实现的,但它是线程安全的,它的所有的公共方法都使用了同步,所以效率相比ArrayList会慢点。二、性能ArrayList:随机访问性能最好,中间插入和删除操作性能不好。LinkedList:在添加和删除元素时特别高效,特别是在列表的开头和结尾,随机访问性能较差。Vector:随机访问相比ArrayList会差
回答因为ArrayList的subList()返回的是原始列表的一个子列表视图,这个特性带来了一些便利,但带来的坑就更加多了:互相影响:任何对原始列表或者子列表进行的非结构性改变都会互相影响。反向影响:任何对子列表的结构修改都会影响到原始列表。结构性修改的限制:如果原始列表进行了结构性的修改(添加、删除元素),那么子列表将变得不可用,使用子元素会抛出ConcurrentModificationException异常。内存泄漏:子列表持有对原始列表的引用,这意味着即使原始列表的其他部分不再需要,只要子列表还在使用,原始列表就不能被垃圾回收。在某些情况下,这可能导致意外的内存泄漏,尤其是在原始列表很大,而子列表只是很小一部分时。详解互相影响任何对原始列表或者子列表进行的非结构性改变都会互相影响。@TestpublicvoidsubListTest(){List<String>lis
回答ArrayList在添加元素时,如果内部数组已满,则会自动执行扩容操作,通常是将当前容量增加到原来的1.5倍。共分为如下几个步骤:计算新容量:一般新容量=旧容量*1.5,也就是增长50%。创建新数组:ArrayList新建一个新的数组,其容量为计算得出的新容量。元素复制:使用System.arraycopy()将旧数组中的所有元素复制到新数组中。引用更新:复制完成后,ArrayList的内部引用会从旧数组更新到新数组,旧数组随后会被垃圾回收处理。由于扩容会涉及数组元素的复制,代价较大,所以在添加大量元素之前预估并指定一个合理的初始容量,可以减少扩容操作的次数,从而优化性能。详解ArrayList底层是基于数组实现的列表数据结构,它提供了动态数组的功能。当元素数量超过数组当前容量时,ArrayList会自动增加存储容量以容纳更多元素。这个过程通常称为“扩容”。在调用add()的时候,Ar
回答Set接口是通过元素之间的比较来保证元素唯一性的,不同的实现类比较的方式不一样:HashSet和LinkedHashSet是基于哈希表实现的。他们是通过元素的hashCode()与equals()来保证的,只有当hashCode()相同且equals()方法返回true时,两个对象才被视为相同的,这样来保证元素的唯一性。TreeSet是基于红黑树实现的。它是通过元素实现的Comparable接口或者在新建TreeSet时传入的Comparator来保证元素的唯一性。详解在Java中Set接口是一个不包含重复元素的集合,它主要有两类实现:HashSet和LinkedHashSet是基于哈希表实现的。TreeSet是基于红黑树实现的。他们是通过如下方式来保证元素的唯一性的。HashSetHashSet使用哈希表来存储元素,底层是基于HashMap来实现的。当向HashSet中添加元素时,它
回答Java对List集合有如下几种排序方式:使用Collections.sort():该方法提供了两种排序方式,一种是自然排序,一种是使用Comparator比较器进行排序。使用List.sort():Java8,在List接口中提供了sort(Comparator<?superE>c),允许我们直接对列表排序。使用StreamAPI:利用Stream的sorted()可以对List中的元素进行排序。详解使用Collections.sort()Collections.sort()有两个重载的方法。自然排序publicstatic<TextendsComparable<?superT>>voidsort(List<T>list)该方法对列表中的元素按照它们的自然顺序进行排序,它要求列表中的元素实现了Comparable接口,并且相互之间可以进
fail-safe机制是Java集合中一种错误处理机制,它允许集合在迭代过程中结构被修改而不会抛出ConcurrentModificationException异常。这与fail-fast机制是相反的,后者在检测到集合在迭代过程中,如果结构被修改时会立即抛出ConcurrentModificationException异常。fail-safe机制通过使用并发集合实现,如java.util.concurrent包中提供的集合(ConcurrentHashMap),或通过创建集合的副本来迭代(如CopyOnWriteArrayList、CopyOnWriteArraySet),从而避免修改原始集合时产生并发问题。它提供了一种安全的方式来处理并发修改,比较适合多线程场景。ConcurrentHashMap:一个高效的并发HashMap,使用分段锁技术来管理对不同段的并发访问,允许多个读取操作和写
回答fail-fast即快速失败机制,是Java集合中一种错误检测机制,旨在尽早地发现并发修改异常。当对一个集合进行迭代时,如果该集合在迭代过程中,其结构发生了改变(添加、删除等操作),就会发生fail-fast,即迭代器会立刻抛出ConcurrentModificationException异常,从而防止不一致的行为。其工作原理是基于对集合结构修改次数的检测,当创建迭代器时,它会记住集合的结构修改次数,在迭代过程中,如果迭代器检测到集合的结构修改次数与它初始化时记录的次数不一致时,就会抛出ConcurrentModificationException。详解演示fail-fast机制下面用一个简单的例子来演示下fail-fast机制:@TestpublicvoidfailFastTest(){List<String>skList=newArrayList<>();s