Kotlin编译器给开发者省的事儿
Posted 宜信大数据创新中心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin编译器给开发者省的事儿相关的知识,希望对你有一定的参考价值。
如果你是一名安卓开发人员,不了解Kotlin那可要Out啦!虽然Kotlin是比较年轻的JVM语言,但凭借简洁的语言特性,与Java 100%的互操作性和谷歌在安卓应用开发上对它的看重和支持,Kotlin大展拳脚的时代可能真的不远了。
一、Kotlin简介
Kotlin是一种基于JVM的编程语言,由JetBrains在圣彼得堡的团队开发。JetBrais在2011年7月首次发布Kotlin项目并于2012年2月开源该项目。2016年2月Kotlin v1.0版本发布,该版本被认为是第一个稳定版本。在2017年谷歌I/O大会上,谷歌宣布对Kotlin作为android开发语言一级支持。2017年8月7日,在Android开源世界有着杰出贡献的Jake Wharton “J神”宣布加入谷歌Android Framwork团队,负责Kotlin相关工作。谷歌和Jake Wharton在今年的举动在相当一定程度上决定了Kotlin的辉煌时期将要到来,在笔者看来Kotlin已经是开发人员,尤其是安卓开发人员不得不了解的一项技术。
二、Kotlin编译过程
Kotlin是基于JVM的编程语言,Kotlin源码最终会被编译成Java字节码由JVM运行。在看Kotlin编译过程之前,先看一张图来回顾一下Java的编译过程:
Java源码经过词法分析器、语法分析器、语义分析器和字节码生成器处理,生成JVM字节码。
其实Kotlin的编译过程和Java编辑过程十分相似,也是经过词法分析、语法分析、语义分析器和中间代码生成、字节码生成。与Java不同的是,在最后一步生成字节码的时候,Kotlin编译器做了更多的事情。Kotlin简洁的语法使得开发的代码量大幅减少,很多语言特性使得一些在Java中需要显式定义的东西在Kotlin中可以省去(数据类的getter, setter;变量类型……),编译器在编译的最后一步会处理这些语言特性带来的省略,自动生成必要的代码。
三、简洁的语法
Kotlin简洁的语法是Kotlin一大宣传点,下面通过几个例子来看一下Kotlin简洁的语言特性。
1. 数据类
我们经常创建一些只保存数据的类,用来接收从网络请求、数据库或者自己定义的数据。在Java中,这些类的标准方法(setter, getter, toString…)往往是从数据推导出来的,由我们自己定义(或IDE生成)。在Kotlin中,有一种类叫数据类(data class)专门用来保存数据:
data class User(val name: String, val age: Int)
编译器会根据属性自动生成成员函数和构造函数(而不是IDE或开发人员显式生成)包括:
·set()/get()对
·equals()/hashCode()对
·以"User(name=John, age=42)"形式生成toString()
·与每个属性相对应的componentN()函数
·copy()函数
上面数据类的定义在Java中的等价代码为:
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (age != user.age) return false;
return name.equals(user.name);
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + age;
return result;
}
public User copy(String name, int age) {
return new User(name, age);
}
}
可以看到Kotlin这一特性极大的减少了代码量。
2. 扩展函数
Kotlin能够在不更改类本身的代码或不继承类的情况下或不使用任何像装饰者这样的设计模式的情况下扩展类的功能。比如为MutableList<Int> 添加一个 swap 函数:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
如此定义之后,我们就可以像使用成员函数一样使用扩展函数了:
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 变为[3, 2, 1]
Kotlin的这个特性使得我们在Kotlin中可以摆脱对工具类的依赖,而更简洁和优雅的完成同样的工作,比如以下例子:
在Java中完成如下代码:
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list));
在这行代码中,用了4次工具类Collections,使得代码很不简洁。在Java中如果想摆脱这种不简洁,有一种做法就是把java.util.Collections工具类静态引入:
import java.util.Collections
…..
swap(list, binarySearch(list, max(otherList)), max(list));
这样代码就会变的非常简洁,但是这种方法不是很优雅,尤其只需要工具类总少数几个方法的时候。
如果有了扩展函数功能后,代码就会变成:
list.swap(list.binarySearch(otherList.max()), list.max());
这样既比使用工具类看着简洁,同时像使用List成员函数一样使用swap在逻辑上更合理。
扩展函数原理
编译器会将扩展函数编译成共有的静态函数,被扩展类的对象在调用扩展函数时,实际上是对静态方法的调用。我们来看一下上例中扩展函数和调用处的字节码。
扩展函数的字节码:
public final class ExtensionFunctionsKt {
// access flags 0x19
// signature <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;II)V
// declaration: void swap<T>(java.util.List<T>, int, int)
public final static swap(Ljava/util/List;II)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "$receiver"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 2 L1
ALOAD 0
ILOAD 1
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
ASTORE 3
L2
LINENUMBER 3 L2
ALOAD 0
ILOAD 1
ALOAD 0
ILOAD 2
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
POP
L3
LINENUMBER 4 L3
ALOAD 0
ILOAD 2
ALOAD 3
INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
POP
L4
LINENUMBER 5 L4
RETURN
L5
LOCALVARIABLE tmp Ljava/lang/Object; L2 L5 3
LOCALVARIABLE $receiver Ljava/util/List; L0 L5 0
LOCALVARIABLE index1 I L0 L5 1
LOCALVARIABLE index2 I L0 L5 2
MAXSTACK = 4
MAXLOCALS = 4
@Lkotlin/Metadata;(mv={1, 1, 7}, bv={1, 0, 2}, k=2, d1={"\u0000\u0016\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010!\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0002\u001a&\u0010\u0000\u001a\u00020\u0001\"\u0004\u0008\u0000\u0010\u0002*\u0008\u0012\u0004\u0012\u0002H\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u0005\u00a8\u0006\u0007"}, d2={"swap", "", "T", "", "index1", "", "index2", "production sources for module HelloWorld"})
// compiled from: ExtensionFunctions.kt
}
调用处字节码:
…
L2
LINENUMBER 3 L2
ALOAD 1
ICONST_1
ICONST_3
INVOKESTATIC ExtensionFunctionsKt.swap (Ljava/util/List;II)V
…
可以看到,扩展函数swap被编译成了静态方法ExtensionFuctionsKt.swap(类名ExtensionFunctionsKt由编译器根据文件名ExtensionFunctions.kt自动生成),而调用处实际调用的就是ExtensionFuctionsKt.swap这个静态函数。
3. 类型推断
Java对局部变量、成员变量及方法签名的定义要求有显式的类型声明,而在Kotlin中,如果编译器可以推断出类型,则可以省略变量及函数返回值的类型声明。
局部变量
不同于Java,Kotlin中变量的定义语法为变量名、类型,然后是值:
var a: String = "abc"
如果通过赋值符号右边的字面值或者表达式的返回值可以推断出定义变量的类型的话,则变量的类型可以省略,比如上例中编译器可以从字符串字面值”abc”推断出其类型为字符串,则可以简写成:
var a = “abc”
根据表达式的返回值推断类型:
Int a = 1 + 3 // 推断出是整型
var b = if (1 < 2) "less" else "not less" // 推断出是字符串类型
类的属性
以下为类属性定义的语法:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
如果编译器可以从属性的initializer推断出属性的类型,则在定义时可省略类型;另外,从Kotlin 1.1起,如果可以从属性的getter推断出属性的类型,也可以省略属性类型:
var initialized = 1 // 从initializer可推断出属性类型为Int
val isEmpty get() = this.size == 0 // 从getter可推断出属性类型为Boolean
函数返回值
Kotlin函数声明示例:
fun double(x: Int): Int {
return 2*x
}
当函数只返回一个表达式时(如上例),则可以省略花括号,在=后定义代码即可,上例的等价代码为:
fun double(x: Int) Int = 2 * x
如果编译器可以推断出表达式类型时,则可以省略返回值类型,所以上述函数可以继续简写为:
fun double(x : Int) = 2 * x
*注意:Kotlin 不推断具有块代码体的函数的返回类型,因为这样的函数在代码体中可 能有复杂的控制流,并且返回类型对于读者(有时甚至对于编译器)是不明显的。
4. 空安全
在许多语言中(包括Java),最常见的bug之一就是访问空引用的成员,导致系统抛出空异常而奔溃[yb1] 。Java中空异常为NullPointerException或NPE。Kotlin的类型系统旨在消除NPE。
在Kotlin中,类型系统会区分一个引用是否可以接受null。常规定义的变量类型都是不可以接受null引用的,如String,Int,Long……
var a: String = "abc"
a = null // 编译错误
变量a的类型为String,String类型默认不能接收null引用,所以给a赋值null时编译器会报错。
如果要允许为空,我们可以声明一个变量为可空字符串,写作 String? :
var b: String? = "abc"
b = null // 正常
由于a的类型为不可空的String类型,所以可以放心的访问a的成员,而不必担心NPE:
var l = a.length
但是如果访问b的成员,由于b有可能是null,所以是不安全的,编译器会报错:
var l = b.length // 编译器报错,由于b可能为null
这样就在编译阶段保证了代码中访问成员的引用不为空,避免了运行时的NPE。
但是对于可空的引用,我们还是需要访问其成员的,否则定义它就没有意义了。想要访问可空引用的成员,有以下几种方法。
在条件中检查null
可以在if语句中显式的检查引用是不是null,编译器会跟踪所执行检查的信息,允许在if作用范围内调用引用的成员
if (b != null) {
print("String of length ${b.length}") // 合法,因为在if条件中已经判断不为null
b = null
print("String of length ${b.length}") // 错误!b变成null且没有再次检查
} else {
print("Empty string")
}
var l = b.length // 错误!不在if作用范围内也没有其他显式检查,编译器会报错
这种方法只适用于变量不变的情况下,即检查和使用变量之间变量不发生变化的情况。如果在中间变量值发生变化且有可能为null,则需再次检查,否则会报错(如上例第4行)。
安全调用
另外一个选择是安全调用操作符——?.
b?.length
如果 b 非空,就返回 b.length ,否则返回 null ,也就是说这个表达式的类型是 Int? 。
安全调用在链式调用很有用。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部 门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的 话)的名字,我们写作:
bob?.department?.head?.name
其中任何一个属性为null的话,这个链式调用都会返回null,否则返回所需的结果。等价于Java代码:
if (bob == null) {
return null;
}
Department department = bob.getDepartment();
if (department == null) {
return null;
}
Person head = department.getHead();
if (head == null) {
return null;
}
return head.getName();
Elvis操作符
还有一种方法允许开发者在值为空时自定义一个值,称为Elvis表达式——?:
val l = b?.length ?: -1
这行代码的逻辑是:我有一个可空字符串b,如果b不为空则返回b.length,否则返回-1。这个逻辑和下面的if表达式是等价的:
val l: Int = if (b != null) b.length else -1
显式NPE
如果开发者需要获得NPE,则需要用!!操作符显示要求,否则在运行时不会有NPE抛出,因为空检查都是在编译阶段完成的
val l = b!!.length
在b不为空时则返回b.length,在b为空时则会抛出NPE。
Kotlin虽然已经问世很多年,但依然是比较年轻的JVM语言,现在还不算是主流开发语言,使用Kotlin开发项目的公司越来越多,但是大部分公司还是没有大量的使用这门语言。但是凭借着简洁的语言特性,与Java 100%的互操作性和谷歌在安卓应用开发上对Kotlin的看重和支持,相信Kotlin大展拳脚的时代即将到来。
以上是关于Kotlin编译器给开发者省的事儿的主要内容,如果未能解决你的问题,请参考以下文章