题目一:方法引用,和 Optional 新上场
User::getName 里的 :: 叫什么,跟 Lambda 什么关系?把 x -> System.out.println(x) 改成这种写法。Lambda 里要用外层方法的局部变量,这变量得满足什么条件,在 Lambda 里给它重新赋值行不行?
我答::: 叫方法引用,本质是 Lambda 的语法糖,Lambda 体只是调用一个已经存在的方法时就能再简化一层,改出来就是 System.out::println。局部变量那个规矩我也答住了。必须是 final 或者事实上的 final,声明之后没再赋值过;在 Lambda 里给它重新赋值,编译器直接报错。
助手补了一点:Lambda 捕获的是变量的值,等于拷一份进去,并非变量本身,这才是"必须不可变"的由来。静态变量和实例字段不受这个限制,随便改。他还说这条在面试里常被用来区分"背过"和"想过"。
同一天还问了 Stream 的中间操作和终止操作,以及一个全新的板块 Optional。中间和终止我答得挺利索:filter、map 是中间操作,惰性的,只记录要做什么;collect 是终止操作,一调用整条流水线才跑。那句"没有 collect 就像流水线没有开始按钮"是我自己想的比方。
Optional 头一回出,我按它的用途答:显式表达一个值可能存在也可能不存在。of 传 null 会直接抛 NPE,ofNullable 允许 null 并返回 empty,取默认值用 orElseGet。助手补了两个我没想到的。头一个是 orElse 和 orElseGet 的差别:orElse 不管值在不在,括号里那个表达式都会被执行一遍,默认值需要计算的时候只能用 orElseGet。另一点是 Optional 的设计意图,它用来当方法返回值,不建议拿它当字段或者方法参数。
题目二:EXPLAIN 的 type 有四档轻重
type 那一列有好几种取值。ref、range、index、ALL 这四个,从好到差排个顺序,各一句话说清是什么意思。有时候明明建了索引,EXPLAIN 里 key 却是 NULL,除了索引压根没建,还有哪些常见原因会让索引用不上?
我排的是 ref、range、index、ALL。ref 是普通索引做等值查找,直接定位到匹配的索引记录,快;range 是走索引做范围扫描;index 是全索引扫描;ALL 是全表扫描,最慢。
索引失效那问我列了四条:索引列被函数或者运算加工过、用 % 开头的模糊查询、两边类型不一致触发隐式转换、优化器觉得全表扫更划算。
助手说这四条把常考的都覆盖了,进阶还能补 OR 连接里有一侧没索引、联合索引不满足最左前缀、两表字符集或排序规则不同导致的 JOIN。他还说这四个 type 前面还有更快的两档。const 是主键或唯一索引的等值查询,eq_ref 是 join 里被驱动表用唯一索引匹配,这两档一出现,基本说明这条 SQL 没多少优化空间了。
题目三:锁到底加在哪一行
InnoDB 既有表锁也有行锁。平时一条 UPDATE 改一行,默认锁的是哪种粒度?这个「行锁」实际加在那一行数据上,还是加在那行对应的索引记录上?两个事务各握着一把锁又都在等对方撒手,这个局面叫什么,MySQL 会一直等下去吗?
我答行级锁,并且点出它不是加在行数据上,是加在那行对应的索引记录上;没索引或者索引没走成,就可能退化成锁全表。死锁那问我说了名字,也说了不会一直锁,InnoDB 检测到会挑一个事务牺牲掉。
助手补了半句:牺牲的时候通常挑改动行数少的那个,代价小。他确认"行锁其实锁的是索引记录"这点是这题的分水岭,很多人答完"行锁"就停了,被追问"锁在哪"就卡住。
题目四:TCP 凭什么可靠
TCP 比 UDP 多出来的那个「可靠」,靠哪些机制撑起来?TCP 不是发一个包等一次确认那么慢,它允许连着发一批,靠什么控制一次能发多少、对方还接不接得住?
我答的是序列号加 ACK、超时重传、校验和与排序去重,窗口那问答的是滑动窗口,再配合拥塞控制避免把网络打爆。
助手说可以再收一句更整齐的:可靠靠确认应答、超时重传、滑动窗口这三件套。窗口那问还能往下走一步:实际能发多少取 min(接收窗口, 拥塞窗口)。接收窗口由接收方通报,管的是对方接不接得住;拥塞窗口由发送方按网络情况自己维护,管的是网络扛不扛得住。
题目五:前端那轮,我把父子顺序说反了
一个父组件的模板里套了一个子组件,两个组件里都写了 onMounted 和 onUnmounted。页面挂载的时候,这两个 onMounted 谁先执行?父组件的 onMounted 里,能不能直接通过 ref 拿到子组件的实例?切路由把这父子俩一起干掉时,卸载阶段的顺序跟挂载阶段一样,还是反过来?
我答错了,还挺有信心。我说父组件的 onMounted 先执行,再执行子组件的,理由还给自己找了个顺口溜:"没有父亲哪来的儿子"。
助手直接把顺序写出来了:父 beforeMount,子 beforeMount,子 mounted,父 mounted。父的 mounted 排在末尾,它得等所有子组件都挂载完才触发。我那句"没有父亲哪来的儿子"其实指对了地方,只是地方站错了,父先发生在 beforeMount 那一类钩子上,不是 mounted。
这一反,后面那问就跟着翻。父的 onMounted 里能不能拿到子组件实例,我答拿不到,理由是"那时候子组件还没挂载"。实际上能拿到。子的 mounted 早就在父之前跑完,ref 那时候已经赋好值。我把 onMounted 和 beforeMount 混在一起了,beforeMount 里确实拿不到。
还有一问,我答的是"卸载反过来,先卸儿子再卸父亲"。方向沾边,可没这么简单。规则是一套一致的:开始类的钩子父先,完成类的钩子子先。卸载时是父 beforeUnmount、子 beforeUnmount、子 unmounted、父 unmounted。我记的那半句对应的是 unmounted 那一对。
这题 9 月中旬考过一次,当时我是答对的。这次重问反而说反了,助手说下一轮前端先复测这一组,答对才往上加。老实讲这种退步比头一回不会更让人窝火。
这轮里我还说错一句:把"foo 是异步函数,会阻塞任务队列"当成解释。await 谁都不阻塞,它只是把当前函数后面的代码挂成微任务,然后把控制权还给调用方,正因为不阻塞,主线程才能接着往下跑。这种把听来的术语当机制的毛病,这个月犯两次了。