今天的练习
昨天太忙,三轮一道没答。今天的题是昨天那三组原样补回来的,不给新难度,也不往回退。三轮下来,Java 那轮十七个小问里错了一个,另外两轮全清。
题目一:四种方法引用,卡在「类名::实例方法」
下面三个写法各是哪种引用:Integer::parseInt、str::length(str 是外面建好的一个 String)、User::new?还有一种写法是类名::实例方法,比如 String::compareTo,它跟 str::length 都沾着实例方法,差在哪?
前三个我判对了,分别是静态方法引用、绑定实例方法引用、构造器引用。改写回 Lambda 我挑的是 s -> Integer.parseInt(s),接收一个字符串、调 parseInt 转成 int,这个也答对了。
卡住的是那半问。我当时说「不知道,但看 ::length 是使用一个已经存在的方法,另一个应该是一个未被构造的方法」,方向沾到边了,词不对。
助手把这两个名字补齐了。str::length 叫绑定实例方法引用,因为接收者 str 是个现成对象,它收不到任何参数,等价于 () -> str.length()。String::compareTo 叫未绑定实例方法引用,接收者得从参数里传进来,等价于 (a, b) -> a.compareTo(b),所以要收两个参数。判断窍门是看冒号前面那一截:是变量就是绑定的,是类名再往后看那个方法,静态就是静态引用,实例就是未绑定。
顺带那两问:排序
Comparator.comparing(User::getAge) 的参数接的是一个取排序键的 Function 函数式接口,这个我答对了;从大到小用 .reversed(),二级排序用 .thenComparing(User::getName),也都对。
「相减会溢出」那题我说的是两个 int 相减遇上负数会排错,得用 Integer.compare(a.getAge(), b.getAge())。助手补了更省事的写法:Comparator.comparingInt(User::getAge),直接走 int,连装箱都省了。
题目二:LinkedHashMap 和几行写一个 LRU
LinkedHashMap 是干嘛的、想解决什么问题?它比 HashMap 多做了什么才把顺序保住?同一个 key 已经在里面了,再 put 一次新值,它在遍历顺序里的位置会不会挪?LRU 缓存经常拿它几行写出来,它内部得额外记点什么,才能知道谁最久没用?
我答的是:它在 HashMap 基础上额外维护了元素顺序,内部多一条双向链表记住插入顺序。重复 put 不挪位置,只顶掉原来的值。LRU 那半问我说对了机制,把最近被访问的元素挪到末尾,头部那个就是最久没用上的。
助手补了落地写法:构造的时候把 accessOrder 传 true,之后每次 get 和 put 都会把节点移到链表尾部,头节点自然就是最久没用的那个;再重写一下 removeEldestEntry 让它返回 true,十来行就是一个带淘汰的缓存。
题目三:left 和 transform 差的是一整条流水线
两个元素都做从左到右的位移,一个改 left、一个改 transform: translateX(),浏览器要干的活差在哪?用 transform 的那个,渲染流水线里有哪几步被省掉了?既然 transform 这么划算,是不是给每个元素都单独加一层走 GPU 更好?
我答的是:改 left 会触发重排还得重绘,整条路走样式计算、布局、绘制、合成;transform 不影响文档流,也不改变其他元素的位置,只需要在合成阶段把元素挪一下。
助手把省掉的那两步点了名:Layout 和 Paint 都不跑,只剩 Composite,前提是这个元素已经被提升成合成层。
第二问我答的是「肯定不是」,理由是前端是交互层、要考虑用户体验,大量加层会吃内存,移动端尤其容易卡。这方面有个专门的叫法叫层爆炸,每个合成层都要占一份显存,手机 GPU 内存本来就小,层一多直接掉帧。
题目四:watchEffect 怎么知道该盯谁
watchEffect 和 watch 最直观的差别是什么,要监听谁需要你自己写出来吗?那它是怎么知道该盯哪些数据的?进页面就把 document.title 设成当前标题,之后标题变了跟着改,用 watch 得加 immediate,用 watchEffect 要不要加?建好的 watch / watchEffect 想手动停掉,调哪个东西?
我答的是:watch 是直接写好监听谁,watchEffect 是自己收集要监听哪些;它跑回调的时候,会把回调里访问过的响应式依赖全收集起来,自动建立追踪。第二问我答不用加 immediate,因为 watchEffect 默认就立即执行一次;要停就调它返回的 stop 函数,watch 和 watchEffect 都一样。
助手补了一条边界:依赖只在回调同步执行的过程中被收集,回调里 await 之后才碰到的响应式数据不会被追踪,那种情况还得用 watch,或者手动 nextTick 一下。
题目五:谁盖谁
一个没写 position 的普通 div,和一个 position: relative; z-index: 0 的 div 叠在一起,谁在上面?一个子元素写 z-index: -1,会跑到父元素背景下面去吗?父元素里那些没定位的兄弟块,又跟它谁在上面?
我答的是 relative 加 z-index: 0 的那个在上面,因为定位元素默认盖在普通元素之上;负 z-index 的子元素排在普通流块级元素下面;父元素一旦创建了层叠上下文,它就钻不出父元素的背景。
助手把顺序补全了,从下往上数一共七层:创建层叠上下文那个元素自己的背景和边框、负 z-index 的子元素、块级非定位子元素、浮动元素、行内内容、z-index 为 auto 或 0 的定位元素、正 z-index 的子元素。前面两问的答案正好卡在第二层和第六层上。
题目六:这列值不值得建索引
users 表有个 gender 列只有男/女两个值,建索引基本没用;phone 列建索引就很值。判断一列值不值得建索引,看的是什么?一个联合索引 (a, b),a 重复度很高、b 基本不重复,哪个该放前面?
我答的是看区分度,也就是这列能不能把数据快速缩小到很少几行,区分度太低建索引意义不大。第二问我答主要看场景:如果查 a 的场景很多,就得权衡最左前缀;如果没有这种场景,b 放前面能很快定位。
助手把两边说细了。区分度的算法是「这一列有多少个不同的值」除以「总行数」,越接近 1 越值得建,gender 这种只有两个值的比例低到没意义。联合索引那边,区分度高的放前面确实能让过滤后剩下的行更少、回表代价更小;但最左前缀决定了查询里必带的那个列只能待在最左边,所以先看查询长什么样,再决定列序。
题目七:锁等待和粘包
某个接口突然一片 Lock wait timeout exceeded,MySQL 里有哪些手段能看到现在谁在等谁的锁?从写 SQL 的角度怎么尽量避免这种等待?客户端连着发 hello 和 world,服务端一次 recv 可能收到 helloworld,为什么?应用层一般怎么解决?
锁那两问我答的是 SHOW ENGINE INNODB STATUS 和 performance_schema.data_lock_waits。避免等待我列了四条:事务尽量短,别在里面夹 RPC、HTTP 请求、文件操作和大批量计算;让 UPDATE / DELETE 走索引,不然可能锁一大片甚至锁表;多个事务按相同顺序访问相同资源;应用层设好锁等待超时并做有限重试。
助手补了两处:排查还可以配合 sys.innodb_lock_waits 和 information_schema.INNODB_TRX 看谁阻塞了谁;另外把隔离级别从 RR 降到 RC 能少掉不少间隙锁,大事务也建议拆小。
粘包那题我答的是 TCP 面向字节流,只保证按次序到达,不认「一条消息」的边界;解决办法给了分隔符、固定长度、消息头里放长度字段这三种。助手提了一句,Netty 里这三种正好对应三个解码器,长度字段那个是 LengthFieldBasedFrameDecoder。