Java常见面试知识点汇总

Posted DreamMakers

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java常见面试知识点汇总相关的知识,希望对你有一定的参考价值。

提问:LinkedHashMap与TreeMap的实现原理?两者怎么保证有序性的?两者有什么区别?

LinkedHashMap和TreeMap都是哈希表结构的具体实现。

(a)LinkedHashMap的实现原理

LinkedHashMap类继承自HashMap,哈希表的内部存取基本和HashMap是一样的。

在LinkedHashMap内部,维护了链表的首尾引用,分别为head和tail,并且定义了一个accessOrder变量,用于控制是根据插入顺序排序还是根据读取顺序排序。

当accessOrder为false时,表示根据插入顺序排序;当accessOrder为true时,表示根据获取顺序排序;

如果使用不带accessOrder的构造函数,默认accessOrder=false,即根据元素的插入顺序进行排序。

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

在插入元素时,LinkedHashMap会使用首尾指针去维护新插入的元素,将当前新增节点放到tail节点的后面;

如果在构造对象时传入了accessOrder=true,则表示根据获取顺序排序。此时,如果在调用get()方法前只有插入,那么链表维护的顺序即是元素插入的顺序,也可以理解为就是元素被访问的顺序。如果通过get()方法获取了元素,则在获取该元素后会调用afterNodeAccess()方法,该将该元素顺序调整到链表的尾部,从而实现在遍历链表时是根据访问顺序排序,最后访问的元素永远在链表的尾部。

public V get(Object key)

Node<K,V> e;

if ((e = getNode(hash(key), key)) == null)

    return null;

if (accessOrder)

    afterNodeAccess(e);

return e.value;

(2)TreeMap的实现原理

TreeMap底层是基于红黑树数据结构实现的,所以时间复杂度在log(n)。

在构造TreeMap对象时,可以传入比较器,也可以不传入。如果不传入,则默认按照键key的自然排序规则(应该是按照ASCII的顺序)进行排序;

如果在构造方法中传入了Comparator对象,则以Comparator对象的方法进行比较;否则,以Comparable的compareTo()方法进行比较,此时要求保存在TreeMap中的类型必须实现了Comparable接口。

TreeMap中不允许插入key为null,否则会报错NullPointerException;(以此对应的,HashMap是可以插入key为null的场景,从而HashSet、LinkedHashSet、LinkedHashMap也都允许,并且HashMap还支持value为null),但是value允许传null;

TreeMap是非线程安全的;

提问:HashTable和ConcurrentHashMap的区别?

HashTable的线程安全是通过在操作方法上添加synchronized关键字实现的,而ConcurrentHashMap的线程安全实现方式在不同的JDK版本中是不同的,在JDK1.7中是通过分段锁实现的,而在JDK1.8中是通过在每个数组元素上加锁的,每个元素可能对应的是一个链表或者是一个红黑树。

它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时map需要被锁定很长的时间。因为ConcurrentHashMap引入了分段锁的概念(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分(这是在JDK1.7中的实现方式,在1.8之后没有采用分段锁),而Hashtable则会锁定整个map。

提问:HashMap在JDK1.8中的实现相比于JDK1.7有哪些优化?

(a)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。

为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(b)扩容后数据存储位置的计算方式也不一样:

在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)

而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

(c)JDK1.7版本中HashMap采用数组+链表实现,JDK1.8版本中通过数组+链表+红黑树实现;

(d) JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。

(e)对于待插入的新数据,在JDK1.7中是在扩容后老数据迁移之后再进行新数据的插入,但是在JDK1.8中,是将新数据插入之后再进行扩容,新老数据在扩容后统一迁移处理;

提问:哈希冲突的解决办法有哪些?

解决哈希冲突的三种方法:拉链法、开放地址法、再散列法。

链接地址法的思路是将哈希值相同的元素构成一个单链表,并将单链表的头指针存放在哈希表的第i个单元中。链表法适用于经常进行插入和删除的情况。

开放定址法从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。

在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法。

再散列法,就是在发生冲突的时候使用其他哈希函数进行计算,直到冲突不在发生。

提问:HashSet能不能保存null值?对于null值怎么处理的hashcode?

HashSet可以保存null值,在HashSet内部是使用HashMap数据结构进行维护的,所有的key==null都指向同一个对象。


对于key=null的情况,会特殊处理,调用putForNullKey(value)方法。所有key值为null的都保存在bucketIndex为0的位置。如果发现已经有null保存进去了,则不再重复添加null节点。

备注说明

如果想第一时间获取最新最全的面试知识和面试技巧,欢迎扫码关注微信公众号(IT面试直通车),关于互联网编程与面试,这里一定有你想要的。

目前公众号处于初期内容整理建设阶段,计划半年时间将所有相关知识点都整理汇总,添加关注后可以跟随公众号一起逐渐成长,如对相关问题有疑问,可回复进行探讨,谢谢。

前文回顾

Java常见面试知识点汇总(1)

Java常见面试知识点汇总(2)

以上是关于Java常见面试知识点汇总的主要内容,如果未能解决你的问题,请参考以下文章

Java常见面试知识点汇总

Java常见面试知识点汇总

Java常见面试知识点汇总

Java常见面试知识点汇总

Java常见面试知识点汇总

Java常见面试知识点汇总