Kotlin Map接口的键类型参数K为何是不变式而非协变?
Kotlin
Map 键类型参数 K 设计为不变型的核心原因 Kotlin 1.6.21 版本中Map接口的签名如下:
interface Map<K, out V>
你提的疑问切中了这个设计的关键点:官方文档给出的“K同时出现在方法入参和返回值位置,因此无法声明为协变”只是表层说法,并没有解释为什么同为只读集合的Set/List可以用@UnsafeVariance注解实现元素类型协变,Map的K却不行。
纯从类型系统理论层面,完全可以给containsKey/get/getOrDefault这些入参位置的K加上@UnsafeVariance,把K声明为out K实现协变,不会产生任何JVM层面的类型强转异常,和V当前的实现逻辑完全一致。最终K保持不变,是基于实际开发体验的实用主义权衡,核心考量有两点:
- 核心方法的类型检查收益远大于协变收益。
get是Map最高频的核心操作,一旦K声明为协变,编译器将彻底失去对key入参的类型校验能力。举个最常见的场景:如果有一个Map<String, User>存储字符串ID对应用户,K协变后它可以合法向上转型为Map<Any, User>,此时如果误把Long类型的ID传入get方法,编译器不会抛出任何类型错误,运行时只会静默返回null,这类低级错误的排查成本极高。而协变本身带来的收益非常有限——现实开发中几乎没有需要把Map<String, V>当作Map<CharSequence, V>或Map<Any, V>传递的通用场景。 - 同V的协变风险不对等。V之所以可以通过
@UnsafeVariance实现协变,是因为V仅在极低频率的containsValue方法中出现在入参位置,就算传入错误类型的值,最多返回false,不会影响从Map中取出的值的类型安全性,协变带来的“可以将Map<String, String>传给接收Map<String, Any>参数的方法”的便利,远大于漏检风险。
至于为什么Set/List可以将元素类型声明为协变:这两个接口的核心入参操作只有contains,使用频率远低于Map的get方法,就算传入错误类型返回false,影响面也极小,协变带来的集合传参便利完全覆盖了漏检的成本。
内容的提问来源于stack exchange,提问作者Leisetreter
相关产品推荐
相关产品推荐

