Kotlin泛型、注解和异常
Kotlin 泛型
泛型类型参数
- 类型形参,如 List
<>
里的 T 叫类型形参
- 类型实参,List,String 叫
类型实参
泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。
正常情况下,编译器可以推导出你创建的类型。但你想创建一个空的列表,这样就没有任何可以推导出类型实参的线索,你就得显式地指定它(类型形参)
创建列表来说,可以在列表变量声明中(引用)说明泛型的类型,也可以选择在创建列表的函数中说明类型实参:
1
2
val readers: MutableList<String> = mutableListOf("test1", "test2")
val readers2 = mutableListOf<String>("test1", "test2")
Kotlin 始终要求实参要么被显式地说明,要么能被编译器推导出来。Kotlin 从一开始就支持泛型,它的类型实参必须定义
泛型类、函数和属性
泛型函数
泛型函数,很多库函数是泛型函数
1
2
3
4
5
public fun <T> List<T>.slice(indices: IntRange): List<T> {
if (indices.isEmpty()) return listOf()
return this.subList(indices.start, indices.endInclusive + 1).toList()
}
// 函数的返回类型是List<T>,函数的类型形参是T
泛型属性
和泛型函数一样,可以声明泛型的扩展属性
1
2
val <T> List<T>.penultimate: T
get() = this[size - 2]
普通(不是扩展属性)属性不能拥有类型参数,不能在一个类的属性中存储多个不同类型的值,因此声明泛型属性非扩展函数没有任何意义。
1
val <T> x:T = null
泛型类
基本和 Java 语法意义,在类名称后加上 <>
类型参数约束
类型参数约束可以限制作为泛型类和泛型函数的类型实参的类型。
上界约束
一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参必须是这个具体类型或者它的子类型。
1
2
3
4
// Java中
<T extends Number> T sum(List<T> list)
// kotlin中
fun <T : Number> List<T>.sum(): T
例子:找出两个条目中最大值的泛型函数:
1
2
3
fun <T : Comparable<T>> max(first: T, second: T): T {
return if (first > second) first else second
}
在一个类型参数上指定多个约束,给定的 seq 以句号结尾
1
2
3
4
5
fun <T> ensureTrailingPerion(seq: T) where T : CharSequence, T : Appendable {
if (!seq.endsWith(".")) {
seq.append(".")
}
}
类型形参非空
没有指定上界的类型形参都会使用 Any?
这个默认的上界。
类型形参非空,用 Any
替代 Any?
,也可以指定任意非空类型作为上界,让类型参数非空。
1
2
3
4
5
class Processsor<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
*
号
Java 中单个 ?
号也能作为泛型通配符使用,相当于 ? extends Object
。
在 Kotlin 中有等效的写法:*
号,相当于 out Any
。
1
var list: List<*>
和 Java 不同的地方是,如果你的类型定义里已经有了 out 或者 in,那这个限制在变量声明时也依然在,不会被 * 号去掉。
比如你的类型定义里是 out T : Number 的,那它加上 <*> 之后的效果就不是 out Any,而是 out Number。
where
Java 中声明类或接口的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集,同时这个边界是可以设置多个,用 & 符号连接:
1
2
3
// 👇 T 的类型必须同时是 Animal 和 Food 的子类型
class Monster<T extends Animal & Food>{
}
Kotlin 只是把 extends 换成了 :
冒号,设置多个边界可以使用 where
关键字:。
1
class Monster<T> where T : Animal, T : Food
reified
具体可见 Kotlin运行时泛型
章节
由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了
比如你不能检查一个对象是否为泛型类型 T 的实例:
1
2
3
4
5
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof
System.out.println(item);
}
}
Kotlin 里同样也不行:
1
2
3
4
5
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}
在 Java 中的解决方案通常是额外传递一个 Class 类型的参数,然后通过 Class#isInstance 方法来检查
1
2
3
4
5
6
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
System.out.println(item);
}
}
Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:
1
2
3
4
5
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 这里就不会在提示错误了
println(item)
}
}
kotlin 协变 out & 逆变 in
和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。
- 使用关键字
out
来支持协变,等同于 Java 中的上界通配符? extends
。 - 使用关键字
in
来支持逆变,等同于 Java 中的下界通配符? super
。
out 协变 只能读不能写
out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;对应 Java 中的 ? extend
。
in 逆变 只能写不能读
in 表示它只用来输入,不用来输出,你只能写我不能读我;对应 Java 中的 ? super T
。
如 List<? extends Foo>
,将无法调用 add() 和 set() 方法,但并不代表这个集合对象的值是不变的 (immutable),如 clear() 方法可以清空集合中的值,通配符类型唯一能够确保的仅仅是类型安全,对象值的不可变性是另外一个问题。
- 能接收的类型为类型实参为 Foo 及子类
- 写的数据是 T 或 T 的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val apples4: MutableList<Apple> = mutableListOf()
val fruit4: MutableList<Fruit> = mutableListOf()
val smallApple4: MutableList<SmallApple> = mutableListOf()
val any4: MutableList<Any> = mutableListOf()
val fruit4_0: MutableList<in Apple> = apples4 // ok
fruit4_0.add(Apple()) // ok
fruit4_0.add(SmallApple()) // ok
// fruit4_0.add(Fruit()) // 编译错误
// fruit4_0.add(Any()) // 编译错误
val fruit4_1: MutableList<in Apple> = fruit4 // ok
fruit4_1.add(Apple()) // ok
fruit4_1.add(SmallApple()) // ok
// fruit4_1.add(Fruit()) // 编译错误
// fruit4_1.add(Any()) // 编译错误
val fruit4_2: MutableList<in Apple> = any4 // ok
fruit4_2.add(Apple()) // ok
fruit4_2.add(SmallApple()) // ok
// fruit4_2.add(Fruit()) // 编译错误
// fruit4_2.add(Any()) // 编译错误
// val fruit4_3: MutableList<in Apple> = smallApple4 // 编译错误
Java 和 Kotlin 数组中的协变和逆变
Java 里的数组是支持协变的,而 Kotlin 中的数组 Array 不支持协变
1
2
3
4
Fruit[] fruitsArray = new Fruit[3]; // Java数组支持协变
fruitsArray[0] = new Fruit();
fruitsArray[0] = new Apple();
fruitsArray[0] = new SmallApple();
因为在 Kotlin 中数组是用 Array 类来表示的,这个 Array 类使用泛型就和集合类一样,所以不支持协变。
Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变
Java 中的 List 不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。
在 Kotlin 中,实际上 MutableList 接口才相当于 Java 的 List。Kotlin 中的 List 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。
1
2
// kotlin
public interface List<out E> : Collection<E>
Kotlin 中的 List 支持协变
1
2
3
4
val apple5: List<Apple> = listOf()
val smallApple5: List<SmallApple> = listOf()
val fruit5_0: List<out Fruit> = apple5
val fruit5_1: List<out Fruit> = smallApple5
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 实现一个 fill 函数,传入一个 Array 和一个对象,将对象填充到 Array 中,要求 Array 参数的泛型支持逆变(假设 Array size 为 1)。 https://kaixue.io/kotlin-generics/
*/
fun <T> fill(array: Array<in T>, t: T) {
array[0] = t
}
/**
* 实现一个 copy 函数,传入两个 Array 参数,将一个 Array 中的元素复制到另外个 Array 中,要求 Array 参数的泛型分别支持协变和逆变。
*/
fun <T> copy(src: Array<out T>, dst: Array<in T>) {
for (index in src.indices) {
val source = src[index]
dst[index] = source
}
}
Kotlin 运行时泛型
1、运行时的泛型:类型检查和转换
和 Java 一样,kotlin 的泛型在运行时也被擦除了。
List<*>
星号投影,和 Java 中的 List<?>
一样。
用 is
可以检查,
2、声明带实化类型参数的函数
实化:消除运行时类型擦除对 Kotlin 的影响,通过将函数声明为 inline 来解决,保证其类型实参不被擦除。
1
fun <T> isA(value: Any) = value is T // 编译报错
内联函数:inline 函数的类型形参能够被实化,可以在运行时引用实际的类型实参;
内联函数,编译器会把每一次函数调用都转换成函数实际的代码实现;使用内联函数还可能提升性能,如果函数使用了 lambda 实参,lambda 的代码也会内联,不会创建任何匿名类。
reified声明的类型参数不会在运行时被擦除
1
inline fun <reified T> isA(value: Any) = value is T // 可以编译了
例子,标准库函数 filterIsInstance
,接收一个集合,选择指定的类型,返回集合中是该类型的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
inline fun <reified T> Iterable<*>.filterIsInstance1(): List<T> {
val dest = mutableListOf<T>()
for (ele in this) {
if (ele is T) {
dest.add(ele)
}
}
return dest
}
// 使用
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
println(items.filterIsInstance1<String>())
注意:带 reified 类型参数的 inline 函数不能再 Java 代码中调用,普通的内联函数可以像常规函数那样在 Java 中调用(它们可以被调用而不能被内联);带实化类型参数的函数需要额外的处理,来把类型实参的值替换到字节码中,所以它们必须永远是内联的,这样它们不可能用 Java 那样普通的方法调用
一个内联函数可以有多个实化类型参数,也可以同时拥有非实化类型参数和实化类型参数。
为了保证良好的性能,函数太大,最好不不依赖实化类型参数的代码抽取到单独的非内联函数中
2-1 实化类型参数 (reified) 替代类引用 (Class)
实化参数可以用来替代接收 java.lang.Class
类型参数的 API,如 JDK 中的 ServiceLoader,用 kotlin 重写
1
2
3
inline fun <reified T> loadService(): ServiceLoader<T>? {
return ServiceLoader.load(T::class.java)
}
Android 中的 startActivity:
1
2
3
4
5
6
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java) // 相当于T.class
startActivity(intent)
}
// 调用
startActivity<DetailActivity>()
2-2 实化类型参数的限制
按照下列方式来使用实化类型参数:
- 用在类型检查和类型转换中 ( is !is as as?)
- 使用 kotlin 反射 API (::class)
- 获取对应的 java.lang.Class (::class.java)
- 作为调用其他函数的类型实参
不能做下面的:
- 创建指定为类型参数的类的实例
- 调用类型参数类的伴生对象的方法
- 调用带实化类型参数的时候使用非实化类型形参作为类型实参
- 把类、属性或者非内联函数的类型参数标记为 reified
Kotlin 之注解
使用注解的语法和 Java 完全一样,而声明自己注解类的语法略有不同。
Kotlin 注解基础
声明应用注解
@Deprecated
注解被增强了,idea 不仅可以提示应该用哪个函数替代,还会提供一个快速自动修正。
1
2
3
4
5
6
@Deprecated("User removeNew(index) instead.", ReplaceWith("removeNew(index)"), DeprecationLevel.WARNING)
fun removeOld(index: Int) {
}
fun removeNew(index: Int) {
}
注解只能有如下类型的参数:
基本数据类型,字符串,枚举,类引用,其他的注解类,以及这些类型的数组
- 要把一个类指定为注解的实参,需要在类名后加上
::class
1
@MyAnnotation(MyClass::class)
- 要把另一个注解指定为实参,去掉注解名称前面的
@
,如@Deprecated 注解的实参@ReplaceWith 是一个注解参数 - 要把一个数组指定为一个实参,使用
arrayOf
函数(如果注解类是在 Java 中声明的,命名为 value 的形参按需自动地被转换为可变长度的形参,所以不用 arrayOf 函数就可以提供多个实参)
1
@RequestMapping(path = arrayOf("/foo","/bar"))
注解实参需要是编译期常量,用 const
修饰符标记。
const 标注的属性可以声明在一个文件的顶层或者一个 Object 之中,而且必须初始化为基本数据类型或 String 类型的值
注解目标
Kotlin 中单个声明会对应多个 Java 声明,而且它们每个都能携带注解,如一个 Kotlin 属性对应一个 Java 字段、一个 getter、可能还有 setter 和它的参数;而一个主构造方法中声明的属性还多拥有一个对应的元素:构造方法的参数。因此,说明这些元素中哪些需要注解十分必要
使用点目标声明被用来说明要注解的元素。点目标放在 @
符号和注解名称直接,并用 :
和注解名称隔开:
1
2
3
4
5
6
7
8
9
10
11
@get:Rule // 注解@Rule被应用到了属性getter上
// @get 使用点目标
// Rule 注解名称
class HasTempFolder {
@get:Rule
private val folder = HasTempFolder()
@Test
fun testUsingTempFolder() {
}
}
使用 Java 中声明的注解来注解一个属性,它会被默认地应用到相应的字段上,Kotlin 也可以让你声明被直接对应到属性上的注解:
- property Java 的注解不能应用这种使用点目标
- field 为属性生成的字段
- get 属性的 getter
- set 属性的 setter
- receiver 扩展函数或扩展属性的接收者参数
- param 构造方法的参数
- setparam 属性 setter 的参数
- delegate 为委托属性存储委托实例的字段
- file 包含在文件中声明的顶层函数和属性的类
任何应用到 file 目标的注解都必须放在文件的顶层,放在 package 指令之前,@JvmName
是常见的应用到文件的注解之一,它改变了对应类的名称。
Kotlin 允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。
用注解控制 Java API,有些注解替代了 Java 的关键字,其他的注解则是被用来改变 Kotlin 声明对 Java 调用者的可见性
@Volatile
和@Strictfp
充当了 Java 关键字 volatile 和 strictfp 的替身- 其他的注解被用来改变 Kotlin 声明对 Java 调用者的可见性
@JvmName
改变由 Kotlin 生成的 Java 方法或字段的名称@JvmStatic
用在对象声明或者伴生对象的方法上,把它们暴露成 Java 的静态方法@JvmOverloads
指导 kotlin 编译器为带默认参数值的函数生成多个重载函数@JvmField
应用于一个属性,把这个属性暴露成一个没有访问器的公有 Java 字段
案例:
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
@MyAnnoClass
class Foo @MyAnnoClass constructor(@MyAnnoClass var n: String) { // 类的主构造器添加注解
var x: Int? = null
@MyAnnoClass set
@MyAnnoClass get
@get:MyAnnoClass // get添加注解
@set:MyAnnoClass // set添加注解
var y: String? = ""
// 属性添加注解
@MyAnnoClass
var z: Double? = 0.0
@MyAnnoClass
fun testFoo(@MyAnnoClass s: String) {
}
}
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION,
AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FIELD, AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class MyAnnoClass
注解到 Lambda 表达式
注解 Lambda,Lambda 表达式的函数体内容将会生成一个 invoke() 方法,注解将被添加到这个方法上
Kotlin 中的元注解
- @Target
指定这个注解可被用于哪些元素 - @Retention
指定这个注解的信息保留的时机 - @Retention
允许在单个元素上多次使用同一个注解 - @MustBeDocumented
表示这个注解是公开 API 的一部分,在自动产生的 API 的文档的类或函数签名中,应该包含这个注解的信息
1
2
3
4
5
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class MyAnnoClass
Kotlin 注解类
注解类可以拥有带参数的构造器
1
2
3
4
5
@Special("hacket")
class TestSpecial {
}
annotation class Special(val why: String) {
}
注解类的构造器只能用下面的类型:
- 与 Java 基本类型对应的数据类型(Int、Long 等)
- String
- 枚举类
- KClass
- 其他注解类
为注解构造器添加参数时,注意 2 点:
- 参数类型只能使用 val,不能用 var,不加也不行
- 当参数类型是另外一个注解类时,该注解类的名字前面不能使用
@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
annotation class Person(val value: String, val num: Int)
enum class MyEnum {
VALUE1, VALUE2
}
// 枚举类和注解类
annotation class NewWorld(val value: MyEnum, val p: Person)
annotation class Ann(val arg1: KClass<*>, val args2: KClass<out Any>)
@Ann(String::class, Int::class) // KCLass,Java类也可以正常访问
class TestAnn {
}
使用注解定制 JSON 序列化反序列化
JKid 库为例@JsonExclude
标记一个属性,这个属性应该排除在序列化和反序列化之外@JsonName
代表这个属性的 JSON 键值对之中的键应该是一个给定的字符串,而不是属性的名称
声明注解
注解类只是用来定义关联到声明和表达式的元数据的结构,他们不能包含任何代码,编译器禁止为一个注解类指定类主体
1
annotation class JsonExclude
对拥有参数的注解来说,在类的构造方法中声明这些参数,对注解类的所有参数来说,val 是强制的
1
annotation class JsonExclude(val name: String)
Java 中有 value 方法,而 kotlin 注解拥有一个 name 属性,kotlin 注解调用就是常规的构造方法调用,可以用命名实参语法让实参名称变为显示的,或可以省略掉这些实参。
1-1 元注解
@Target
注解应用的目标
Java 中无法使用目标为PORPERTY
的注解,除了添加一个AnnotationTarget.FIELD
,这样注解可以在 Java 和 Kotlin 中的属性或字段都可以使用@Retention
Java 默认是保存的 CLASS
而 Kotlin 默认注解拥有 RUNTIME 保留
1-2 使用类作为注解参数
1
2
3
4
annotation class DeserializeInterface(val targetClass: KClass<out Any>) // 没有写上out就不能传递Company::class,唯一允许的实参是Any::class;out关键字说明允许引用那些继承Any的类,而不仅仅是引用Any自己
// 引用
DeserializeInterface(Company::class) // Company::class表示KClassM<Company>
KClass 对应的是 Java 中的 java.lang.Class
类型,它用来保存 kotlin 类的引用。
1-3 使用泛型类做注解参数
kotlin 之异常处理
基本语法和 Java 差不多
Kotlin 不存在 checked exception。
try{}catch(e:Exception){}finally{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun calc() {
while (true) {
println("请输入第一个数字:")
var num1Str = readLine(); // 可能为null
println("请输入第二个数字:")
var num2Str = readLine(); // 可能为null
try {
var num1 = num1Str!!.toInt()
var num2 = num2Str!!.toInt()
println("$num1+$num2=" + (num1 + num2))
} catch(e: Exception) {
println("大哥,你输入的数字不正确:")
e.printStackTrace()
}
}
}
try{}catch(){}可作为表达式
表达式的返回值,要么是 try 代码块内最后一个表达式的值,要么是 catch 代码块内最后一个表达式的值,finally 代码内不影响 try 表达式的结果
1
2
3
4
5
6
7
8
9
fun testException() {
var var1: Nothing? = null
val a = try {
parseInt(var1)
} catch (e: Exception) {
null
}
println(a)
}
1
2
3
4
5
java.lang.NumberFormatException: For input string: "123a"
kotlin.Unit
kotlin.KotlinNullPointerException
kotlin.Unit