null引用
对于空指针异常,当前java已经有了如下处理:
1函数内对于无效值,更倾向于抛异常处理;
2采用@NotNull/@Nullable标注,明确参数是否可空,避免非法null值进一步传递;
3使用专门的optional对象对可能为null的变量进行装箱。这类对象必须拆箱后才能参与运算。
*java8新增了Optional
Kotlin的可空类型:?.
*由于null只能被存储在java的引用类型的变量中,所以在kotlin中基本数据的可空版本都会使用该类型的包装形式。同样,如果你用基本数据类型作为泛型类的类型参数,Kotlin同样会使用该类型的包装形式。
Elvis操作符:?:
非空断言!!.
上面说到过解决npe一般有三种方式:
1用try catch捕获异常
2用Optional
3用@NotNull/@Nullable注解来标注
kotlin是:在方法参数上标注了@Nullable,在实现上,依旧采用了if..else来对可空情况进行判断。兼容了java老版本,实现与java100%互转,性能上达到最佳。
如需抛出异常则为:
val s :Student?= null
println(s?.glas?:throw NullPointerException("some thing null"))
类型检查 is
if(obj is String){
pringln(obj.lenth)
}
这里的obj类型为Any,是kotlin的智能转换(Smart Casts)帮我们省略了一些工作。
Kotlin与其他面向对象语言一样,无法直接将父类型转为子类型,当需要类型转换时,利用as操作符来实现。
*Kotlin还提供了as?操作符
5.3比java更面向对象的设计
与Object作为java类层级结构的顶层类似,Any类型时Kotlin中所有非空类型(如String,Int)的超类。
与Object作为Java类层级结构的顶层类似,Any类型是 Kotlin 中所有非空类型的超类。
对于Kotlin 来说,如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如果定义了父类型,那么该父类型是该类的直接父类型,但是新类型的最终根类型为Any。
Kotlin的Type Checker强制检查了父子关系。例如,你可以将子类型值存储到父类型变量中。但是不能将父类型值存储到子类型中。
Kotlin把Java方法参数和返回类型中用到的Object类型看作Any(更准确的说是“平台类型”)。当在Kotlin函数中使用Any时,它会编译成Java字节码中的Object。
Any? 所有类型的根类型
Any是所有非空类型的根类型,Any?才是所有类型(可空和非空类型)的根类型。
Any? 与Any??
如果Any?是Any的父类型,那么Any?? 是否又是Any?的父类型,如果成立,是否意味着就没有所谓的所有类型的根类型了?
在Kotlin 中,可空类型可以看作是所谓的Union Type,近似于数学中的并集。如果用类型的并集来表示Any?可写为Any U Null。
相应的Any??就可以表示为Any U Null U Null,等价于Any U Null,即Any??等价于Any?。因此,说Any?是所有类型的根类型是没有问题的。
Nothing与Nothing?
Nothing是没有实例的类型。Nothing类型的表达式不会产生任何值。需要注意的是:任何返回值为Nothing的表达式之后的语句都是无法执行的。Kotlin中retrun、thorw等(流程控制中与跳转相关的表达式)返回值都为Nothing。
Nothing对应的Nothing?,可以从字面上解释为:可空的空。与Any、Any?类似,所以Nothing?是Nothing的父类型,所以Nothing处于Kotlin类型层级结构的最低层。它只能包含一个值:null,本质上与null没有区别。所以可以使用null作为可空类型的值。
自动装箱与拆箱
Koltin中没有int、double、float、long等基本数据类型,取而代之的是引用类型包装类Int、Float、Double、Long。
除了代表数值的类型,还有布尔(Boolean)、字符(Char)、字符串(String)及数组(Arry)。
但是只能说Kotlin比Java更接近纯面向对象的设计。
因为:
Kotlin代码
val x1:Int = 10
val x2:Int? =12
转Java代码
public final class TestIntDemoKt {
private static final int x1 = 10;
@Nullable
private static final Integer x2 = 12;
public static final int getX1() {
return x1;
}
@Nullable
public static final Integer getX2() {
return x2;
}
}
对应的字节码
BIPUSH 10
PUTSTATIC com/example/kotlindemo/anyclassdemo/TestIntDemoKt.x1 : I
BIPUSH 12
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
观察上面代码可以发现,Koltin中的Int在JVM中实际以int存储(对应字节码类型是I)
所以我们可以简单的认为:
Kotlin中Int类型等同于int。
Kotlin中Int?等同于Integer。
“新“的数组类型
Kotlin中数组的创建:
val funList0 = arrayOf()//长度为0的数组
val funList= arrayOf(1,2,3) //初始化长度为3的数组
Kotlin中Array并不是一个原生的数据结构,而是一种Array类,甚至可以将Kotlin中Array视作集合类的一部分。
由于Smart Casts,编译器能够隐式推断出 funList元素类型。
在Kotlin中,还有一些实用的类,IntArray、CharArray、ShortArray等,分别对应Java中的int[]、char[]、short[]等。
val intArray = intArrayOf(1,2)
IntArray等并不是Array的子类,所以两者创建的相同值的对象,并不是相同对象。
*由于Kotlin中对原始类有特殊的优化(主要体现在避免自动装箱带来的开销),所以建议优先使用原始类型数组。
5.4泛型:让类型更加安全
泛型是一种编译时的安全检测机制,它允许在定义类、接口、方法时使用类型参数,声明的类型参数在使用时用具体的类型来替换。
众所周知,Java 1.5引入泛型。那么我们来思考一个问题,为什么Java一开始没有引入泛型,而1.5版本却引入泛型?先来看一个场景:
List stringList = new ArrayList();
stringList.add(new Double(2.0));
String str = (String)stringList.get(0);
执行结果:
>>> java.lang.ClassCastException:
java.lang.Double cannot be cast to java.lang.String
at javat.Rectangle.main(Rectangle.java:29)
因为ArrayList底层是用一个Object类型的数组实现的,这种实现虽然能让ArrayList变得更通用,但也会带来弊端。比如上面例子中,我们不小心向原本应作为String类型的List中添加了一个Double类型的对象,理想的情况下编译器应该能够提示错误,但事实上这段代码能编译通过,在运行时却会报错。这是一个非常糟糕的体验,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。这便是泛型诞生的一个重要的原因。有了泛型后,我们可以这么做:
List stringList = new ArrayList();
stringList.add(new Double(2.0)); //编译时报错,add(java.lang.String)无法适配add
(java.lang.Double)
利用泛型代码在编译时期就能发现错误,防止在真正运行的时候出现ClassCastException。当然,泛型除了能帮助我们在编译时期进行类型检查外,还有很多其他好处,比如自动类型转换。
继续来看第一段代码,在获取List中的值的时候,我们进行了以下操作:
String str = (String)stringList.get(0);
是不是感觉异常的烦琐,明明知道里面存的是String类型的值,取值的时候还要进行类型强制转换。但有了泛型之后,就可以利用下面这种方式实现:
List stringList = new ArrayList();
stringList.add("test");
String str = stringList.get(0);
有了泛型之后,不仅在编译的时候能进行类型检查,在运行时还会自动进行类型转换。而且通过引入泛型,增强上述功能的同时并没有增加代码的冗余性。比如我们无须为声明一个类型安全的List而去创建StringList、DoubleList等类,只需在声明List的同时指定参数类型即可。
总的来说,泛型有以下几点优势:
类型检查,能在编译时就帮你检查出错误;
更加语义化,比如我们声明一个List
自动类型转换,获取数据时不需要进行类型强制转换;
能写出更加通用化的代码。
如何在Kotlin中使用泛型
假设现在我们有一个需求,定义一个find方法,传入一个对象,若列表中存在该对象,则返回该对象,不存在则返回空。由于原有的集合类不存在这个方法,所以可以定义一个新的集合类,同样也要声明泛型。我们可以这么做:
class SmartList : ArrayList () {
fun find(t: T): T? {
val index = super.indexOf(t)
return if (index >= 0) super.get(index) else null
}
}
fun main(args: Array) {
val smartList = SmartList()
smartList.add("one")
println(smartList.find("one"))//输出one
println(smartList.find("two").isNullOrEmpty())//输出true
}
发现,Kotlin定义泛型类的方式与我们在Java中所看到的类似。另外泛型类同样还可以继承另一个类,这样我们就可以使用ArrayList中的属性和方法了。
当然,除了定义一个新的泛型集合类外,我们还可以利用扩展函数来实现这种需求。由于扩展函数支持泛型的情况,所以我们可以这么做:
fun ArrayList.find(t: T): T? {
val index = this.indexOf(t)
return if (index >= 0) this.get(index) else null
}
fun main(args: Array) {
val arrayList = ArrayList()
arrayList.add("one")
println(arrayList.find("one"))//输出one
println(arrayList.find("two").isNullOrEmpty())//输出true
}
利用扩展函数这种方式也非常简洁,所以,当你只是需要对一个集合扩展功能的时候,使用扩展函数非常合适。
使用泛型时是否需要主动指定类型?
在Kotlin中,以下的方式不被允许:
val arrayList = ArrayList()
而在Java中却可以这么做,这主要是因为泛型是Java 1.5版本才引入的,而集合类在Java早期版本中就已经有了。各种系统中已经存在大量的类似代码:
List list = new ArrayList();
所以,为了保证兼容老版本的代码,Java允许声明没有具体类型参数的泛型类。而Kotlin是基于Java 6版本的,一开始就有泛型,不存在需要兼容老版本代码的问题。所以,当你声明一个空列表时,Kotlin需要你显式地声明具体的类型参数。当然,因为Kotlin具有类型推导的能力,所以以下这种方式也是可行的:
val arrayList = arrayListOf("one", "two")
总的来说,使用泛型可以让我们的代码变得更加通用化,更加灵活。但有时过于通用灵活并不是一个好的选择,比如现在我们创建一个类型,只允许添加指定类型的对象。接下来我们就来看看如何在Kotlin中约束类型参数。
类型约束:设定类型上界
假设现在有一个盘子,他可以放任何东西,在Kotlin中我们可以这么做
class Plate(val t :T)
突然你想把自己的盘子归归类,一些只放水果,一些放菜,我们定义Fruit类并声明Apple和Banana类来继承他
open class Fruit(val weight:Double)
class Apple(weight:Double):Fruit(weight)
class Banana(weight:Double):Fruit(weight)
在定义一个水果盘子
class FruitPlate(val t :T)
这里的T只能是Fruit类及其子类型。
val applePlate = FruitPlate(Apple(100.0))
val applePlate = FruitPlate(Apple(100.0))//简化写法
java中通过extends关键字,而kotlin使用:
class FruitPlate{}
通过where关键字,实现对泛型参数类型添加多个约束条件。
假设一把刀只能切在地上的水果:
fun main() {
cut(Watermelon(3.0))
cut(Apple(2.0))//Type mismatch: inferred type is Apple but Ground was expected
}
interface Ground{}
open class Fruit(val weight:Double)
class Apple(weight:Double):Fruit(weight)
class Watermelon(weight:Double):Fruit(weight),Ground
fun cut(t:T) where T:Fruit,T:Ground{
print("cut me")
}
5.5泛型的背后:类型擦除
Java为什么无法声明一个泛型数组(加入泛型后,运行时无法知道数组的类型,无法满足数组协变的原则)
我们先来看一个简单的例子,Apple是Fruit的子类,思考下Apple[]和Fruit[],以及List
Apple[] appleArray = new Apple[10];
Fruit[] fruitArray = appleArray; //允许
fruitArray[0] = new Banana(0.5); //编译通过,运行报ArrayStoreException
List appleList = new ArrayList();
List fruitList = appleList; //不允许
我们发现一个奇怪的现象,Apple[]类型的值可以赋值给Fruit[]类型的值,而且还可以将一个Banana对象添加到fruitArray,编译器能通过。作为对比,List
其实这里涉及一个关键点,数组是协变的,而List是不变的。简单来说,就是Object[]是所有对象数组的父类,而List
在解释为什么在Java中无法声明泛型数组之前,我们先来看一下Java泛型的实现方式。Java中的泛型是类型擦除的,可以看作伪泛型,简单来说,就是你无法在程序运行时获取到一个对象的具体类型。我们可以用以下代码来对比一下List
System.out.println(appleArray.getClass());
System.out.println(appleList.getClass());
// 运行结果
class [Ljavat.Apple;
class java.util.ArrayList
从上面的代码我们可以知道,数组在运行时是可以获取自身的类型,而List
Kotlin中的泛型机制与Java中是一样的,所以上面的特性在Kotlin中同样存在。比如通过下面的方式同样无法获取列表的类型:
val appleList = ArrayList()
println(appleList.javaClass)
但不同的是,Kotlin中的数组是支持泛型的,当然也不再协变,也就是说你不能将任意一个对象数组赋值给Array
val appleArray = arrayOfNulls(3)
val anyArray: Array = appleArray //不允许
我们已经知道了在Kotlin和Java中泛型是通过类型擦除来实现的,那么这又是为什么呢?
向后兼容的罪
简单来说,就是老版本的Java文件编译后可以运行在新版本的JVM上。我们知道,Java一开始是没有泛型的,那么在Java 1.5之前,在程序中会出现大量的以下代码:
ArrayList list = new ArrayList(); //没有泛型
一般在没有泛型的语言上支持泛型,一般有两种方式,以集合为例:
1全新设计一个集合框架(全新实现现有的集合类或者创造新的集合类),不保证兼容老的代码,优点是不需要考虑兼容老的代码,写出更符合新标准的代码;缺点是需要适应新的语法,更严重的是可能无法改造老的业务代码;
2在老的集合框架上改造,添加一些特性,兼容老代码的前提下,支持泛型。
很明显,Java选择了后种方式实现泛型,这也是有历史原因的,主要有以下两点原因:
1)在Java1.5之前已经有大量的非泛型代码存在了,若不兼容它们,则会让使用者抗拒升级,因为他要付出大量的时间去改造老代码;
2)Java曾经有过重新设计一个集合框架的教训,比如Java 1.1到Java1.2过程中的Vector到ArrayList, HashTable到HashMap,引起了大量使用者的不满。
所以,Java为了填补自己埋下的坑,只能用一种比较别扭的方式实现泛型,那便是类型擦除。
那么,为什么使用类型擦除实现泛型可以解决我们上面说的新老代码兼容的问题呢?我们先来看一下下面两行代码编译后的内容:
ArrayList list = new ArrayList(); //(1)
ArrayList stringList = new ArrayList(); //(2)
对应字节码:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: new #2 // class java/util/ArrayList
11: dup
12: invokespecial #3 // Method java/util/ArrayList."":()V
15: astore_2
我们发现方式1和方式2声明的ArrayList再编译后的字节码是完全一样的,这也说明了低版本编译的class文件在高版本的JVM上运行不会出现问题。既然泛型在编译后是会擦除泛型类型的,那么我们又为什么可以使用泛型的相关特性,比如类型检查、类型自动转换呢?
类型检查是编译器在编译前就会帮我们进行类型检查,所以类型擦除不会影响它。那么类型自动转换又是怎么实现的呢?我们来看一个例子:
ArrayList stringList = new ArrayList();
String s = stringList.get(0);
这段代码大家都应该很熟悉,get方法返回的值的类型就是List泛型参数的类型。来看一下ArrayList的get方法的源码:
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index]; //强制类型转换
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
我们发现,背后也是通过强制类型转化来实现的。这点从编译后的字节码也可以得到验证:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: iconst_0
10: invokevirtual #4 // Method java/util/ArrayList.get:(I)Ljava/lang/Object; 获取的是Object
13: checkcast #5 // class java/lang/String强制类型转换
16: astore_2
17: return
所以可以得出结论,虽然Java受限于向后兼容的困扰,使用了类型擦除来实现了泛型,但它还是通过其他方式来保证了泛型的相关特性。
类型擦除的矛盾
通常情况下使用泛型我们并不在意它的类型是否是类型擦除,但是在有些场景,我们却需要知道运行时泛型参数的类型,比如序列化/反序列化的时候。这时候我们应该怎么办?通过前面的学习相信你对Java和Kotlin的泛型实现原理已经有了一定的了解,既然编译后会擦除泛型参数类型,那么我们是不是可以主动指定参数类型来达到运行时获取泛型参数类型的效果呢?我们试着对上面的例子的Plate进行一下改造:
open class Plate(val t : T, val clazz: Class) {
fun getType() {
println(clazz)
}
}
val applePlate = Plate(Apple(1.0), Apple::class.java)
applePlate.getType()
//结果
class Apple
使用这种方式确实可以达到运行时获取泛型类型参数的效果。但是这种方式也有限制,比如我们就无法获取一个泛型的类型,比如
val listType = ArrayList::class.java //不被允许
val mapType = Map::class.java //不被允许
那么,还有没有另外的方式能获取各种类型的信息呢?有,那就是利用匿名内部类。我们来看下面的一个例子:
val list1 = ArrayList()
val list2 = object : ArrayList(){} //匿名内部类
println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)
//结果:
java.util.AbstractList
java.util.ArrayList
不可思议,第2种方式竟然能在运行时知道这个list是一个什么样的类型。心细的读者应该发现,list2声明的其实是一个匿名内部类。关于如何在Kotlin中用object来声明一个匿名内部类的相关知识可以回顾一下前面相应内容。那么,为什么使用匿名内部类的这种方式能够在运行时获取泛型参数的类型呢?其实泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应class的常量池中的。
Java将泛型信息存储在哪里?
可以参考以下网页:Where are generic types stored in java class files?
所以,既然还存储着相应的类型信息,那么我们就能通过相应的方式来获取这个类型信息。使用匿名内部类我们就可以实现这种需求。我们着手来设计一个能获取所有类型信息的泛型类:
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
open class GenericsToken { //
var type: Type = Any::class.java
init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).getActualTypeArguments()[0]
}
}
fun main(args: Array) {
val gt = object : GenericsToken
匿名内部类在初始化的时候就会绑定父类或父接口的相应信息,这样就能通过获取父类或父接口的泛型类型信息来实现我们的需求。你可以利用这样一个类来获取任何泛型的类型,我们常用的Gson也是使用了相同的设计。
Gson的TypeToken实现参考以下网址:https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/reflect/TypeToken.java
比如,我们在Kotlin中可以这样使用Gson来进行泛型类的反序列化:
val json = ...
val rType = object : TypeToken>() {}.type
val stringList = Gson().fromJson>(json, rType)
其实,在Kotlin中除了用这种方式来获取泛型参数类型以外,还有另外一种方式,那就是内联函数。
使用内联函数获取泛型
Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方,也就是说,参数类型也会被插入字节码中,我们就可以获取参数的类型了。有关内联函数的内容可以看一下前面的相应章节。下面我们就用内联函数来实现一个可以获取泛型参数的方法:
inline fun getType() {
return T::class.java
}
使用内联函数获取泛型的参数类型非常简单,只需加上reified关键词即可。这里的意思相当于,在编译的会将具体的类型插入相应的字节码中,那么我们就能在运行时获取到对应参数的类型了。所以,我们可以在Kotlin中改进Gson的使用方式:
inline fun Gson.fromJson(json: String): T { //对Gson进行扩展
return Gson().fromJson(json, T::class.java)
}
//使用
val json = ...
val stringList = Gson().fromJson>(json)
这里利用了Kotlin的扩展特性对Gson进行了功能扩展,在不改变原有类结构的情况下新增方法,很多场景用Kotlin来实现便会变得更加优雅。有关扩展的相关内容会在第7章讲解。
另外需要注意的一点是,Java并不支持主动指定一个函数是否是内联函数,所以在Kotlin中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数;而用reified来实例化的参数类型的内联函数则不能在Java中调用,因为它永远是需要内联的。
为什么List不能赋值给List
假如可以:
List stringList = new ArrayList();
List
如果这样,她将会和数组支持泛型一样,不在保证类型安全,所以java不支持这种行为。
但是Kotlin这里我们发现一个奇怪的现象:
val stringList :List = ArrayList()
val anyList:List = stringList//编译成功
在Kotlin中竟然能将List
java中
public interface List extends Collection {
...
}
kotlin中
public interface List extends Collection {
...
}
虽然都叫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
kotlin中,如果在定义泛型类和泛型方法的泛型参数前面加上 out 关键词,说明这个泛型类及泛型方法是协变的。类型A 是 类型 B的子类型,那么Generic< A> 也是 Generic< B> 的子类型。
因为Kotlin的List支持协变,所以他无法添加元素,只能从里面读取内容;
val stringList:List = ArrayList()
stringList.add("kotlin")//编译报错,不允许!!
List 一旦创建 就不能再被修改。这便是将泛型声明为协变需要付出的代价。
结论:支持斜边的List只可以读取而不可以添加,否则不是类型安全的,违背泛型的初衷。
通常情况下,若一个泛型类Generic< out T> 支持协变,那么它里面的方法的参数类型就不能使用T 类型,因为一个方法的参数不允许传入参数父类型的对象,可能会导致错误。可以添加@UnsafeVariance 注解 来解除这个限制。
一个支持逆变的Comparator
逆变:类型A 是 类型B的子类型,但是Generic< B>反过来又是 Generic< A>的子类型。
加上现在需要对一个MutableList< Double>进行排序,利用其sortWith 方法,我们需要传入一个比较器:
val doubleComparator = Comparator{
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{
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 kotlin.collections.MutableList.sortWith(comparator: kotlin.Comparator /* = java.util.Comparator */): kotlin.Unit { /* compiled code */ }
这里又出现了一个in 关键词、和out类似,它也是泛型有个另一个特性——逆变:类型A 是 类型B的子类型,但是Generic< B>反过来又是 Generic< A>的子类型。
用out关键字声明的泛型参数类型将不能作为方法的参数类型,但是可以作为方法的返回值类型。而in刚好相反。
协变和逆变
类型通配符代替泛型参数,Java中的泛型类型通配符为"?",而Koltin中用"*"来表示类型通配符。