Kotlin 泛型详解及简单实例

 Kotlin 泛型详解

概述

一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的限制很大。而OOP的多态采用了一种泛化的机制,在SE 5种,Java引用了泛型。泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

在Kotlin中,依然可以使用泛型,解耦类与函数与所用类型之间的约束,甚至是使用方法都与Java一致。

泛型类

声明一个泛型类

class Box<T>(t: T) {
  var value = t
}

通常, 要创建这样一个类的实例, 我们需要指定类型参数:

val box: Box<Int> = Box<Int>(1)

但是, 如果类型参数可以通过推断得到, 比如, 通过构造器参数类型, 或通过其他手段推断得到, 此时允许省略类型参数:

val box = Box(1) // 1 的类型为 Int, 因此编译器知道我们创建的实例是 Box<Int> 类型

泛型函数

泛型函数与其所在的类是否是泛型没有关系。泛型函数使得该函数能够独立于其所在类而产生变化。在<Thinking in Java>有这么一句话:无论何时只要你能做到,你就应该尽量使用泛型方法,也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更明白。这种泛型使用思想,在Kotlin中依然可以延续。

下面我们声明了一个泛型函数doPrintln,当T是一个Int类型时,打印其个位的值;如果T是String类型,将字母全部大写输出;如果是其他类型,打印“T is not Int and String”。

fun main(args: Array<String>) {
  val age = 23
  val name = "Jone"
  val person = true

  doPrintln(age) // 打印:3
  doPrintln(name) // 打印:JONE
  doPrintln(person) // 打印:T is not Int and String
}

fun <T> doPrintln(content: T) {

  when (content) {
    is Int -> println(content % 10)
    is String -> println(content.toUpperCase())
    else -> println("T is not Int and String")
  }
}

注:

  1. 类型参数放在函数名称之前。
  2. 如果在调用处明确地传入了类型参数, 那么类型参数应该放在函数名称 之后。如果不传入参数类型,编译器会根据传入的值自动推断参数类型。

擦除的神秘之处

下面我们先看一段代码:

class Box<T>(t : T) {
  var value = t
}

fun main(args: Array<String>) {
  var boxInt = Box<Int>(10)
  var boxString = Box<String>("Jone")

  println(boxInt.javaClass) // 打印:class com.teaphy.generic.Box
  println(boxString.javaClass) // 打印:class com.teaphy.generic.Box
}

现声明了一个泛型类Box<T>,在不同的类型的类型在行为方面肯定不一样,但是在我们获取其所在类时,我们只是得到了“class com.teaphy.generic.Box”。在这里我们不得不面对一个残酷的现实:在泛型内部,无法获得任何有关泛型参数类型的信息。

不管是Java还是Kotlin,泛型都是使用擦除来实现的,这意味着当你在使用泛型时,任务具体的类型信息都被擦除的,你唯一知道的就是你再使用一个对象。比如,Box<String>和Box<Int>在运行时是想的类型,都是Box的实例。在使用泛型时,具体类型信息的擦除是我们不不懂得不面对的,在Kotlin中也为我们提供了一些可供参考的解决方案:

  • 类型协变
  • 类型投射
  • 泛型约束

类型协变

在类型声明时,使用协变注解修饰符(in或者out)。于这个注解出现在类型参数的声明处, 因此我们称之为声明处的类型变异。如果在使用泛型时,使用了该类型编译了会有什么效果呢?

假设我们有一个泛型接口Source<in T, out R>, 其中T由协变注解in修饰,R由协变注解Out修饰.

internal interface Source<in T, out R> {
  fun mapT(t: T): Unit
  fun nextR(): R
}
  • in T: 来确保Source的成员函数只能消费T类型,而不能返回T类型
  • out R:来确保Source的成员函数只能返回R类型,而不能消费R类型

从上面的解释中,我们可以清楚的知道了协变注解in和out的用意,其实际上是定义了类型参数在该类或者接口的用途,是用来消费的还是用来返回的,对其做了相应的限定。

类型投射

上面我们已经了解到了协变注解in和out的用意,下面我们将会用in和out,做一件有意义的事,看下面代码

fun copy(from: Array<out String>, to: Array<Any>) {
  // ...
}

fun fill(dest: Array<in String>, value: String) {
  // ...
}

对于copy函数中中,from的泛型参数使用了协变注解out修饰,意味着该参数不能在该函数中消费,也就是说在该函数中禁止对该参数进行任何操作。

