今日八股:javac 把泛型擦干净之后,有几件事就做不了了

今日八股:javac 把泛型擦干净之后,有几件事就做不了了

题目一:泛型解决什么,擦除又带走了什么

以前写 List list = new ArrayList(),取的时候 String s = (String) list.get(0)。取出来为什么非得强转?如果有人塞了个 Integer,这个错什么时候才炸出来?换成 List<String> 之后,尖括号里的 String 管了什么,把错误提前到了哪个阶段?a.getClass() == b.getClass() 是 true 还是 false?既然运行时不在了,get(0) 赋值时那句强转是谁加的?

我答的是:存进去的值取出来是 Object,所以必须强转;错要到运行时才炸,因为编译器看到强转不会去检查实际值。换成 List<String> 之后,尖括号既管存也管取,存的时候检查类型,取的时候按约束自动转换,把错误提前到了编译阶段。

擦除那两个问题也答对了:两个 list 的 getClass() 相等,因为泛型只活到编译期,编译之后就被擦掉了;那句强转是编译器自动插的,擦完之后就是一个裸 List。

助手补了两个更准的说法。抛的是 ClassCastException,位置不在 get(0) 那一行,而在编译器替你插进去的那句强转上,所以栈里报的看起来是调用处。擦除的准确表述是 List<String> 擦成裸的 List,类型变量擦到它的上界,没写 extends 时上界就是 Object,所以 List<? extends Number> 擦完是 List<Number>。

擦除之后做不了的两件事

一个是 List<int> 编译不过,泛型参数必须是引用类型,基本类型得用包装类。第二个是 instanceof List<String> 编译不过,运行时那个 String 已经没了,编译器直接拒绝。想判断「里面装的是不是都是 String」,我给的思路是用 stream 的 allMatch 逐个判断类型。

助手提醒了一个坑:allMatch 在空集合上返回 true,因为「所有元素都是 String」对零个元素天然成立,写业务代码的时候要把空集合单独处理一下。

题目二:继承那两行代码埋的坑

Student 里写 Person.call(this, name),再写 Student.prototype = new Person()。头一行在干什么,去掉之后 new Student('张三', 3).name 是什么?第二行执行完,实例调 sayHi 时从哪找到方法?这种写法留下了什么毛病?

我答的是:头一行是在 Student 构造函数里借用 Person 构造函数;去掉之后 name 是 undefined,因为 name 是 Person 构造函数里的实例属性,Student 自己没有。第二行执行完,实例就顺着原型链去 person 那里找 sayHi。

毛病我答了两处:Student.prototype 上多了一个不该有的 name 属性,还有 constructor 指向跑到了 person。

助手把这段写法的名字和更好的版本给了:这叫组合继承,代价是 Person 被构造了两遍。规范写法是 Student.prototype = Object.create(Person.prototype),再把 constructor 手动指回 Student;或者直接用 class,super(name) 干的就是 call 那件事。

题目三:v-model 在组件上是怎么跑的

自己写了个 MyInput,想让父组件用 v-model="keyword"。这一行展开成 props 和事件是哪两个名字?子组件把输入回传给父组件靠什么触发?一个组件上一起绑两个 v-model 怎么写?

我答的是:一个绑定值、一个监听值更新来触发更新。默认的 prop 是 modelValue,事件是 update:modelValue。子组件内部用 defineEmits 声明,然后 emit 出去。多个 v-model 就写成 v-model:title 和 v-model:content,子组件对应 update:title 和 update:content 两个事件。

助手补了个新写法:Vue 3.4 之后有 defineModel 宏,一行 const title = defineModel('title') 就把 props 和 emit 都省了,多个 v-model 也一样。

同一个上下文里还问了 BFC

父元素里全是浮动子元素、高度塌成 0,加 overflow: hidden 就好了,为什么?我答的是 BFC 的盒子算高度时认浮动子元素,普通盒子不认。相邻两个兄弟,一个 margin-bottom 30px、一个 margin-top 20px,中间实际隔 30px,取大的那个;想让两边各算各的、加起来 50px,得让它们不在同一个 BFC 里,最简单是中间加个 div 隔开。

助手补充:现代写法是给父元素 display: flow-root,它专门为清除浮动加的,不像 overflow: hidden 那样顺手把溢出内容裁掉。

题目四:JOIN 为什么拿小表当驱动

orders 一千万行、users 一万行,执行 SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.city = '成都'。最常见的那套算法叫什么?哪张表更适合当驱动表,为什么?被驱动表的连接列上得有什么?反过来拿大表当驱动,代价是什么?

我答的是嵌套循环连接,users 更适合当驱动表,因为外层扫一遍、内层查一次,外表一万行内层就查一万次。被驱动表的连接列上得有索引,否则内层就是全表扫。反过来让大表当驱动表,外层行数太多,内层查找次数直接爆炸。

助手补了个例外:MySQL 8.0.18 之后,对没有索引的等值连接会用 hash join,先把小表建成哈希表再扫大表,所以大表对大表也不一定老老实实走嵌套循环。判断实际走哪套,还是得看 EXPLAIN 的 Extra。

题目五:读不加锁,凭什么还能读到一致的旧值

RR 级别,会话 A 更新了 id=1 的 name 但不提交,会话 B 普通 SELECT 读 id=1,读到的是张三还是李四?B 既没被卡住也没加锁,凭什么拿到前后一致的旧值,这套机制叫什么?旧版本数据存在哪儿?B 靠什么判断哪个版本该看见?

我答的是读到张三。RR 级别下普通 SELECT 是快照读,走 MVCC,A 没提交 B 不知道要改,B 看到的是事务开始时的快照版本。旧版本存在 undo log 里,靠读视图判断。

助手把读视图补细了:RR 是事务里头一次快照时生成一个 read view 然后就一直复用,RC 是每条语句都重新生成,这就是两个隔离级别在「可重复读」上差别的来源。读视图里记着当时活跃的事务 id,拿行版本上的事务 id 跟它比,比出来的结果决定这个版本能不能看见。

题目六:JWT 把登录态交给了谁

前后端分离、后端好几台机器,登录态以前放 Session 再存 Redis。JWT 和 Session 最大的区别在哪?服务端还要不要存一份登录态?它把「你是谁」交给了谁保管?JWT 那三段分别是什么?第三段签名保证了什么?payload 里能不能直接放密码或完整手机号?

我答的是:Session 服务端要存登录态,JWT 服务端不存,登录态放在客户端的 token 里,这件事交给 token 本身加上服务端签名。三段是 Header 说明类型和签名算法、Payload 放声明、Signature 用前两段加密钥签名。签名保证前两段没被篡改。payload 里不能放密码和完整手机号,因为 JWT 只是 base64 编码,很容易就解出来了。

助手补了一个取舍上的坑:无状态的好处是服务端不用存、多台机器不用共享,代价是没法主动让某个 token 失效,要做「退出登录立刻生效」得配黑名单,或者用短过期加 refresh token 的组合。

← 返回文章列表