kotlin泛型

kotlin泛型

lucas Lv4

前言

学习 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
2
3
<T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}

泛型类型声明的写法

  1. 定义泛型类/接口
    <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> {
    }
  2. 定义泛型方法
    <T> 写在方法的返回类型之前,表示该方法独立于类的泛型参数,拥有自己的泛型类型 T

    1
    2
    3
    4
    5
    public 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 是在使用泛型类型的时候,写的

  • 一般含有 TE 的都是在定义(声明)泛型类或者泛型函数的时候,写的

  • 除非在定义的时候,也存在调用,例如:

    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
    3
    List<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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Box<T> {
void putIn(T t) {
System.out.println("我是一个什么都能装的 Box,放入" + t.toString());
}
}

class StringBox extends Box<String> {
@Override
void putIn(String s) {
System.out.println("我是一个只能装 String 的 Box,放入" + s);
}
}

class Box_class {
public static void main(String[] args) {
Box box = new StringBox();
box.putIn("ABC");
}
}

重写的规则

  • 重写的规则就是,子类的 Override 方法必须要和父类的声明(访问权限修饰符,参数列表,返回类型)完全一致,但内部实现可以重新定义

  • 因为我们这里使用了泛型,所以 TString 这样是被允许的

  • 如果参数一个是 Integer 一个是 String 则不被允许

上述代码中,StringBoxBox 的子类,在声明一个 Box box 但是 new 一个 StringBox 的时候,调用 box.putIn(); 会去实际调用 StringBoxputIn() 方法

但是如果,你在使用泛型,T 会被擦除为 Object ,也就是说,父类变成了 void putIn(Object obj),此时如果没有桥方法,重写就失效了,因为此时父类方法和子类方法的参数不一样了

这时候,编译器就是在子类生成一个桥方法,也就是生成一个参数为 Object 的方法,让重写继续生效

1
2
3
4
5
6
7
8
9
10
11
class StringBox extends Box<String> {
@Override
void putIn(String s) {
System.out.println("我是一个只能装 String 的 Box,放入" + s);
}

// 编译器生成的桥方法
void putIn(Object obj) {
putIn((String)obj);
}
}

第二个问题:协变和逆变

接下来我们就使用这三个类的继承关系

搞明白了类型擦除,我们就可以解释为什么 List<Animal>List<Peopple> 是完全不同的类型

因为 List<Animal>List<People> 在运行时表现为相同的原始类型(List<Object>),但编译器在编译时通过类型擦除和强制转换确保它们无法互换使用,从而维护泛型类型的安全性。运行时的类型擦除不影响编译时对类型一致性的严格检查。

为什么这么设计呢?

  • 这是 java 的有意为之,原因就在于泛型会在编译的时候进行类型擦除之后,导致运行时无法进行类型判断。

  • 即使使用父类子类进行约束泛型类型参数,也会出现混乱的情况,导致使用泛型类型参数的时候,开发者无法判断出来到是是什么类型。

  • 在运行时,也无法动态判断类型,导致代码出现不可空的问题

  • 其实数组其实也有这个问题,但是它就可以在运行时捕捉到异常,导致类型不匹配可以被检测出来,这就变得可控了

  • 至于为啥数组和泛型要使用不同的设计,这是语言设计开发的考量,并且其中多多少少都有一些历史问题,为了兼容性啊什么之类的。

所以 List<Animal>List<People> 在编译器认为,这就不是同种类型,没有任何继承关系

1
2
3
4
// 协变 People[] 是 Animal[] 的子类
Animal[] animals = new People[10];
// 这里会抛出 ArrayStoreException 尝试将错误类型的对象类型存储到对象数组中
animals[0] = new Animal();

也正是因为泛型使用了类型擦除,很难在运行时捕捉到类型异常,所以干脆直接禁止了协变(参数是子类的泛型类,可以赋值给,参数是父类的泛型类)

为什么使用类型擦除?

  • 既然类型擦除这么不好,为什么要是用类型擦除呢?

  • 其实也没有很不好,毕竟这样设计避免一些类型安全问题,在编译期间就直接报出类型匹配问题,避免了运行时的类型问题

然后为了突破这种限制,java 给了协变和逆变的修饰符 ? super? extends

协变(? extends):参数是子类的泛型类 是 参数是父类的泛型类的 子类
逆变(? super):参数是父类的泛型类 是 参数是子类的泛型类的 父类

注意,这个 superextends 又和 平时在使用类的继承和父类的不一样