对于fill函数中,dest的泛型参数使用了协变注解in修饰,Array<in String>与Java的 Array<? super String> 相同, 也就是说, 你可以使用CharSequence数组,或者 Object 数组作为 fill() 函数的参数

这种声明在Kotlin中称为类型投射(type projection),类型投射的主要用于对参数做了相对因的限定,避免了对该参数类的不安全操作。

星号投射

有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓”安全地使用”是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型.

对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection):

  1. 假如类型定义为 Foo<out T> , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper ,Foo<> 等价于 Foo<out TUpper> . 它表示, 当 T 未知时, 你可以安全地从 Foo<> 中 读取TUpper 类型的值.
  2. 假如类型定义为 Foo<in T> , 其中 T 是一个反向协变的类型参数, Foo<> 等价于 Foo<inNothing> . 它表示, 当 T 未知时, 你不能安全地向 Foo<> 写入 任何东西.
  3. 假如类型定义为 Foo<T> , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper , 对于读取值的场合, Foo<*> 等价于 Foo<out TUpper> , 对于写入值的场合, 等价于 Foo<in Nothing> .

如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface Function<in T, out U> , 那么可以出现以下几种星号投射:

  1. Function<*, String> , 代表 Function<in Nothing, String> ;
  2. Function<Int, *> , 代表 Function<Int, out Any?> ;
  3. Function<, > , 代表 Function<in Nothing, out Any?> .

注意: 星号投射与 Java 的原生类型(raw type)非常类似, 但可以安全使用

泛型约束

对于一个给定的类型参数, 所允许使用的类型, 可以通过泛型约束(generic constraint) 来限制。

上界

最常见的约束是 上界(upper bound):

fun <T : Comparable<T>> sort(list: List<T>) {
  // ...
}

冒号之后指定的类型就是类型参数的 上界(upper bound): 对于类型参数 T , 只允许使用 Comparable<T>的子类型. 比如:

sort(listOf(1, 2, 3)) // 正确: Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误: HashMap<Int, String> 不是
Comparable<HashMap<Int, String>> 的子类型

如果没有指定, 则默认使用的上界是 Any? . 在定义类型参数的尖括号内, 只允许定义唯一一个上界. 如果同一个类型参数需要指定多个上界, 这时就需要使用单独的 where 子句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T> where T : Comparable,
T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

时间: 2017-06-02

Kotlin 基础教程之泛型

