文章目录
- 一、hash冲突后,将元素放到链表头还是尾?
- 二、HashMap哪些操作会导致扩容? 扩容机制?resize方法的执行过程?扩容后的位置计算?为什么负载因子是0.75?
- 三、jdk1.8 对 HashMap 主要做了哪些优化呢?为什么?
- 四、HashMap 为什么不是线程安全的?
- 五、如何解决HashMap线程不安全的问题呢?
- 六、HashMap 内部节点是有序的吗?
- 七、LinkedHashMap怎么实现有序的?
- 八、TreeMap怎么实现有序的?
- 九、TreeMap 和 HashMap 的区别
- 十、HashSet底层实现?
- 十一、HashMap与Hashtable区别
- 十二、HashMap和ConcurrentHashMap的区别?
- 十三、ConcurrentHashMap和Hashtable区别?
- 十四、有哪些线程不安全的集合?高并发情况下如何保证线程安全?√
- 十五、集合如何排序?
- 十六、set实现类HashSet、LinkedHashSet、TreeSet的数据结构以及各自有优点
- 十七、Comparable和Comparator区别?
- 十八、集合类使用注意事项
- Array与ArrayList区别与选用?
- 总结
一、hash冲突后,将元素放到链表头还是尾?
- JDK1.7插入时采用头插法,多线程下,有链表闭环的bug。假设链表原来的元素是元素的顺序是C->B->A,此时线程1和2指向指向C,线程1和2.next指向B,当hashMap扩容并且线程2完成插入,此时链表的状态为A->B->C,此时线程1指向C,线程1.next指向B,此时线程1想要变量就变成C->B->C了,因为此时B.next->C
- JDK1.8改成了尾插法,主要是为了减少线程安全的问题,转成红黑树后按照红黑树的规则来插了
二、HashMap哪些操作会导致扩容? 扩容机制?resize方法的执行过程?扩容后的位置计算?为什么负载因子是0.75?
- 为了减少哈希冲突发生的概率,当HashMap元素个数达到一个临界值threshold的时候,就会触发扩容,是一个相当耗时的操作。
- 扩容时机
- 第一次调用HashMap的put方法且数组为null时,会调用resize方法对table数组进行初始化,默认大小为16。
- 当hashMap元素个数大于扩容阈值threshold = 负载因子loadFactor(0.75) * 初始容量capacity(16)时。容量变为原来的2倍,先插入数据再扩容
- 容量范围:16-2^30个
- 加载因子过高,扩容频率变低,hash碰撞几率变大,查找时间长,但占用空间小,空间利用率变高
- 加载因子过低,扩容频率变高,hash碰撞几率变低,查找时间短,但占用空间大,空间利用率变低
- 扩容机制:扩容时,HashMap 会创建一个新的数组,其容量是原数组容量的两倍。然后将键值对放到新计算出的索引位置上。根据(e.hash & oldCap)是否为0,使得扩容后的位置=原位置 or 原位置 + 旧容量(原哈希值的高位中新增的那一位是否为1,因为位置计算实际上是保留低位值,去掉所有高位值,比如原容量16则保留4位低位,扩容后32为保留5位低位,相差1位,而这1位刚好是原容量16所在的位置,因此只需hash与原容量与操作得到最新的位是1还是0决定新元素的位置,上面的推论都得益于长度是2的倍数和hash的高低位运算)
三、jdk1.8 对 HashMap 主要做了哪些优化呢?为什么?
- 优化了数据结构:在数组+链表的基础上改为数组+链表+红黑树
- 优化了链表插入的方式:jdk1.7是头插法,jdk1.8尾插法,主要是因为头插法有闭环的bug
- 优化了扩容的处理:jdk1.7需要重新执行hash函数重新放置数据的位置,而jdk1.8只需判断hash值与旧容量的与操作是否为0即可,为0则是原位置,否则就是原位置+旧容量
- 优化了扩容机制:jdk1.7的先判断是否需要扩容,再插入数据,jdk1.8则是先插入数据再扩容
- 优化了hash函数:jdk1.7经过4个移位和亦或,而jdk1.8只需高低16为亦或就行
四、HashMap 为什么不是线程安全的?
- jdk1.7在多线程下因为头插法形成闭环,因此扩容可能会死循环。
- 多线程put可能会导致元素的丢失。因为计算出来的位置可能会被其他线程的put覆盖,本来哈希冲突是应该用链表的,但多线程时由于没有加锁,相同位置的元素可能就被干掉了。
- put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出阈值而导致出现扩容,线程2此时执行get,就有可能出现这个问题,因为线程1执行完table = newTab后,线程2中的table此时也发生了变化,此时去get时当然会get到null,因为元素还没有转移。
五、如何解决HashMap线程不安全的问题呢?
- Collections.synchronizedMap
- ConcurrentHashMap
六、HashMap 内部节点是有序的吗?
- 无序,根据hash值排序
七、LinkedHashMap怎么实现有序的?
- 节点Node有前后节点的指针
八、TreeMap怎么实现有序的?
- TreeMap通过Comparable接口或者key来排序,底层是红黑树
九、TreeMap 和 HashMap 的区别
- HashMap是数组+链表+红黑树
- TreeMap是红黑树
十、HashSet底层实现?
- HashSet底层基于HashMap实现(除了clone()、writeObject()、readObject()是HashSet⾃⼰实现之外,其他⽅法都是直接调⽤HashMap中的⽅法。HashSet 会自动去重,因为HashMap 的键是唯一的(哈希值),相同键的值会覆盖掉原来的值,
十一、HashMap与Hashtable区别
- HashMap可以接收null键和值。当key为null时返回的值为0,Hashtable不能接受null键和值,会抛出空指针异常
- HashMap线程不安全,Hashtable线程安全
- HashMap默认大小是16,扩容每次为2的指数大小,HashTable中数组默认大小是11 ,扩容方法是old*2 + 1
十二、HashMap和ConcurrentHashMap的区别?
- ConcurrentHashMap线程安全;对整个桶数组进行了分割分段(Segment),每一个分段上都用lock锁保护,ConCurrentHashMap不允许键值对null
- HashMap线程不安全;HashMap的键值对允许有null
十三、ConcurrentHashMap和Hashtable区别?
- ConcurrentHashMap和Hashtable区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: ConcurrentHashMap和hashMap一样。Hashtable和JDK1.7的HashMap一样
- 实现线程安全的方式:
- ConcurrentHashMap:(JDK1.7分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment) JDK1.8,使用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap锁的方式是稍微细粒度的
- Hashtable(同一把锁) :使用synchronized保证线程安全,效率低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态
十四、有哪些线程不安全的集合?高并发情况下如何保证线程安全?√
- Vector、HashTable、Properties是线程安全的;
- ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
- 使用线程安全的类,使用synchronized关键字等
- 普通集合变为同步集合的工具方法Collections.synchronizedList(<? extends Collection> collection);
十五、集合如何排序?
- 排序:实现Comparator接口compare(T o1,T o2)小于、等于或者大于o2分别返回负整数、0或者正整数
十六、set实现类HashSet、LinkedHashSet、TreeSet的数据结构以及各自有优点
- HashSet:无序,唯一,底层采用HashMap来保存元素(数组机制)
- LinkedHashSet:有序,唯一,基于 LinkedHashMap 实现 链表和哈希表组合
- TreeSet:有序,唯一,红黑树(自平衡的排序二叉树)
十七、Comparable和Comparator区别?
- Comparable接口用于当前对象和其它对象的比较,compareTo(Object obj) 方法用来排序,在比较类上修改
- Comparator接口用于传入的两个对象的比较,compare(Object obj1, Object obj2) 方法用来排序,新增一个类专门用于比较
十八、集合类使用注意事项
- 基于应用的需求来选择使用正确类型的集合。如果元素的大小是固定的且已知优先级此时使用Array,而不是ArrayList
- 指定初始大小避免重复扩容
- 使用泛型来保证类型安全,可靠性和健壮性
- Map中尽量使用不可变类String作为一个key,可避免hashcode的实现和我们自定义类的equals方法
- 返回零长度的集合或者数组,而不是返回一个null ,这样可以防止底层集合是空的
Array与ArrayList区别与选用?
- Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
- Array是指定大小的,而ArrayList大小是固定的,可自动扩容。
- 列表的大小已经指定且存储和遍历推荐使用Array
- 多维数组推荐使用Array[][]
总结
本文介绍了java面试-java集合(下),如有问题欢迎私信和评论