第五章 类型系统
null引用
在Java中如何解决空指针异常:
- 函数内对于无效值,更倾向于抛异常处理。
- 采用 @NotNull / @Nullable 标注
- 使用专门的Optional 对象对可能为null的变量就行装箱。
可空类型
在Kotlin中,可以在任何类型后面加上“?" Int? 等同于 Int or null
由于null 只能被存储在Java的引用类型中,所以在Kotlin中基本数据类型的可空版本都会使用该类型的包装形式,同样,如果使用基本数据类型作为泛型类的类型参数,Koltin同样会使用该类型的包装类。
- 安全调用 “?.” XXX?.YYY: 当XXX不为null,才调用YYY
- Elvis操作符 “?:” 或合并操作符。
- 非空断言 “!!”
类型检查:在Koltin中用is 代替Java中的 instanceof
当类型需要强制转换时,可以利用 " as "操作符来实现。
Any:非空类型的根类型
Kotlin 把Java 方法参数和返回类型中用到的Object 类型看作Any(更确切的说是当作“平台类型”);
Any? : 所有类型的根类型。
Nothing 与 Nothing? : Kotlin类型层级结构的最底层是Nothing类型。Nothing是没有实例的类型,Nothing类型的表达式不会产生任何值。它只能包含一个值“null”。
自动装箱和拆箱
Kotlin中的Int类型等同于int;
Kotlin中的Int?等同于 Integer;
Kotlin中有Array,但是并不是一种原生的数据结构,而是一种Array类。甚至我们可以将Kotlin中的Array视作为集合类的一部分。
Kotlin中还为原始类型额外引入了一些实用类:IntArray,CharArray,ShortArray等。分别对应了Java中的int[] 、char[]、short[].
IntArray 等 并不是 Array的子类。
Kotlin对原始类型有特殊优化,推荐使用原始类型数组。
泛型
将参数的类型进行参数化。
泛型的优势:
- 类型检查,能在编译时就帮你检查出错误。
- 更加语义化,比如声明一个List< String> ,便可以知道里面存储的是String对象,
- 自动类型转换,获取数据时不需要进行类型强制转换;
- 能写出更加通用的代码。
在Kotlin中使用泛型的格式也是< T>,< S>类似的。
声明一个泛型类和泛型函数:实现定义一个find方法,传入一个对象,若列表中存在该对象,就返回该对象,不存在则返回空;
class SmartList<T>:ArrayList<T>() {
fun find(t:T):T?{
val index = super.indexOf(t)
return if(index >=0) super.get(index) else null
}
}
fun main() {
val smartList = SmartList<String>()
smartList.add("qq")
println(smartList.find("qq"))
println(smartList.find("aa"))
}
除了上述做法,还可以有扩展函数;
fun main() {
val arrayList = ArrayList<String>()
arrayList.add("qq")
println(arrayList.find("qq"))
println(arrayList.find("aa"))
}
fun <T> ArrayList<T>.find(t:T):T?{
val index = this.indexOf(t)
return if(index >=0) this[index] else null
}
类型约束:设定类型上界;
示例;需求为:有一把刀只能用来切长在地上的水果(如西瓜)
interface Ground{}
open class Fruit(val weight:Double)
class Apple(weight: Double):Fruit(weight)
class Banana(weight: Double):Fruit(weight)
class FruitPlate<T:Fruit>(val t:T) // :类型约束,表示只能装水果的盘子
class Watermelon(weight:Double):Fruit(weight),Ground
fun <T> cut(t:T) where T:Fruit,T:Ground{
println("You can cut")
}
fun main() {
cut(Watermelon(5.0))
}
通过 where 关键字,他可以实现泛型参数类型添加多个约束条件。
上面的泛型是在静态时的行为,也就是Kotlin代码编译阶段关于泛型的知识点。
public class TestArray {
public static void main(String[] args) {
Apple[] applyArray = new Apple[10];
Fruit[] fruitArray = applyArray;
//fruitArray[0] = new Banana(0.5);
List<Apple> appleList = new ArrayList<Apple>();
//List<Fruit> fruitList = appleList;
System.out.println(applyArray.getClass());
System.out.println(appleList.getClass());
}
}
>>>class [Lchapter5.Apple;
>>>class java.util.ArrayList
数组是协变的,而List是不变的。简单来说就是Object[] 是所有对象数组的父类,而List< Object> 却不是List< T> 的父类。
从上面的打印结果可以知道,数组在运行时是可以获取自身的类型,而List< Apple>在运行时只知道自己是一个List,而无法获取泛型参数的类型。而Java数组是协变的,也就是说任意的类A和类B,若A是B的父类,则A[] 也是 B[] 的父类。但是假如给数组加入泛型后,将无法满足数组协变的原则,因为在运行时无法知道数组的类型。
Kotlin中的泛型机制和Java中是一样的,上面的特性Kotlin中也存在。
类型擦除:Java受限于先后兼容的困扰,使用了类型擦除来实现了泛型,但是它还是通过其他方式来保证了泛型的相关特性。泛型在编译后是会擦除泛型类型的,而泛型的一些相关特性,比如类型检查是在编译器在编译前就会帮我们进行类型检查,类型自动转换是通过强制类型转化来实现的。
类型擦除的矛盾:在某些场景需要知道运行时泛型参数类型。既然编译后会擦除泛型参数类型,那我们是不是可以主动指定参数类型来达到运行时获取泛型参数类型的效果呢?
fun main() {
val applePlate = Plate(Apple(1.0),Apple::class.java)
applePlate.getType()
}
open class Plate<T>(val t:T, private val clazz:Class<T>){
fun getType(){
println(clazz)
}
}
使用这种方法确实可以到达运行时获取泛型类型参数的效果,但是这样也有限制。
val listType = ArrayList<String>()::class.java //不被允许
还可以使用匿名内部类来是实现。
fun main() {
val list1 = ArrayList<String>()
val list2 = object :ArrayList<String>(){}
println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)
}
>>>
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String>
现在可以设计一个能获取到所有类型信息的泛型类:
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
open class GenericsToken<T> {
var type: Type = Any::class.java
init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).actualTypeArguments[0]
}
}
fun main() {
val gt = object :GenericsToken<Map<String,String>>(){}
println(gt.type)
}
Gson 也是使用的类似方法。
在Kotlin中除了用这种方式来获取泛型参数类型以外,还有另一种方式——内联函数。
Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方。
Java并不支持主动指定一个函数是否是内联函数,所有在Kotlin中声明的普通内联函数可以在Java中调用,因为他不会被当做一个常规函数;而用reified 来实例化的参数类型的内联函数则不能在Java中调用,因为它永远是需要内联的。
打破泛型不变
Kotlin中的List 与 Java的List 有区别
虽然都叫List,也同样支持泛型,但是Kotlin 中的List 定义的泛型参数 前面多了一个 out 关键字。
这个关键字就对这个List 的特性起到了很大作用。普通方式定义的泛型是不变的,简单来说就是不管类型A和类型B 是什么关系,Generic< A> 与 Generic< B>(Generic 代表泛型类) 都没有任何关系。
在Java中String是Object 的 子类型,但是List< String> 并不是 List< Object> 的子类型。在Kotlin中泛型的原理是一样的。但是,Kotlin的List 为什么允许List< String> 赋值给List< Any>呢?
一个支持协变的List
如果在定义泛型类和泛型方法的泛型参数前面加上 out 关键词,说明这个泛型类及泛型方法是协变的。类型A 是 类型 B的子类型,那么Generic< A> 也是 Generic< B> 的子类型。
因为Kotlin的List支持协变,所以他无法添加元素,只能从里面读取内容;
List 一旦创建 就不能再被修改。这便是将泛型声明为协变需要付出的代价。
通常情况下,若一个泛型类Generic< out T> 支持协变,那么它里面的方法的参数类型就不能使用T 类型,因为一个方法的参数不允许传入参数父类型的对象,可能会导致错误。可以添加**@UnsafeVariance** 注解 来解除这个限制。
一个支持逆变的Comparator
逆变:类型A 是 类型B的子类型,但是Generic< B>反过来又是 Generic< A>的子类型。
加上现在需要对一个**MutableList< Double>**进行排序,利用其sortWith 方法,我们需要传入一个比较器:
val doubleComparator = Comparator<Double>{
d1,d2 -> d1.compareTo(d2)
}
fun main() {
val doubleList = mutableListOf(2.0,3.0)
doubleList.sortWith(doubleComparator)
for(i in doubleList){
print("$i ")
}
}
但是如果又需要对MutableList< Int>,**MutableList< Long>**等进行排序,那我们可能又需要定义不同的Comparator 。试想定义一个比较器,给这些列表用,这些数字类的共同父类是Number类。
val numberComparator = Comparator<Number>{
num1,num2-> num1.toDouble().compareTo(num2.toDouble())
}
fun main() {
val doubleList = mutableListOf(2.0,3.0)
doubleList.sortWith(numberComparator)
for(i in doubleList){
print("$i ")
}
println()
val intList = mutableListOf(5,1)
intList.sortWith(numberComparator)
for(i in intList){
print("$i ")
}
}
结果是成功运行了,这说明是可以这样做的。
public fun <T> kotlin.collections.MutableList<T>.sortWith(comparator: kotlin.Comparator<in T> /* = java.util.Comparator<in T> */): kotlin.Unit { /* compiled code */ }
这里又出现了一个in 关键词、和out类似,它也是泛型有个另一个特性——逆变:类型A 是 类型B的子类型,但是Generic< B>反过来又是 Generic< A>的子类型。
用out关键字声明的泛型参数类型将不能作为方法的参数类型,但是可以作为方法的返回值类型。而in刚好相反。
协变和逆变
类型通配符代替泛型参数,Java中的泛型类型通配符为"?",而Koltin中用"*"来表示类型通配符。