Kotlin 支持泛型, 语法和 Java 类似. 例如,泛型类: class Hello<T>(val value: T) val box = Box<Int>(1) val box1 = Box(2) 泛型函数: fun <T> foo(item: T): List<T> { // do something } val list = foo<Int>(1) fun <T> T.toString2(): String { // 扩展

Kotlin 单例实例详解

Kotlin 单例实例详解 单例的实现方法,可以通过同伴对象,或者 lazy. 示例: class Hello private constructor() { companion object { val instance = Hello() } } 通过 lazy 实现 class Hello private constructor() { private object Holder { val INSTANCE = Hello() } companion object { val insta

Kotlin 基础语法实例详解

Kotlin 基础语法实例详解 包 定义和引入Java一样,在文件开头, 行结束不需要" ; " package com.test.hello import android.os.Bundle 变量 只读变量,val 开头,初始化后不能再赋值,相当于Java的 final 变量 val a: Int = 1 val b = 1 //类型自动推断为Int val c: Int //没有初始化时必须指定类型 c = 1 //初始化 可变变量, var 关键字开头 var x = 10 x

Kotlin 基础教程之类、对象、接口

Kotlin 基础教程之类.对象.接口 Kotlin中类.接口相关概念与Java一样,包括类名.属性.方法.继承等,如下示例: interface A { fun bar() fun foo() { // 可选方法体 } } class Child: A { override fun bar() { // todo } override fun foo() { super.foo() } } class 构造器 Kotlin 中的类可以有一个 主构造器, 以及一个或多个次构造器, 主构造器是类头

详解Kotlin的空指针处理

详解Kotlin的空指针处理 Kotlin的空指针处理相比于java有着极大的提高,可以说是不用担心出现NullPointerException的错误,kotlin对于对象为null的情况有严格的界定,编码的阶段就需要用代码表明引用是否可以为null,为null的情况需要强制性的判断处理. 咋看一下这些在java里面其实也有,问题是一般开发中不写也是可以的(大部分开发不会花很多时间考虑这些),等出了空指针错误再一个个打补丁.这样往往会遗漏很多空指针,后期的解决仅仅是做一个if判断,没有从根源解决

详解Kotlin中的变量和方法

详解Kotlin中的变量和方法 变量 Kotlin 有两个关键字定义变量:var 和 val, 变量的类型在后面. var 定义的是可变变量,变量可以被重复赋值.val 定义的是只读变量,相当于java的final变量. 变量的类型,如果可以根据赋值推测,可以省略. var name: String = "jason" name = "jame" val max = 10 常量 Java 定义常量用关键字 static final, Kotlin 没有static,

Kotlin 基础教程之注解与java中的注解比较

Kotlin 的注解完全兼容 Java 的注解. 声明注解 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class Fancy 可以通过向注解类添加元注解(meta

Kotlin 基础教程之数组容器

Kotlin 基础教程之数组容器 Arrays Kotlin 标准库提供了arrayOf()创建数组, **ArrayOf创建特定类型数组 val array = arrayOf(1, 2, 3) val countries = arrayOf("UK", "Germany", "Italy") val numbers = intArrayOf(10, 20, 30) val array1 = Array(10, { k -> k * k

Kotlin基础教程之数据类型

Kotlin基础教程之数据类型 一切都是对象. 在Kotlin中一切都是对象.Kotlin有一些基本类型Boolean,Byte,Shot,Int,Long,Float,Double 在Kotlin中没有原始类型,以上这些类型都是对象,比如 运行结果如下 Kotlin支持16进制字面值,二进制字面值和科学记数法,官方文档中没有提到8进制 所有基本的几个数据类型不会进行显式类型转换,这一点尤为重要!那么如何进行转换呢?如下: 可以使用toLong,toInt,toShort,toChar等函数进行

Kotlin 基础教程之反射

Kotlin 基础教程之反射 概述 反射是语言与库中的一组功能, 可以在运行时刻获取程序本身的信息.在Kotlin中,不仅可以通过发射获取类的信息,同时可以获取函数和属性的信息.也就是说,在在运行时刻得到一个函数或属性的名称和数据类型) 可以通过简单的函数式, 或交互式的编程方式实现. 在Java平台上, 使用反射功能所需要的运行时组件是作为一个单独的JAR文件发布的( kotlinreflect.jar). 这是为了对那些不使用反射功能的应用程序, 减少其运行库的大小. 如果你需要使用反射,

Kotlin 基础教程之异常

Kotlin 基础教程之异常 概述 在Kotlin-null的处理里提到的NPE,它就是一个异常.而,异常是程序运行过程中出现的错误.在Kotlin中,所有的异常都继承于Throwable.对于每一个异常而言,它不仅仅包括异常的信息,还可以选择性包括异常的原因,而其原因也是一个异常的实例. 抛出异常 使用 throw表达式抛出异常: throw MyException("Hi There!") 捕获异常 如果在函数内部抛出了异常(或者在函数内部调用的其他函数抛出了异常),这个函数将在抛

Kotlin基础教程之操作符与操作符重载

Kotlin基础教程之操作符与操作符重载 Kotlin操作符的使用方法与其他语言差不多,最大的特点就在于infix function call(事实上infix function call并不是操作符的特性,而是函数的特性)和操作符重载. 可以看到在Kotlin中大部分的操作符都与一个函数相对应,之所以这样做,大概是为了进行操作符重载. Kotlin官方文档中强调"=赋值"操作在Kotlin中不是操作符,换句话说类似于x = y = z这样的连续赋值并不被允许(甚至赋值操作也不能和常规

Kotlin基础教程之函数定义与变量声明

Kotlin基础教程之函数定义与变量声明 可以看到,函数定义就是 <访问控制符> <函数名> <参数列表> <:返回类型(不写就是无返回类型)> { 函数体 } 单语句函数可以简写,比如add函数和add1函数效果是一样的 变量定义 var <标识符> : <类型> = <初始化值> 常量定义 val <标识符> : <类型> = <初始化值> 常量与变量都可以没有初始化值,但是在引用前

kotlin基础教程之类和继承

kotlin基础教程之类和继承 类声明 使用class关键字声明类,查看其声明格式: : modifiers ("class" | "interface") SimpleName typeParameters? primaryConstructor? (":" annotations delegationSpecifier{","})? typeConstraints (classBody? | enumClassBody)