无论是 ? super 还是 ? extends,都是修饰符,语法像 public static

注意区别

协变逆变并不是改变使用泛型类型参数的功能,而是改变泛型类型本身继承关系的功能,以下代码在不使用协变逆变的时候也会正常编译运行

1
2
3
4
5
6
List<Animal> animalArrayList = new ArrayList<>();
animalArrayList.add(new People());
animalArrayList.add(new Man());

System.out.println(animalArrayList.get(0));
System.out.println(animalArrayList.get(1));

协变

? extends 的修饰,表示使用协变————类型参数是子类的泛型类型,是此泛型类型的,子类。

但是这会还有一个限制,你只能用这个泛型类型参数,但不能修改它,也就是只读不写,因为你没法找到一个类型为 ? extends 的变量。

是的,编译器会直接把类型识别成 ? extends

具体表现在,你不能调用,方法参数里 包含泛型类型参数 的方法。也不能给它 包含类型参数的字段 赋值,除了 null

以上原因————都是,因为没法填写类型为 ? extends 的参数。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 初始化一个 泛型类型参数为 People 的 ArrayList
List<People> peopleArrayList = new ArrayList<>();
peopleArrayList.add(new People());

// 使用 ? extends Animal 就表示所有 泛型类型参数使用 Animal 子类的
// 都是参数类型是 ? extends Animal 的泛型类型的子类,所以可以赋值
List<? extends Animal> animalList = peopleArrayList;
People p = (People) animalList.get(0);
System.out.println(p);

// 这行会报错 animalList01.add(new People());
// ^
// 方法 List.add(CAP#1)不适用
// (参数不匹配; People无法转换为CAP#1)
// 方法 List.add(int,CAP#1)不适用
// (实际参数列表和形式参数列表长度不同)
// 其中, CAP#1是新类型变量:
// CAP#1从? extends Animal的捕获扩展Animal
animalList.add(new Man());

逆变

逆变有点反直觉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 协变
List<People> peopleList01 = new ArrayList<>();
peopleList01.add(new People());
List<? extends Animal> animalList01 = peopleList01;
System.out.println(animalList01.get(0));

// 逆变
List<Animal> animalList02 = new ArrayList<>();
List<? super People> peopleList02 = animalList02;
peopleList02.add(new People());

// 这行会报错 peopleList02.add(new Animal());
// ^
// 方法 List.add(CAP#1)不适用
// (参数不匹配; Animal无法转换为CAP#1)
// 方法 List.add(int,CAP#1)不适用
// (实际参数列表和形式参数列表长度不同)
// 其中, CAP#1是新类型变量:
// CAP#1从? super People的捕获扩展Object 超 People
peopleList02.add(new Animal());

// 逆变在知道类型的时候,可以进行读取
Animal animal02 = (Animal) peopleList02.get(0);

逆变用 ? 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))

总结一下

  1. 协变逆变只是改变泛型类型本身的子类父类关系,并不影响泛型类型参数的继承关系,使用泛型类型参数的时候,还是只能使用类型参数的子类。
  2. 协变会增加一些写的限制,这些限制的来源是因为编译器自己在生成了一个类型,一般叫做 CAP#0CAP#1 之类的,你声明这个类型。
    • 协变里,CAP#1 这个新类型,不允许你初始化它,所以你没办法将这个类型填写到任何地方,并且他的子类也没发用,或者说,它没有继承关系,连 Object 跟他都没关系。
    • 逆变里,你可以像不使用协变逆变一样读写,但是只能使用当前声明的类型参数的本身或子类。

<?>

  • <?> 相当于 <? extends Object>

泛型数组

确切的泛型数组是不允许创建的,因为所有的泛型类型,都会被类型擦除,所以就会出现 List<String>[]List<Integer>[] 都会擦除成 List<Object>[]
但如下代码是被允许的

1
2
List<?>[] lists_0 = new ArrayList[10];
List[] lists_1 = new ArrayList[10];

多个泛型类型参数

java 允许多个泛型类型参数,比如 HashMap<K,V>,那么我们思考一下,多个泛型参数如何进行协变和逆变呢?

答案是交集!

比如你有一个 HashMap<? extends Animal, ? super People> 的对象,那他的子类,必须是第一个参数是 Animal 及其子类,第二个参数是 People 及其父类,这样的才是他的子类,就像这样:

1
2
HashMap<People, Animal> hm = new HashMap<>();
HashMap<? extends Animal, ? super People> map = hm;

