# Kotlin教程 - 10 泛型

Kotlin 中的泛型允许将类型参数化,使代码更通用且可重用。这提高了代码的灵活性和可维护性。在 Kotlin 中,泛型常用于定义集合类和类似数据结构。

在 Kotlin 中使用容器时已经接触过泛型。例如:

fun main() {
    // 不使用泛型,没有类型限制
    val nameList1 = mutableListOf("Doubi", "Erbi", "Niubi", 111)

    // 使用泛型进行类型限制,只能放字符串类型
    val nameList2 = mutableListOf<String>("Doubi", "Erbi", "Niubi")
    // nameList2.(123) // 报错,mutableListOf<String>中只能放String类型数据
}
1
2
3
4
5
6
7
8

同样,Map 也可以指定泛型:

fun main() {
    // 不使用泛型,key和value可以是任意类型
    val map1 = mapOf(123 to "one", "name" to "Doubi", "age" to 12)

    // 使用泛型,key和value只能是字符串,否则报错
    val map2 = mapOf<String, String>("name" to "Doubi", "age" to "12")
}
1
2
3
4
5
6
7

# 10.1 泛型类

在开发中,经常需要请求服务器获取各种信息,如学生信息或老师信息。我们会定义相关的业务类:

定义学生类:

data class Student(val name: String, val age: Int)
1

定义老师类:

data class Teacher(val name: String, val age: Int)
1

我们一般会定义一个返回的结果类,然后在类中封装返回码和返回的数据,通过这个结果类返回学生或老师的信息。

举个栗子:

data class DataResult(val messageCode: Int, val data: Any?)
1

因为我们返回的数据可能是不同的类型,所以我们将数据的类型定义为 Any 类型。因为有时候可能只需要返回信息码,没有数据信息,所以这里定义 data 可以为空,使用 Any? 定义。

测试一下:

fun main() {
    // 封装数据
    val stu = Student("Doubi", 12);
    val result1 = DataResult(200, stu);

    val tea = Teacher("Niubi", 32);
    val result2 = DataResult(200, tea);

    // 获取数据
    if (result1.data is Student) {
        val stu2 = result1.data as Student // 需要进行类型转换
        print(stu2.name);
    }

    if (result2.data is Teacher) {
        val tea2 = result2.data as Teacher // 需要进行类型转换
        print(tea2.name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在上面的代码中,我们先将学生和老师的信息封装到了 DataResult 中。然后重新从 DataResult 获取老师和学生的信息。

但是获取到信息以后,需要进行类型判断,或这使用 as 进行类型转换,如果不进行类型判断,使用 val stu2 = result1.data 直接赋值代码会报错,因为 dataAny? 类型。

这个时候我们可以使用泛型,使用泛型我们可以编写更通用、可重用的代码,以适应不同类型的数据。

修改上面的代码:

class DataResult<T>(val messageCode: Int, val data: T?)
1

在类名后面添加 <T> 表示当前类声明的泛型,泛型可以任意字母,一般使用 T ,然后可以使用 T 声明变量 val data: T?,如果需要用到多个泛型,也是可以定义多个的,比如: <T,E>

使用泛型后,测试一下:

fun main() {
    val stu = Student("Doubi", 12)
    val result1 = DataResult(200, stu)

    val tea = Teacher("Niubi", 32)
    val result2 = DataResult(200, tea)

    val stu2: Student? = result1.data // 不需要进行类型转换
    println(stu2?.name)

    val tea2: Teacher? = result2.data // 不需要进行类型转换
    println(tea2?.name)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这个在创建 DataResult 对象的时候,使用泛型,然后在通过对象来获取 data 的时候,就不用类型转换了。

# 10.2 泛型函数

泛型函数是一种具有泛型类型参数的函数,可以在函数签名中使用类型参数来实现更通用和灵活的代码。泛型函数可以在参数、返回类型或函数体中使用这些类型参数。

举个栗子:

我们写一个函数获取列表的最后一个元素:

// 获取列表的最后一个元素
fun <T> getLast(list: List<T>): T {
    return list.last();
}

fun main() {
    val intList = listOf(1, 2, 3)
    val strList = listOf("Doubi", "Niubi", "Shabi")

    val i: Int = getLast(intList);  // 不用类型转换
    println(i);

    val s: String = getLast(strList)    // 不用类型转换
    println(s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中定义了一个泛型方法 fun <T> getLast(list: List<T>): Tfun 后面的 <T> 定义函数中用到的泛型类型,最后面的 T 表示返回值的类型。

通过使用泛型定义,我们不同数据类型的列表都可以调用,而且在获取到元素后,不要类型转换。

# 10.3 泛型接口

在 Kotlin 中,泛型接口是指可以具有泛型类型参数的接口。泛型接口允许在接口定义中使用类型参数,并在实现接口时指定具体的类型参数。

举个栗子:

定义一个泛型接口。

// 定义一个泛型接口
interface Repository<T> {
    fun save(data: T)
    fun findById(id: Int): T
}
1
2
3
4
5

下面写一个类来实现这个泛型接口:

// 实现泛型接口
class UserRepository : Repository<String> {
    override fun save(data: String) {
        println("Saving user: $data")
    }

    override fun findById(id: Int): String {
        return "User with ID $id"
    }
}
1
2
3
4
5
6
7
8
9
10

我们在实现接口的时候,指定了泛型的类型,那么类中用到泛型的地方,类型就确定确定了。

我们还可以定义其他的子类来实现该接口,并使用不同的数据类型。

调用:

fun main() {
    val userRepository = UserRepository()
    userRepository.save("Doubi")
    val user = userRepository.findById(1)
    println(user)
}
1
2
3
4
5
6

# 10.4 泛型约束

Kotlin 中的泛型约束允许你限制泛型类型参数可以是哪种类型,这有助于提高代码的安全性和可靠性。通过泛型约束,你可以指定泛型类型参数必须符合特定的条件,例如必须是某个类的子类、必须实现某个接口,或者必须具有某些属性。

# 1 类和接口约束

我们可以在泛型类中约束泛型必须是某个类的子类。

举个栗子:

interface Engine {
    fun getName(): String
}
class Car : Engine {
    override fun getName(): String {
        return "小汽车"
    }
}

class Bike

class Garage<T : Engine> {
    // 加油
    fun putGas(vehicle: T) {
        println("正在给 ${vehicle.getName()} 加油")
    }
}

fun main() {
    val carGarage = Garage<Car>()
    val bikeGarage = Garage<Bike>() // 报错 Bike不属于Engine接口的实现

    val car = Car()

    carGarage.putGas(car)       // 正在给 小汽车 加油
}
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

在上面的代码中,Garage<T : Engine> 限制了泛型 T 必须实现 Engine 接口,所以在下面的代码中 Garage<Bike> 会报错。

再举个例子:

open class Animal(val name: String, val age: Int)

class Cat(name: String, age: Int) : Animal(name, age)

// 限制T 必须是 Animal 类的子类
fun <T : Animal> printInfo(item: T) {
    println("Name: ${item.name}, Age: ${item.age}")
}

fun main() {
    val cat = Cat("Doubi", 5)
    printInfo(cat) // 可以正常调用,因为 Cat 是 Animal 的子类
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面通过 <T : Animal> 限制 T 必须是 Animal类的子类。

# 2 子类和接口同时约束

我们在进行泛型约束的时候,也可以同时约束泛型类型参数实现某个父类和某个接口。可以使用 where 关键字在泛型参数声明时使用逗号 , 分隔不同的约束条件。

举个栗子:

// 定义一个接口
interface Helpful {
    fun help()
}

open class Animal

class Dog : Animal(), Helpful {       // 类实现接口
    override fun help() {
        println("我会看门")
    }
}

fun <T> performSound(animal: T) where T : Animal, T : Helpful {	// 同时约束父类和接口
    animal.help()
}

fun main() {
    val dog = Dog()
    performSound(dog)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在上面的代码中,使用 where T : Animal, T : Helpful 同时约束泛型的父类和接口。

# 10.5 协变与逆变

# 1 协变

什么是协变?

协变允许泛型类型参数在子类型化关系中保持一致,即使泛型类型参数发生向上转换(父类关系),你仍然可以将泛型类型视为相同类型。

什么意思呢?

先举个栗子:

interface Animal {
    fun sound()
}

class Dog : Animal {
    override fun sound() {
        println("汪汪汪!")
    }
}

class Cat : Animal {
    override fun sound() {
        println("喵喵喵!")
    }
}

class Producer<out T>(private val value: T) {
    fun produce(): T {      // 只能作为方法的返回值
        return value
    }
}

fun main() {
    val dogProducer: Producer<Dog> = Producer(Dog())
    val catProducer: Producer<Cat> = Producer(Cat())

    val dog: Dog = dogProducer.produce()
    val cat: Cat = catProducer.produce()

    dog.sound() // 汪汪汪!
    cat.sound() // 喵喵喵!
}
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
28
29
30
31
32

在上面的 Producer 类中,使用了协变,所以泛型只能用于类中的方法返回值,不能用于方法参数。

注意:协变表示泛型只能用作为输出,不能作为输入,也就是只能作为方法返回值,不能作为方法输入的参数。协变使用 out 关键字声明泛型类型。

# 2 逆变

什么是逆变?

逆变表示泛型只能用作为输入,不能作为输出,也就是只能作为方法参数,不能作为方法返回值。协变使用 in 关键字声明泛型类型。

举个栗子:

interface Animal {
    fun sound()
}

class Dog : Animal {
    override fun sound() {
        println("汪汪汪!")
    }
}

class Cat : Animal {
    override fun sound() {
        println("喵喵喵!")
    }
}

class Consumer<in T:Animal> {
    fun consume(value: T) {     // 只能作为方法参数
        value.sound()
    }
}

fun main() {
    val animalConsumer: Consumer<Animal> = Consumer()
    val dog: Dog = Dog()
    val cat: Cat = Cat()

    animalConsumer.consume(dog)     // 汪汪汪!
    animalConsumer.consume(cat)     // 喵喵喵!
}
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
28
29
30

在上面的 Consumer 类中,使用了逆变,所以泛型只能用于类中的方法参数,不能作为返回值。

# 10.6 reified

先看一段代码:

fun <T> checkType(value: Any) {
    if (value is T) {       // 报错
        println("$value is of type ${T::class.simpleName}")     // 报错
    } else {
        println("$value is not of type ${T::class.simpleName}")
    }
}

fun main() {
    checkType<String>("Hello") 		// 输出:Hello is of type String
    checkType<Int>("Hello") 			// 输出:Hello is of type String
}
1
2
3
4
5
6
7
8
9
10
11
12

上面的代码 value is T 是报错的,因为在函数运行时,泛型类型的信息被擦除,因此无法直接访问。

所以如果想在运行的时候获取类型信息,可以使用 reified 关键字。

举个栗子:

inline fun <reified T> checkType(value: Any) {
    if (value is T) {		// 可以获取到类型信息
        println("$value is of type ${T::class.simpleName}")
    } else {
        println("$value is not of type ${T::class.simpleName}")
    }
}

fun main() {
    checkType<String>("Hello") // 输出:Hello is of type String
    checkType<String>(10) // 输出:10 is not of type String
}
1
2
3
4
5
6
7
8
9
10
11
12

reified 是一个关键字,主要与内联函数一起使用。它允许在内联函数中获取泛型类型的实际类。