java7 道集合⾯试题

Q1:说⼀说 ArrayList

ArrayList 是容量可变的⾮线程安全列表,使⽤数组实现,集合扩容时会创建更⼤的数组,把原有数组复制到新数组。⽀持对元素的快速随机访问,但插⼊与删除速度很慢。ArrayList 实现了 RandomAcess 标记接⼝,如果⼀个类实现了该接⼝,那么表示使⽤索引遍历⽐迭代器更快。

elementDataArrayList 的数据域,被 transient 修饰,序列化时会调⽤ writeObject 写⼊流,反序列化时调⽤ readObject 重新赋值到新对象的 elementData。原因是 elementData 容量通常⼤于实际存储元素的数量,所以只需发送真正有实际值的数组元素。

size 是当前实际⼤⼩,elementData ⼤⼩⼤于等于 size

modCount 记录了 ArrayList 结构性变化的次数,继承⾃ AbstractList。所有涉及结构变化的⽅法都会增加该值。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查modCount 和 expectedModCount 是否相等,不相等就会抛出异常。这种机制叫做 fail-fast,所有集合类都有这种机制。

Q2:说⼀说 LinkedList

LinkedList 本质是双向链表,与 ArrayList 相⽐插⼊和删除速度更快,但随机访问元素很慢。除继承AbstractList 外还实现了 Deque 接⼝,这个接⼝具有队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 类似。

LinkedList 包含三个重要的成员:sizefirst lastsize 是双向链表中节点的个数,first last 分别指向⾸尾节点的引⽤。

LinkedList     的优点在于可以将零散的内存单元通过附加引⽤的⽅式关联起来,形成按链路顺序查找的线性结构,内存利⽤率较⾼。

Q3Set 有什么特点,有哪些实现?

Set 不允许元素重复且⽆序,常⽤实现有 HashSetLinkedHashSet TreeSet

HashSet 通过 HashMap 实现,HashMap Key HashSet 存储的元素,所有 Key 都使⽤相同的Value ,⼀个名为 PRESENT Object 类型常量。使⽤ Key 保证元素唯⼀性,但不保证有序性。由于HashSet 是 HashMap 实现的,因此线程不安全。

HashSet 判断元素是否相同时,对于包装类型直接按值⽐较。对于引⽤类型先⽐较 hashCode 是否相同,不同则代表不是同⼀个对象,相同则继续⽐较 equals,都相同才是同⼀个对象。

LinkedHashSet 继承⾃ HashSet,通过 LinkedHashMap 实现,使⽤双向链表维护元素插⼊顺序。

TreeSet 通过 TreeMap 实现的,添加元素到集合时按照⽐较规则将其插⼊合适的位置,保证插⼊后的集合仍然有序。

Q4TreeMap 有什么特点?

TreeMap 基于红⿊树实现,增删改查的平均和最差时间复杂度均为 O(logn) ,最⼤特点是 Key 有序。

Key 必须实现 Comparable 接⼝或提供的 Comparator ⽐较器,所以 Key 不允许为 null

HashMap 依靠 去重,⽽ TreeMap 依靠 Comparable  Comparator

TreeMap 排序时,如果⽐较器不为空就会优先使⽤⽐较器的 ⽅法,否则使⽤ Key 实现的Comparable TreeMap 通过 和⽅法,两者都不满⾜会抛出异常。实现增加和删除树节点。插⼊新节点的规则有三个:

① 需要调整的新节点总是红⾊的。② 如果插⼊新节点的⽗节点是⿊⾊的,不需要调整。③ 如果插⼊新节点的⽗节点是红⾊的,由于红⿊树不能出现相邻红⾊,进⼊循环判断,通过重新着⾊或左右旋转来调整。

TreeMap 的插⼊操作就是按照 Key 的对⽐往下遍历,⼤于节点值向右查找,⼩于向左查找,先按照⼆叉查找树的特性操作,后续会重新着⾊和旋转,保持红⿊树的特性。

Q5HashMap 有什么特点?

JDK8 之前底层实现是数组 + 链表JDK8 改为数组 + 链表/红⿊树,节点类型从Entry 变更为 Node。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因⼦ loadFactor

table 数组记录 HashMap 的数据,每个下标对应⼀条链表,所有哈希冲突的数据都会被存放到同⼀条链表Node/Entry 节点包含四个成员变量:keyvaluenext 指针和 hash 值。

HashMap 中数据以键值对的形式存在,键对应的 hash 值⽤来计算数组下标,如果两个元素 key 的hash 值⼀样,就会发⽣哈希冲突,被放到同⼀个链表上,为使查询效率尽可能⾼,键的 hash 值要尽可能分散。

HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次⽅、最⼤容量为 1<< 30 、默认加载因⼦为0.75。

Q6HashMap 相关⽅法的源码?

JDK8 之前hash:计算元素 key 的散列值

 处理 String 类型时,调⽤ ⽅法获取 hash 值。

② 处理其他类型数据时,提供⼀个相对于 HashMap 实例唯⼀不变的随机值 hashSeed 作为计算初始量。

③ 执⾏异或和⽆符号右移使 hash 值更加离散,减⼩哈希冲突概率。

indexFor:计算元素下标将 hash 值和数组⻓度-1 进⾏与操作,保证结果不会超过 table 数组范围。

get:获取元素的 value

 如果 key  null,调⽤ ⽅法,如果 size 0 表示链表为空,返回 null。如果 size

不为 0 说明存在链表,遍历 table[0] 链表,如果找到了 key null 的节点则返回其 value,否则返回null。

 如果 key  不为 null,调⽤ ⽅法,如果 size  0 表示链表为空,返回 null 值。如果