到调用的时候,还是,参与协变的类型参数不在继承关系里,没法手动生成,所以没法填进方法参数里;参与逆变的类型参数需要知道具体类型进行强制类型转换。

OK !说了这么多,终于要开始对接 Kotlin 泛型了

Kotlin 泛型

泛型类型的声明写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 泛型接口
public interface List<T> {
void add(T element);
T get(int index);
}

// 泛型类
public class Box<T> {
}

// 泛型函数
fun <T> printArray(array: Array<T>) {
for (element in array) {
println(element)
}
}

extendskotlin 里是 :

1
2
3
fun <T : Number> add(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}

kotlin 的协变和逆变

我们还是使用之前的 Animal People

java 的 ? extends 在 kotlin 里变成了 out? super 在 kotlin 里变成了 in

这样就会避免了 ? extendsT extends 的歧义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 协变
val peopleArrayList: MutableList<People> = ArrayList()
peopleArrayList.add(People())

val animalList: MutableList<out Animal> = peopleArrayList
val p = animalList[0] as People
println(p)

// 这行会报错,Argument type mismatch: actual
// type is 'org.czb.bean.Man', but
// 'CapturedType(out org.czb.bean.Animal)' was expected.
// 参数类型不匹配:实际类型是 'org.czb.bean.Man',但
// 'CapturedType(out org.czb.bean.Animal)' 是预期的。
animalList.add(Man())

// 逆变
peopleArrayList.clear()
val manList: MutableList<in Man> = peopleArrayList
manList.add(Man())
val m = manList[0] as Man
println(m)

协变这里,说预期的类型是 CapturedType(out org.czb.bean.Animal) ,这和 java 的 CAP#1 是一个意思,这里的参数没法写入,因为这个类型是编译器自己生成的,开发者自己没法生成

逆变这里,也是和 java 一样,可以读取,但是前提是你必须知道类型,因为返回的是 Any? 类型

在看一个例子:

1
2
3
fun <T> consume(list: MutableList<out T>) {
list.add()
}

out? extends / in? super

  • java 的 ? extends? super 会给一种,类型参数的继承关系改变的错觉(实际上 ? extends? super 是将泛型类型的继承关系改变,而不是类型参数)

  • kotlin 的 out in 可以避免这种歧义,但泛型类型的继承关系就变的不太明显了

官方文档的胡言乱语

  • 为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。

  • 在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变: 可以标注 Source 的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此请使用 out 修饰符:

另外,outin 可以写在声明泛型类或者接口的地方(泛型函数不允许声明),这是 ? extends? super 做不到的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 协变
class Box1<out T> {
// 这行会报错,Type parameter T is declared as 'out'
// but occurs in 'in' position in type T
// 类型参数t被声明为“ out”,但在出现在类型 T 的 'in' 位置
fun addValue(value: T) {}
}

// 逆变
class Box2<in T> {
// 这行会报错,Type parameter T is declared as 'in'
// but occurs in 'out' position in type T
// 类型参数 T 声明为 'in',但出现在类型 T 的 'out' 位置
fun getValue(): T {}
}

当你直接将 outin 写在类和接口的声明时,表明你在定义类或者接口的时候,你的意图就是将泛型类型设计成只写不读,或者只读不写。

这时候的 outin 是 kotlin 的独家语法,并且这个语法要求会更严格

借助郭霖大佬的一个图

方法参数位置是 in position ,返回值位置是 out position

kotlin 多类型参数

kotlin 这里也和 java 保持一致,取交集

1
2
val hm = HashMap<People, Animal>()
val map: HashMap<out Animal, in People> = hm

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
abstract class SomeClass<T> {
abstract fun execute() : T
}

class SomeImplementation : SomeClass<String>() {
override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
override fun execute(): Int = 42
}

object Runner {
inline fun <reified S: SomeClass<T>, T> run() : T {
return S::class.java.getDeclaredConstructor().newInstance().execute()
}
}

fun main() {
// T is inferred as String because SomeImplementation derives from SomeClass<String>
val s = Runner.run<SomeImplementation, _>()
assert(s == "Test")

// T is inferred as Int because OtherImplementation derives from SomeClass<Int>
val n = Runner.run<OtherImplementation, _>()
assert(n == 42)
}

可实例化的泛型类型

在 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
2
3
4
5
6
7
8
9
10
11
12
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}

// 实际调用的时候
// 在 Activity 里面
startActivity<MainActivity>(this) {
putExtra("param1", "data1")
putExtra("param2", "data2")
}

特别强调,得是 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.
Comments