今天练了三轮
Java 打 HashMap,前端打事件修饰符和闭包,数据库打 SQL 排序分页加 Redis 入门
HashMap 定位
题目问 HashMap 凭什么不用遍历就能找到 key。我答了数组加桶、hashCode 扰动取模。
还有一处:我说 hashCode 得到的是内存地址。不准确。hashCode() 返回的是一个 int,Object 的默认实现确实跟对象地址有点关系,但那是 HotSpot 的实现细节,业务类重写之后就是你自己的算法算出来的值。稳妥说法是"调 key 的 hashCode() 拿一个整数,再经过扰动"。
顺带补了两个原来没提的:实际代码里不是取模,是 (n - 1) & hash 位运算,因为容量固定是 2 的幂,位运算比取模快;不同 key 算到同一个下标叫哈希冲突,冲突了先挂链表,链表长度到 8 并且数组长度到 64 就转红黑树。
另外两问答得还行。只重写 equals 不重写 hashCode 的后果我说了"存进去了,取不出来",这个描述比较具体。HashSet 底层是 HashMap、add 调 put、靠 hashCode 加 equals 判重,连 value 是占位对象所以只留一个这个细节都答出来了。
闭包
这段代码问打印什么:
function outer() {
let count = 0
return function inner() {
count++
console.log(count)
}
}
const fn = outer()
fn() // 1
fn() // 2
count 本来就住在 outer 的作用域里,inner 定义在同一个作用域内,所以它访问的就是同一个变量。outer 执行完本来该销毁这个作用域,但 inner 被赋值给 fn 一直活着,它引用着 outer 的作用域,count 就跟着活下来。两次 fn() 改的是同一个 count。
闭包的定义准确的是:函数记住了它定义时所在的作用域,即使外层函数已经执行完,那些变量依然活着。关键在"记住"和"引用"。回调函数、事件监听里到处是闭包。
晚上又问了一句 Java 的内部类是不是一样。大方向一样,但得分开说。非静态成员内部类隐式持有外部类实例的引用,所以得 outer.new Inner() 这样创建,只要内部类实例活着,外部类就回收不掉,Android 里 Handler 持有 Activity 导致泄漏就是这个原因。静态内部类不持有外部引用,随便 new。真正对标 JS 闭包的是匿名内部类访问方法局部变量,但 Java 是把值复制一份进去,而且变量必须 effectively final,只能读不能改;JS 闭包抓的是变量本身,随便改。面试问"Java 匿名内部类为什么局部变量必须 final",答案就在这个复制语义上。
分页:差了一位
LIMIT 20, 10 和 LIMIT 10 OFFSET 20 是一个意思,这个答对了。offset 20 是跳过前 20 条,从第 21 条开始取。记法就是第一个数字是"扔掉几条",不是"从第几条开始"。
LIMIT 100000, 10 这种深分页很慢,MySQL 得先扫掉前 10 万行再丢掉。优化用游标,WHERE id > 上一页最后一个id LIMIT 10,或者延迟关联。先记着有这回事。
Redis 那问基本全对
内存和磁盘这个本质区别说对了,快的原因也对:请求先查 Redis,命中就直接返回不碰数据库。场景给了三个,分布式锁、计数器、session 共享,秒杀那句还点到了锁的用途。
一处修正:说"重启默认丢失数据"不够准。Redis 有持久化,RDB 快照默认就是开的,准确说是"可能丢掉最后一次快照之后的数据",配了 AOF 才接近不丢。
说反的地方比答不上来更值得记,因为那是脑子里已经有一套错误模型。