size 不为 0,⾸先计算 key hash 值,然后遍历该链表的所有节点,如果节点的 key hash 值都和要查找的元素相同则返回其 Entry 节点。

③ 如果找到了对应的 Entry 节点,调⽤put:添加元素

① 如果 key null,直接存⼊ table[0]

② 如果 key 不为 null,计算 key hash 值。⽅法获取其 value 并返回,否则返回 null

③ 调⽤计算元素存放的下标 i

④ 遍历 table[i] 对应的链表,如果 key 已存在,就更新 value 然后返回旧 value

⑤ 如果 key 不存在,将 modCount 值加 1,使⽤resize:扩容数组⽅法增加⼀个节点并返回 null

① 如果当前容量达到了最⼤容量,将阈值设置为 Integer 最⼤值,之后扩容不再触发。

 否则计算新的容量,将阈值设为 的较⼩值。

③ 创建⼀个容量为 newCapacity Entry 数组,调⽤

transfer:转移元素⽅法将旧数组的元素转移到新数组。

① 遍历旧数组的所有元素,调⽤的 hash 值。⽅法判断是否需要哈希重构,如果需要就重新计算元素 key② 调⽤

JDK8⽅法计算元素存放的下标 i,利⽤头插法将旧数组的元素转移到新数组。

hash:计算元素 key 的散列值

如果 key null 返回 0,否则就将 key ⽅法返回值⾼低16位异或,让尽可能多的位参与运算,让结果的 0 和 1 分布更加均匀,降低哈希冲突概率。

put:添加元素

 调⽤ ⽅法添加元素。

② 如果 table 为空或⻓度为 0 就进⾏扩容,否则计算元素下标位置,不存在就调⽤节点。

③ 如果存在且是链表,如果⾸节点和待插⼊元素的 hash key 都⼀样,更新节点的 value

④ 如果⾸节点是 TreeNode 类型,调⽤⽅法增加⼀个树节点,每⼀次都⽐较插⼊节点和当前节点的⼤⼩,待插⼊节点⼩就往左⼦树查找,否则往右⼦树查找,找到空位后执⾏两个法: balanceInsert ⽅法,插⼊节点并调整平衡、 moveRootToFront ⽅法,由于调整平衡后根节点可能变化,需要重置根节点。

⑤ 如果都不满⾜,遍历链表,根据 hash key 判断是否重复,决定更新 value 还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值 7,还需要调⽤

⑥ 存放元素后将 modCount 1,如果

get :获取元素的 value ,调⽤把链表重构为红⿊树扩容。

① 调⽤

②⽅法获取 Node 节点,如果不是 null 就返回其 value 值,否则返回 null⽅法中如果数组不为空且存在元素,先⽐较第⼀个节点和要查找元素的 hash key ,如果都相同则直接返回。

③ 如果第⼆个节点是 TreeNode 类型则调⽤key 查找,如果没有找到就返回 null

resize:扩容数组

⽅法进⾏查找,否则遍历链表根据 hash 和重新规划⻓度和阈值,如果⻓度发⽣了变化,部分数据节点也要重新排列。

重新规划⻓度

① 如果当前容量

② 如果未达到最⼤容量,当且达到最⼤容量,将阈值设为 Integer 最⼤值,return 终⽌扩容。不超过最⼤容量就扩⼤为 2 倍。

③ 如果都不满⾜且当前扩容阈值 oldThr > 0 ,使⽤当前扩容阈值作为新容量。

④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12

重新排列数据节点

① 如果节点为 null 不进⾏处理。

② 如果节点不为 null 且没有next节点,那么通过节点的 hash 值和新的 table 数组。进⾏与运算计算下标存⼊

③ 如果节点为 TreeNode 类型,调⽤法转回链表。⽅法处理,如果节点数 hc 达到6 会调⽤

④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash &的部分不需要做处理,否则需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。

Q7HashMap 为什么线程不安全?

JDK7 存在死循环和数据丢失问题。

数据丢失:

并发赋值被覆盖: 在⽅法中,新添加的元素直接放在头部,使元素之后可以被更快访问,但如果两个线程同时执⾏到此处,会导致其中⼀个线程的赋值被覆盖。

已遍历区间新增元素丢失: 当某个线程在 ⽅法迁移时,其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,table 数组引⽤指向了 newTable,新增元素丢失。

新表被覆盖: 如果⾏插⼊。但如果多线程同时完成,执⾏了 table = newTable ,则后续元素就可以在新表上进,每个线程都会 new ⼀个数组,这是线程内的局部对象,线程之间不可⻅。迁移完成后 resize 的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插⼊的对象都会被丢弃。

死循环: 扩容时 调⽤使⽤头插法迁移元素,虽然 newTable 是局部变量,但原先table 中的 Entry 链表是共享的,问题根源是 Entry next 指针并发修改,某线程还没有将 table 设为newTable 时⽤完了 CPU 时间⽚,导致数据丢失或死循环。

JDK8 在⽅法中完成扩容,并改⽤尾插法,不会产⽣死循环,但并发下仍可能丢失数据。可⽤ConcurrentHashMap 或包装成同步集合。

最后呢,本文章的所有知识取自于B站高淇老师讲的Java300集教程,里面更加全面的讲述了关于Java面试中所能遇到的各种问题,包括解决问题的方法。小编也给大家准备了充分的资源:

给同学们带来全新的Java300集课程啦!java零基础小白自学Java必备优质教程_手把手图解学习Java,让学习成为一种享受_哔哩哔哩_bilibili

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值