
kotlin泛型

前言
学习 kotlin 泛型肯定离不开 java 泛型,这是显而易见的
很多大佬写过 kotlin 泛型的文章或者出过讲 kotlin 泛型的教程,我深受启发
所以,我们先学 java 泛型
Java 泛型
泛型是什么
一些前提术语
- 泛型:泛型是 Java 引入的一种参数化类型机制,允许在定义类、接口或方法时使用类型占位符(如 T、E 等),并在调用时指定具体类型,从而增强代码的类型安全性和复用性。
- 泛型类型:指包含类型参数的类或接口。例如
List<T>
是一个泛型类型,而List<String>
是其参数化后的具体类型。 - 泛型类型参数:泛型定义中使用的类型占位符(如 T、K、V 等),用于表示未知的具体类型,在实例化时由开发者指定。
泛型类型是一种参数化的类型,比如 List<T>
这个 List
本身是一种类型,但它的泛型类型参数(T
)不同,它本身的类型也不同,也就是说 List<String>
和 List<Integer>
就是不同的类型
当然,我们也不一定非要使用这种套一层的泛型类型,直接使用泛型类型参数 T
就可以,此时泛型类型 等同于 泛型类型参数
但我们往往都会给泛型类型参数(T
)加一个“范围”,或者一个限制,这样方便我们更明确地表示使用泛型类型的意义,比如让 T
作为 List
里的元素,或者让 T
作为 Number
的子类,例子如下:
1 | <T extends Number> double add(T a, T b) { |
泛型类型声明的写法
定义泛型类/接口
将<T>
写在类名或接口名之后,表示该类/接口接受一个泛型类型参数T
1
2
3
4
5
6
7
8
9// 泛型接口
public interface List<T> {
void add(T element);
T get(int index);
}
// 泛型类
public class Box<T> {
}定义泛型方法
将<T>
写在方法的返回类型之前,表示该方法独立于类的泛型参数,拥有自己的泛型类型T
。1
2
3
4
5public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
第一个问题:类型擦除
我们肯定都听过类型擦除,他是实现泛型的原理中的一部分,简单来说就是在编译期间将类型直接打回到声明时的边界类型,在运行时无法识别具体类型是什么
边界类型
如果你的泛型类型这么写 <T extends String>
那类型擦除后的就是 String
,如果你泛型类型这么写 <String>
那边界类型是 Object
也就是,有继承关系,边界类型就是继承关系的上界,没有继承关系就是 Object
java 泛型可以多继承?
java 泛型允许多继承,但这其实是一种语法简化,是将
implement
接口也用extends
了比如
<T extends Number & Comparable<? extends T>>
,这样的多重边界,选择第一个为边界类型,并且编译器还会给你提示,让你将类作为第一个边界,并且不允许集成多个类
? extends 和 T extends
? extends
是在使用泛型类型的时候,写的一般含有
T
或E
的都是在定义(声明)泛型类或者泛型函数的时候,写的除非在定义的时候,也存在调用,例如:
1
private <E extends Comparable<? super E>> E max(List<? extends E> e1)
这里的
Comparable<? super E>
就是调用E
,并非定义
容易混淆的写法
所以编译器的类型擦除,只是在定义泛型类型参数(
T
)的时候,也就是调用T extends String
的时候,会将边界类型识别为String
,而? extends String
与类型擦除的边界类型无关。这涉及我们后面讲的协变和逆变。不用管有些教程里面说的什么上界下界,那是协变逆变里的知识,而且在我看来属于冗余知识,对写代码没什么帮助,而且还会迷惑初学者
你肯定也发现了,
T extends String
和? extends String
是容易混淆的两个写法,这确实是学习泛型的一个难点,而kotlin
的泛型很好地规避了这点,这在后面讲。
类型擦除干了什么
类型擦除会在编译期间,将泛型类型参数(T
)替换成他的边界类型,然后在字节码中插入强制类型转换,并在继承的子类中,为重写(覆盖)方法生成桥方法
强制类型转换
将边界类型再转回声明时的类型
1
2
3List<String> list = new ArrayList<>();
list.add("hello");
String value0 = list.get(0); // 这里就有字节码插入的强制类型转换返回泛型类型对象时
开发者需要使用Class.cast()
进行类型安全转换,try-catch
一下及时处理,而不是直接强制类型转换(T)obj
Class.cast()
和(T)obj
的区别为什么说
Class.cast()
更安全,因为cast()
会在此方法执行的时候,就抛出ClassCastException
而
(T)obj
会在转换完之后,调用的时候抛出异常,这个时间点比Class.cast()
更晚转换的时候不进行异常捕捉,在调用的时候,再抛出异常,这就容易出错
在生成的桥方法中也会有强制类型转换
什么是桥方法?
桥方法是为了避免 java 多态失效的
java 多态中,有一个方法重写,也就是子类重写父类方法,比如:
1 | class Box<T> { |
重写的规则
重写的规则就是,子类的
Override
方法必须要和父类的声明(访问权限修饰符,参数列表,返回类型)完全一致,但内部实现可以重新定义因为我们这里使用了泛型,所以
T
和String
这样是被允许的如果参数一个是
Integer
一个是String
则不被允许
上述代码中,StringBox
是 Box
的子类,在声明一个 Box box
但是 new
一个 StringBox
的时候,调用 box.putIn();
会去实际调用 StringBox
的 putIn()
方法
但是如果,你在使用泛型,T
会被擦除为 Object
,也就是说,父类变成了 void putIn(Object obj)
,此时如果没有桥方法,重写就失效了,因为此时父类方法和子类方法的参数不一样了
这时候,编译器就是在子类生成一个桥方法,也就是生成一个参数为 Object
的方法,让重写继续生效
1 | class StringBox extends Box<String> { |
第二个问题:协变和逆变
接下来我们就使用这三个类的继承关系

搞明白了类型擦除,我们就可以解释为什么 List<Animal>
和 List<Peopple>
是完全不同的类型
因为 List<Animal>
和 List<People>
在运行时表现为相同的原始类型(List<Object>
),但编译器在编译时通过类型擦除和强制转换确保它们无法互换使用,从而维护泛型类型的安全性。运行时的类型擦除不影响编译时对类型一致性的严格检查。
为什么这么设计呢?
这是 java 的有意为之,原因就在于泛型会在编译的时候进行类型擦除之后,导致运行时无法进行类型判断。
即使使用父类子类进行约束泛型类型参数,也会出现混乱的情况,导致使用泛型类型参数的时候,开发者无法判断出来到是是什么类型。
在运行时,也无法动态判断类型,导致代码出现不可空的问题
其实数组其实也有这个问题,但是它就可以在运行时捕捉到异常,导致类型不匹配可以被检测出来,这就变得可控了
至于为啥数组和泛型要使用不同的设计,这是语言设计开发的考量,并且其中多多少少都有一些历史问题,为了兼容性啊什么之类的。
所以 List<Animal>
和 List<People>
在编译器认为,这就不是同种类型,没有任何继承关系
1 | // 协变 People[] 是 Animal[] 的子类 |
也正是因为泛型使用了类型擦除,很难在运行时捕捉到类型异常,所以干脆直接禁止了协变(参数是子类的泛型类,可以赋值给,参数是父类的泛型类)
为什么使用类型擦除?
既然类型擦除这么不好,为什么要是用类型擦除呢?
其实也没有很不好,毕竟这样设计避免一些类型安全问题,在编译期间就直接报出类型匹配问题,避免了运行时的类型问题
然后为了突破这种限制,java 给了协变和逆变的修饰符 ? super
和 ? extends
协变(? extends
):参数是子类的泛型类 是 参数是父类的泛型类的 子类
逆变(? super
):参数是父类的泛型类 是 参数是子类的泛型类的 父类
注意,这个 super
和 extends
又和 平时在使用类的继承和父类的不一样
无论是 ? super
还是 ? extends
,都是修饰符,语法像 public
static
注意区别
协变逆变并不是改变使用泛型类型参数的功能,而是改变泛型类型本身继承关系的功能,以下代码在不使用协变逆变的时候也会正常编译运行
1 | List<Animal> animalArrayList = new ArrayList<>(); |
协变
? extends
的修饰,表示使用协变————类型参数是子类的泛型类型,是此泛型类型的,子类。
但是这会还有一个限制,你只能用这个泛型类型参数,但不能修改它,也就是只读不写,因为你没法找到一个类型为 ? extends
的变量。
是的,编译器会直接把类型识别成 ? extends

具体表现在,你不能调用,方法参数里 包含泛型类型参数 的方法。也不能给它 包含类型参数的字段 赋值,除了 null
。
以上原因————都是,因为没法填写类型为
? extends
的参数。
举个例子:
1 | // 初始化一个 泛型类型参数为 People 的 ArrayList |
逆变
逆变有点反直觉
1 | // 协变 |
逆变用 ? super
修饰,表示类型参数是父类的泛型类型,是此泛型类型的,父类。
逆变不是允许泛型类型参数是父类吗???
- 因为这里确实有个歧义,逆变说的泛型类型参数允许是父类,指的是对于整个泛型类型而言的。
也就是List<? super People>
的子类变成了 类型参数为People
子类的泛型类型。
而List<? super People>
还是只允许添加People
的子类。
只读不写 vs 只写不读
很多大佬,包括扔物线都说,协变是只允许读不允许写,逆变是只允许写不允许读
但这种说法不仅仅是不对的,还没有透彻地解释清楚这些限制的由来,而且逆变是可以读的,但是前提是你可以准确判断他的类型是什么。
在 java 语法中,最起码在我使用的 java17 里面是没有任何问题的(OpenJDK Runtime Environment Temurin-17.0.14+7 (build 17.0.14+7))
总结一下
- 协变逆变只是改变泛型类型本身的子类父类关系,并不影响泛型类型参数的继承关系,使用泛型类型参数的时候,还是只能使用类型参数的子类。
- 协变会增加一些写的限制,这些限制的来源是因为编译器自己在生成了一个类型,一般叫做
CAP#0
或CAP#1
之类的,你声明这个类型。- 协变里,
CAP#1
这个新类型,不允许你初始化它,所以你没办法将这个类型填写到任何地方,并且他的子类也没发用,或者说,它没有继承关系,连Object
跟他都没关系。 - 逆变里,你可以像不使用协变逆变一样读写,但是只能使用当前声明的类型参数的本身或子类。
- 协变里,
<?>
<?>
相当于<? extends Object>
泛型数组
确切的泛型数组是不允许创建的,因为所有的泛型类型,都会被类型擦除,所以就会出现 List<String>[]
和 List<Integer>[]
都会擦除成 List<Object>[]
但如下代码是被允许的
1 | List<?>[] lists_0 = new ArrayList[10]; |
多个泛型类型参数
java 允许多个泛型类型参数,比如 HashMap<K,V>
,那么我们思考一下,多个泛型参数如何进行协变和逆变呢?
答案是交集!
比如你有一个 HashMap<? extends Animal, ? super People>
的对象,那他的子类,必须是第一个参数是 Animal
及其子类,第二个参数是 People
及其父类,这样的才是他的子类,就像这样:
1 | HashMap<People, Animal> hm = new HashMap<>(); |
到调用的时候,还是,参与协变的类型参数不在继承关系里,没法手动生成,所以没法填进方法参数里;参与逆变的类型参数需要知道具体类型进行强制类型转换。
OK !说了这么多,终于要开始对接 Kotlin
泛型了
Kotlin 泛型
泛型类型的声明写法
1 | // 泛型接口 |
extends
在 kotlin
里是 :
1 | fun <T : Number> add(a: T, b: T): Double { |
kotlin 的协变和逆变
我们还是使用之前的 Animal People
java 的 ? extends
在 kotlin 里变成了 out
,? super
在 kotlin 里变成了 in
这样就会避免了 ? extends
和 T extends
的歧义
1 | // 协变 |
协变这里,说预期的类型是 CapturedType(out org.czb.bean.Animal)
,这和 java 的 CAP#1
是一个意思,这里的参数没法写入,因为这个类型是编译器自己生成的,开发者自己没法生成
逆变这里,也是和 java 一样,可以读取,但是前提是你必须知道类型,因为返回的是 Any?
类型
在看一个例子:
1 | fun <T> consume(list: MutableList<out T>) { |

out
和 ? extends
/ in
和 ? super
java 的
? extends
和? super
会给一种,类型参数的继承关系改变的错觉(实际上? extends
和? super
是将泛型类型的继承关系改变,而不是类型参数)kotlin 的
out
in
可以避免这种歧义,但泛型类型的继承关系就变的不太明显了
官方文档的胡言乱语
为了修正这一点,我们必须声明对象的类型为
Source<? extends Object>
。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变: 可以标注
Source
的类型参数T
来确保它仅从Source<T>
成员中返回(生产),并从不被消费。 为此请使用out
修饰符:
另外,out
和 in
可以写在声明泛型类或者接口的地方(泛型函数不允许声明),这是 ? extends
和 ? super
做不到的,例如:
1 | // 协变 |
当你直接将 out
和 in
写在类和接口的声明时,表明你在定义类或者接口的时候,你的意图就是将泛型类型设计成只写不读,或者只读不写。
这时候的 out
和 in
是 kotlin 的独家语法,并且这个语法要求会更严格
借助郭霖大佬的一个图

方法参数位置是 in position
,返回值位置是 out position
kotlin 多类型参数
kotlin 这里也和 java 保持一致,取交集
1 | val hm = HashMap<People, Animal>() |
kotlin 星号投影 <*>
这是一个比较复杂的东西,首先 <*>
是在调用的写的
泛型定义 | <*> 代表的含义 |
---|---|
MutableList<T> |
MutableList<out Any?> |
MutableList<T : TUpper> |
MutableList<out TUppper> |
MutableList<out T> |
MutableList<out Any?> |
MutableList<out T : TUpper> |
MutableList<out TUppper> |
MutableList<in T> |
MutableList<in Nothing> |
如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>
,可以使用以下星投影:
Function<*, String>
表示Function<in Nothing, String>
Function<Int, *>
表示Function<Int, out Any?>
Function<*, *>
表示Function<in Nothing, out Any?>
不可空的泛型类型
·要将泛型类型 T
声明为绝对不可空,请使用 & Any
声明该类型。例如: T & Any
_
下划线类型推导
下划线运算符 _
可用于类型参数。当明确指定其他类型时,使用它来自动推断参数的类型:
1 | abstract class SomeClass<T> { |
可实例化的泛型类型
在 java 中泛型类型是无法实例化的,获取泛型类型的 Class
对象 T.class
或者类型转换 a as T
的写法是不被支持的,因为 T
在运行的时候类型就被擦除了,但在 kotlin (或者说其他所有的 JVM 语言)中想实现泛型的实例化就不再是梦了
kotlin 有内联函数(inline
),内联函数会直接把函数体了的代码直接替换到调用位置,并且将参数直接改成实际调用的对象。这也就是 kotlin 将泛型类型实例化的秘密了,当然了,实例化泛型类型参数,还需要一个关键字 reified
1 | inline fun <reified T> getGenericType() = T::class.java |
附上郭霖大佬在第三行代码里推荐的写启动 Activity
的写法
1 | inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) { |
特别强调,得是 inline
函数
- Title: kotlin泛型
- Author: lucas
- Created at : 2025-03-24 21:49:32
- Updated at : 2025-04-01 20:45:13
- Link: https://darkflamemasterdev.github.io/2025/03/24/kotlin泛型/
- License: This work is licensed under CC BY-NC-SA 4.0.