Kotlin使用笔记
接触Kotlin也有一段时间了。最近在使用OKHttp的时候,发现4.0以后的版本使用Kotlin重写了。Google说Kotlin first , 不是 Kotlin must,但是从这次版本重写可以知道google对Kotlin重视程度。
在空余时间,我尝试把之前的Java工具类使用Kotlin重写了,中途遇到了一些问题,就随手记录下来,方便以后查询也让新手走少点弯路。熟悉了kotlin之后,再写Java会发现已经不能回头了,就像使用了Java 8的lambda之后,让你再回到Java 7的那种感觉。
大家经常拿Java,Kotlin,Scala比较。我比较认可的观点是 :
Java 本身的优缺点就不用说了,是一门严格的语言,但是也有很多约束;
Scala 是一门学院派的语言,追求更好的灵活性, 热衷于编程语言本身的研究和探索 ;
Kotlin 的自我定位非常清晰—它的目标就是在应用领域成为一门实用且高效的编程语言。
如果说Scala的设计理念是more than Java(不仅仅是Java),那么Kotlin才是一门真正意义上的better Java(更好的Java)。
从个人工具库看语言发展。 语言的发展,应该要尽力提高开发效率,使代码越少,工具库越少,设计模式尽量在语言层面支持。一开始写汇编和C语言,用汇编写个循环累加都需要一大堆代码,到了C语言,for循环解决了很多问题,还有很多其他的特性大大提高了编程效率,但是类似集合等操作要自己写,通常我们会自己写一些集合类和工具类的,方便在不同场景的使用,于是慢慢就有了收集和积累工具类的习惯。后面从C语言转到C++,STL提供了很多集合工具,C语言写的集合类也几乎无用武之地。C++没用多久,就转用了Java和matlab 。matlab提供了强大的科学计算工具,绘图工具和分析工具,开箱即用,就算是算法新手和编程新手都可以快速入门。而Java完善了C++很多容易出错的操作,提供更多更方便的库支持,并从语言层面支持了适配器,观察者,模板等设计模式。从Java7到Java8,引入了lambda和流,工具库的代码又变得更加的简洁易懂。现在使用kotlin重写工具库,你会发现原有Java的代码还可以继续优化,kotlin提供了更多编译器级别错误的检验,代码也变得更加的简洁了,简化了集合操作,带接收者lambda,高效语法糖,实现接口引入 by,从语言层面实现 装饰器模式,让我们减少枯燥的模板代码折磨。甚至有些工具类都已经没有重写的必要了,本身库就支持了。
后面可能还会使用其他语言重写现有的工具类,下一面语言是什么?我也不清楚,但是我觉得这个语言应该让现有的代码更加的简洁,更容易理解,让我们觉得上一门语言是如此的繁琐。
kotlin 现在可以编译成 Native 和 JS了,还要什么自行车!!!
more:最近发现一个购书网站,孔夫子二手书网,价格几乎5折,《Kotlin in action》就是在上面买的,这本书一个星期就可以读完。
省略一万字废话了。。。
Kotlin类型系统
基本类型
Kotlin的类型和Java的类型是一致,只是在对应的基本类型把首字母大写了。
- 整数类型----Byte 、Short 、Int, Long
- 浮点数类型----Float 、Double
- 字符类型---- Char
- 布尔类型---- Boolean
像Int 这样的Kotlin 类型在底层可以轻易地编译成对应的Java 基本数据类型,因为两种类型都不能存储null 引用。如果需要存储可空类型,需要在类型后面添加?,如Int?
。关于可空行,下面有讨论,有需要可以翻阅。
数字转换
Kotlin 和Java 之间一条重要的区别就是处理数字转换的方式。Kotlin 不会自动地把数字从一种类型转换成另外一种,即便是转换成范围更大的类型。
每一种基本数据类型( Boolean 除外)都定义有转换函数: toByte 、toShort 、toChar等。这些函数支持双向转换。
Kotlin 除了支持简单的十进制数字之外,还支持在代码中书写数字字面值的方式。注意, Kotlin 1.1 才开始支持数字字面值中的下画线。对字符字面值来说,可以使用和Java 几乎一样的语法。
注意, Kotlin 算术运算符关于数值范围溢出的行为和Java 完全一致: Kotlin 并没有引入由溢出检查带来的额外开销。
kotlin 中 var, val 和 const val 的区别
-
var 是可读写的变量,默认是 private 的, 提供get/set 方法
-
val 是只读变量,默认是private的,提供get 方法
-
const val ,是 public 常量,const只能修饰val并且只能在top-level中修饰, 对于java就是 public final static 的类型。
对于kotlin,const val和val 使用几乎没什么区别,但是在java中调用,val需要get方法访问,如
ConstDemoKt.getNameNormal()
, 而const val则因为是常量,直接引用ConstDemoKt.constNameNormal
“Any"和 "Any?" :根类型
和Object 作为Java 类层级结构的根差不多, Any 类型是Kotlin 所有非空类型的超类型(非空类型的根〉。但是在Java 中, Object 只是所有引用类型的超类型(引用类型的根) ,而基本数据类型并不是类层级结构的一部分。这意味着当你需要Object 的时候,不得不使用像java.lang.Integer 这样的包装类型来表示基本数据类型的值。而在Kotlin 中, Any 是所有类型的超类型(所有类型的根),包括像Int 这样的基本数据类型。
注意Any 是非空类型,所以Any 类型的变量不可以持有null 值。在Kotlin中如果你需要可以持有任何可能值的变量,包括null 在内,必须使用Any?类型。
在底层, Any 类型对应Object 。Kotlin 把Java 方法参数和返回类型中用到的Object 类型看作Any。当Kotlin 函数使用Any 时,它会被编译成Java字节码中的Object 。所有Kotlin类都包含下面三个方法: toString、
equals 和hashCode 。这些方法都继承自Any 。 Any 并不能使用其他Object 的方法(比如wait 和notify ),但是可以通过于动把值转换成Object来调用这些方法。
Unit 类型: Kotlin 的“void ”
Kotlin 中的Unit 类型完成了Java 中的void 一样的功能。当函数没什么有意思的结果要返回时,它可以用作函数的返回类型,Unit可以省略:
fun f () : Unit { . . . }
那么Kotlin 的Unit 和Java 的void 到底有什么不一样呢? Unit 是一个完备的类型,可以作为类型参数,而void 却不行。只存在一个值是Unit 类型,这个值也叫作Unit ,并且(在函数中)会被隐式地返回。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit 类型的值。
Unit 这个名字习惯上被用来表示“只有一个实例”,这正是Kotlin 的Unit 和Java 的void 的区别。本可以沿用Void 这个名字,但是Kotlin 还有一个叫作Nothing 的类型,它有着完全不同的功能。如果使用了就会让人更加迷惑
Nothing 类型:“这个函数永不返回”
对某些Kotlin 函数来说,"返回类型”的概念没有任何意义,因为它们从来不会成功地结束。。例如, 许多测试库都有一个叫作fail 的函数,它通过抛出带有特定消息的异常来让当前测试失败。一个包含无限循环的函数也永远不会成功地结束。
fun fail(rnessage: String): Nothing {
throw IllegalStateException (rnessage)
}
注意,返回Nothing 的函数可以放在E lv i s 运算符的右边来做先决条件检查
val address =company.address ?: fail("No address")
println(address.c 工ty)
上面这个例子展示了在类型系统中拥有Nothing 为什么极其有用。编译器知道这种返回类型的函数从不正常终止, 然后在分析调用这个函数的代码时利用这个信息。在上面这个例子中,编译器会把address 的类型推断成非空,因为它为null 时的分支处理会始终抛出异常。
kotlin 中的静态变量和静态方法
顶层函数
原来在Java中,类处于顶层,类包含属性和方法,在Kotlin中,函数站在了类的位置,我们可以直接把函数放在代码文件的顶层,让它不从属于任何类。
Kotlin 顶层函数相当于 Java 中的静态函数,往往我们在 Java 中会用到类似 Utils 的类来放置不属于任何类的公共静态函数。
就像下面这样,我们在一个Str.kt文件中写入如下的Kotlin代码。
package util
fun joinToStr(collection: Collection<String>): String{
//....
return "";
}
会编译成下面的Java代码
package util;
public class StrKt {
public static String joinToStr(...) { ... }}
kotlin 调用
import util.joinToStr
fun main(args: Array<String>){
joinToStr(collection = listOf("123", "456"))
}
Java调用
import java.util.ArrayList;
import util.StrKt;
public class Main {
public static void main(String[] args) {
StrKt.joinToStr(new ArrayList<>());
}
}
可能有时候你觉得Kotlin为你自动生成的这个类名不好,那你可以通过@file:JvmName注解来自定义类名,就像下面这样。 在package 上面加上注解。 例如 @file:JvmName("StrUtil")
静态变量
之前我们会把类变量放到 类的大括号里面,但是在kotlin中, 没有static关键字。 并不是直接放到class的括号中,需要放到 companion object
中,如定义 HTTP_REQUEST_TIMEOUT 这个类变量时,可以使用下面的写法
class OKHttpUtil {
companion object{
const val HTTP_REQUEST_TIMEOUT = 10L
}
}
静态代码块
有时我们需要在初始化静态代码块中处理一下初始化的事情,那么需要 companion object
中 操作,java使用 static {} 这种形式定义,kotlin使用 init{}
定义,换了个关键字。注意,如果init 不是放到 companion object
中,就变成了构造函数方法了 。
companion object{
init {
val i:Int = 123
val j = i + 1
}
}
静态方法
直接把方法定义在companion object
中 就行,如果需要在java静态调用,还要增加@JvmStatic 注解
如
companion object{
@JvmStatic
@Throws(IOException::class)
fun getConnection(url: String?): Int? {
return 1
}
}
构造函数
1、在定义class中定义
class Person(username: String, age: Int){
private val username: String = username
private var age: Int = age
}
2、在init中初始化赋值,有时我们在构造方法中写写逻辑,可以在init中处理,注意这个init是放到 class体中的
class Person (username: String, age: Int){
private val username: String
private var age: Int
init{
this.username = username
this.age = age
}
}
3、定义次构造函数,有时我们需要定义多个构造函数
class MyTest {
//1.次构造函数声明前缀有constructor
constructor(name: String) {
.....
}
}
4、 如果类有一个主构造函数,每个次构造函数可以使用this关键字,直接委托或者通过别的次构造函数间接委托给主构造函数
class MyTest(var name: String) {
var mName : String = name
var mAge: Int = 0
init {
Log.e("MyTest--","init1")
this.mName = name
this.mAge = mAge
Log.e("MyTest--","init mName = $mName mAge = ${mAge} name = ${name}")
}
//2.使用this关键字,直接委托给主构造函数
constructor(name: String="1", age: Int): this(name){
Log.e("MyTest--","construct 1")
this.mName = name
this.mAge = age
Log.e("MyTest--"," mName = $mName mAge = $mAge name = $name")
}
}
5、 如果不希望类有一个公有构造函数,需要声明一个带有非默认可见性的主构造函数
class MyPrivateTest private constructor () {
}
6、构造函数之间的调用
open class MyTestOne(var name: String){
init {
Log.e("MyTestOne--", "MyTestOne init")
}
constructor(age: Int):this("test"){
Log.e("MyTestOne--", "MyTestOne construct 1")
}
constructor(age: Int, name:String):this(age){
Log.e("MyTestOne--", "MyTestOne construct 2 $name")
}
// val test1: MyTestOne = MyTestOne("name") //ok
// val test2: MyTestOne = MyTestOne(1) //ok
// val test3: MyTestOne = MyTestOne(1,"wf") //ok
}
嵌套类
open class MyTestOne(var name: String){
init {
Log.e("MyTestOne--", "MyTestOne init")
}
constructor(age: Int):this("test"){
Log.e("MyTestOne--", "MyTestOne construct 1")
}
constructor(age: Int, name:String):this(age){
Log.e("MyTestOne--", "MyTestOne construct 2 $name")
}
// val test1: MyTestOne = MyTestOne("name") //ok
// val test2: MyTestOne = MyTestOne(1) //ok
// val test3: MyTestOne = MyTestOne(1,"wf") //ok
}
扩展函数和属性
扩展函数
kotlin使用了java的标准库,但是kotlin允许对一些类的方法进行扩展,如果扩展之后无法转换成java api,那就有兼容性的问题。kotlin引入了 扩展函数
实现。
Kotlin的扩展函数可以让你作为一个类成员进行调用的函数,但是是定义在这个类的外部。这样可以很方便的扩展一个已经存在的类,为它添加额外的方法。
扩展函数 很简单,就是一个类的成员函数,不过定义在类的外面。
例如定义一个获取字符串最后一个字符的函数
fun String.lastChar(): Char = get(length -1)
调用方式,import 对应的包,然后像对象本身方法调用。
val lastChar = "Jam".lastChar()
看定义, String 是扩展的类型, lastChar是扩展函数名字, Char 是返回值。后面是方法的定义,可以看到kotlin的方法很简洁, 下面给出另一种类似Java的定义,看起来就比较麻烦。转换成JAVA的时候,其实就是调用了下面的代码。String类没有更改,实质是调用了一个静态函数,高效的语法糖。
fun String.lastChar(): Char { return this.get(this.length - 1)}
扩展函数是不可以重写的(扩展函数其实就是静态方法),因为扩展函数定义在类的外面,如果子类和父类都有同名的扩展函数,那么调用时,会调用静态类型的函数,也就是如果子类对象赋值给父类变量,会调用父类的扩展函数,而不是像多态那样调用子类的方法。
如果成员函数和扩展函数有相同的签名,优先调用成员函数
我在重写OKHttp工具的时候,发现了这个功能,看源码 RequestBody 有很多扩展函数,然后看了《kotlin in action》,发现这个好功能。kotlin强大功能之一。
扩展属性
这里的扩展属性,并不是给类增加属性,因为不可能为现有的java对象增加额外的字段,只是个短语法。需要定义getter函数
val String.lastChar1: Char
get() = get(length -1)
kotlin调用
val lastChar1 = "Jam".lastChar1
如果是Java调用,假设扩展属性定义在 StringUtilKt 中
StringUtilKt.getLastChar1("jam");
继承和多态
委托实现
通常如果我们需要扩展某个类,有直接的方法就是直接在原有的类进行修改,但这样不符合设计模式,而且有的类可能是第三方的,无法修改。通常我们还是使用 装饰器模式,这个模式本质是新建一个类,实现原来的接口,并将实现直接转发到原始类的实例。但是这样就会有很多重复的代码,实现接口只是做转发动作。
kotlin提供了 by
将接口的实现委托到另一个对象
class A<T>(innerList : Collection<T> = ArrayList<T>())
: Collection<T> by innerList {}
这样,虽然需要实现Collection,但是不用些对应接口的方法了,该类实例的实现会委托innerList 实现,因为ArrayList 也实现了Collection接口
重写
如果个类可以被继承,方法可以被重写,那么需要在类或者方法中增加 open关键字,默认是不可以的。
重载
之前写Java我们有时需要写几个带默认参数的重载方法,这样就需要定义多个方法。
在Kotlin中,当调用一个Kotlin定义的函数时,可以显示地标明一些参数的名称,而且可以打乱顺序参数调用顺序,因为可以通过参数名称就能唯一定位具体对应参数。 类似与matlab,python等。
方法定义
fun method1(p1:Int=10 , p2:Double):Double{
return p1 * p2
}
调用
// 指定p2参数,和顺序无关,p1使用默认参数
val result = method1(p2 = 2.5)
多态
通过 is
, !is
进行类型检查, 使用 as
, as?
进行类型转换。
在kotlin中,如果代码中通过 is
判断知道了类型,接下来的代码就不需要使用as
进行转换了,自动使用判断好的类型操作就行。
fun test(list:Collection<Any>) {
when (list ) {
is List<Any> -> {
// subList是List的方法,自动转换了
list.subList(0,1)
}
is Set<Any> -> {
// 这个转换是多余的
list as Set<Any>
list.size
}
}
}
使用 as 运算符强制转换的过程中,如果类型不兼容会发生运行期异常,抛出 ClassCasException
异常。如果使用 as? 运算符进行转换,在类型不兼容是返回空值,不会抛出异常,所以 as? 运算符称为“安全转换”运算符。
空引用处理
kotlin是不允许空引用操作的,在可能有空引用的地方需要进行特殊的处理。例如定义一个变量,如果可能是null,那么需要在变量后面添加 安全调用运算符 ?
。
问号可以加在任何类型的后面来表示这个类型的变量可以存储null 引用:String?、Int?、MyCustomType? 等等
没有问号的类型表示这种类型的变量不能存储null 引用。这说明所有常见类型默认都是非空的,除非显式地把它标记为可空。
可空操作在发现为null时,就会直接中断操作或者返回null, 可以在变量末尾使用 !!
,遇到null直接抛出异常。
最重要的操作就是和null 进行比较。而且一旦你进行了比较操作,编译器就会记住,并且在这次比较发生的作用域内把这个值当作非空来对待。
例子1:
// a有可能是空,需要这样定义
var a : String? = null
例子2:response 有可能为空,如果response为空,后面获取变量的操作就中断了。
fun testRequest() {
val url = "https://www.baidu.com"
val response = OKHttpUtil.requert(url,"POST", null, null)
println(" message:${response?.message}, body:${response?.body?.string()}")
}
Elvis 运算符:"?: "
kotlin 有方便的运算符来提供代替null 的默认值。它被称作 Elvis 运算符(听说旋转顺时针90度像猫王)。
在JAVA中我们经常有下面的代码
String a = xxx()
if (a == null) {
a = "default";
}
Elvis 运算符接收两个运算数,如果第一个运算数不为null ,运算结果就是第一个运算数;如果第一个运算数为null ,运算结果就是第二个运算数。
如
a?:"default"
非空断言:"!!"
非空断言是Kotlin 提供给你的最简单直率的处理可空类型值的工具。它使用双感叹号表示,可以把任何值转换成非空类型。如果对null 值做非空断言,则会抛出异常。
断言抛出的异常只能说明异常哪一行,不能判断是哪个变量,因此不要在同一行出现多个非空断言
如
person.company!!.address!!.country
延迟初始化的属性
很多框架会在对象实例创建之后用专门的方法来初始化对象。Koti in 通常要求你在构造方法中初始化所有属性,如果某个属性是非空类型,你就必须提供非空的初始化值。否则, 你就必须使用可空类型。如果你这样做,该属性的每一次访问都需要null 检查或者!!运算符。
为了解决这个问题,可以把 属性声明成可以延迟初始化的, 使用 lateinit 修饰符来完成这样的声明。
lateinit 属性常见的一种用法是依赖注入。在这种情况下,lateinit 属性的值是被依赖注入框架从外部设直的。为了保证和各种Java (依赖注入)框架的兼容性, Kotlin 会自动生成一个和lateinit 属性具有相同可见性的字段。如果属性的可见性是public ,生成字段的可见性也是public 。
可空性
泛型的可空性
Kotlin 中所有泛型类和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型的声明都允许为川口,尽管类型参数T 井没有用问号结尾。
Java的可空性
是Kotlin 引以为豪的是和Java 的互操作性,而你知道Java 的类型系统是不支持可空性的。那么当你混合使用Kotlin 和Java 时会发生什么?有些时候Java 代码包含了可空性的信息,这些信息使用注解来表达。当代码中出现了这样的信息时, Kotiin 就会使用它。因此Java 中的 @Nullable String 被Kotlin当作 String?,而@NotNull String 就是String
平台类型
Kotlin 可以识别多种不同风格的可空性注解,包括JSR-305 标准的注解在j avax.annotation 包之中) 、Android 的注解( android.support.annotation )和JetBrains 工具支持的注解( org.jetbrains.annotations )。这里有一个有意思的问题,如果这些注解不存在会发生什么?这种情况下, Java 类型会变成Kotlin 中的平台类型。
平台类型本质上就是Kotlin 不知道可空性信息的类型。既可以把它当作可空类型处理, 也可以当作非空类型处理。
。这意味着,你要像在Java 中一样,对你在这个类型上做的操作负有全部责任。编译器将会允许所有的操作,它不会把对这些值的空安全操作高亮成多余的,但它平时却是这样对待非空类型值上的空安全操作的。
对Kotlin 来说,把来自Java 的所有值都当成可空的是不是更安全?这种设计也许可行,但是这需要对永远不为空的值做大量冗余的nul l 检查,因为Kotlin 编译器无法了解到这些信息。
涉及泛型的话这种情况就更糟糕了。例如,在Kotlin 中,每个来自Java 的ArrayList<String > 都被当作ArrayList < String ? >? ,每次访问或者转换类型都需要检查这些值是否为null ,这将抵消掉安全性带来的好处。编写这样的检查非常令人厌烦,所以Kotlin 的设计者做出了史实用的选择,让开发者负责正确处理来自Java 的值。
String ! 表示法被Katlin 编译器用来表示来自Java 代码的平台类型。你不能在自己的代码中使用这种语法。而且感叹号通常与问题的来源无关,所以通常可以忽略它。它只是强调类型的可空性是未知的。
多线程
- 简化的线程操作
Thread { println("通过Lambda表达式") }.start()
-
复用Java的线程池,kotlin可以调用java库所以直接使用就行
-
使用协程,一个线程可以有多个协程,它比线程更加轻量级,切换快,内存占用低
同步
kotlin没有 synchronized 和 volatile 关键字,改为了注解。
@Synchronized
fun appendText() { }
@Volatile
private var s = ""
集合Collection
Kotlin 并没有自己的集合,都是复用了Java的集合,kotlin的集合操作很方便。
但是还是有点区别:
一种Java 集合接口在Kotlin 中都有两种表示: 一种是只读的,另一种是可变的。
List<String>一一如何表示成了两种不同的Kotlin 类型: 一种是List
函数式编程风格在操作集合时提供了很多优势。大多数任务都可以通过库函数完成,来简化你的代码。在这一节中, 我们将讨论Koti in 标准库中和集合有关的一些函数。
首先定义个数据对象,方便后面的例子
data class Perso口( val 口ame: String , val age: Int)
filter
集合过滤操作,直接filter,不需要collect就返回过滤的列表
val people= listOf(Person (”Alice ” , 2 9) , Person (”Bob ”, 31))
people.filter { it.age > 30 })
max
查找年龄最大的人,有多个相同的返回第一个匹配到的
people.maxBy { it.age }
如果需要查所有最大年龄的人,可以这样写
val maxAge = people.maxBy(Person::age)?.age
val maxAgePeoples = people.filter { it.age == maxAge }
any 和 all
val ageJudge = { p:Person -> p.age <= 30 }
val all = people.all(ageJudge)
val any = people.any(ageJudge)
println( "$all $any")
count 和 size
查找符合条件的数量,下下面count和size结果都是一样的,但是count会更高效
val count = people.count(ageJudge)
val size = people.filter(ageJudge).size
groupBy :把列表转换成分组的map
分组生成map是一个常用的功能,因为hash查找的性能会比列表查找更好。
val ageMap: Map<Int, List<Person>> = people.groupBy { it.age }
flatMap 和flatten :处理嵌套集合中的元素
flatMap 函数做了两件事情:首先根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺〉成一个列表。
假设你有一堆藏书,使用Book 类表示:
class Book (val title: String, val authors: List<String>)
每本书都可能有一个或者多个作者,可以统计出图书馆中的所有作者的set :
books.flatMap { it.authors }. toSet ()
也可以使用flatten(),返回一个平铺后的list
books.map { it.authors }.flatten().toSet()
集合map
filter 函数可以从集合中移除你不想要的元素,但是它并不会改变这些元素。元素的变换是map 的用武之地。
map 函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合。
像下面 ,把数字列表变换成它们平方的列表
>> val list= listOf(l, 2, 3, 4)
>> println(list.map { it * it }
>> [1, 4,9,16]
键和值分别由各自的函数来处理。filterKeys 和mapKeys 过滤和变换map的键,而另外的filterValues 和mapValues 过滤和变换对应的值。
惰性集合操作:序列
在Java集合的流操作中,中间操作不会立即执行,遇到终端操作操作才会进行执行。例如在kotlin中fliter后面不需要跟toList()的终端操作。这些函数会及早地创建中间集合,也就是说每一步的中间结果都被存储在一个临时列表。序列
给了你执行这些操作的另一种选择,可以避免创建这些临时中间对象。
使用方式很简单,可以想象成 stream()方法
listOf(1, 2, 3).asSequence().filter { it > 1 }
kotlin和java一样,也分中间操作和终端操作。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象
重载算术运算符
在Kotlin 中使用约定的最直接的例子就是算术运算符。在Java 中,全套的算术运算只能用于基本数据类型,+运算符可以与String 值一起使用。
二元算术运算
可以重载的二元算数运算有
在使用了operator 修饰符声明了plus 函数之后,你就可以直接使用+号来求和了。前面操作,后面是对应的函数名。
运算符 | 函数名 |
---|---|
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
例子:重载也是适用的
data class Point( val x: Int , val y: Int) {
operator fun plus(other: Point) : Point {
return Point(x + other.x, y + other.y)
}
operator fun plus(other: Int) : Point {
return Point(x + other, y + other)
}
}
使用,kotlin会自动调用对应的方法
fun main () {
var add = Point(1,2) + Point(2,3)
var point = Point(1, 2)
point += Point(2,3)
var add2 = Point(1,2) + 1
}
注意, Kotlin 运算符不会自动支持交换性(交换运算符的左右两边),要严格按照定义的顺序。如果需要Int + Point 的形式,还需要定义新的函数。
operator fun Int.plus(other: Point) : Point {
return Point( toInt() + other.x, toInt() + other.y)
}
// 使用
var add2 = 1 + Point(1,2)
kotlin重载了运算符,也可以使用 +=,-= 这些复合赋值运算符,而且 对应的引用会重新分配。例如 下面例子中,point使用了+=之后,会重新指向了新生产的Point实例。
var point = Point(1, 2)
point += Point(2,3)
有时不想指向新的实例,例如+=的对象是一个集合,我们只是想更改内容而不是修改引用。kotlin也提供了对应的重载方法,plusAssign,minusAssign ,timesAssign 等。当你在代码中用到+=的时候,理论上plus 和plusAssign 都可能被调用。如果在这种情况下,两个函数都有定义且适用,编译器会报错。一种可行的解决办法是,替换运算符的使用为普通函数调用。另一个办法是,用val 替换var ,这样plusAssign 运算就不再适用。但一般来说,最好一致地设计出新的类:尽量不要同时给一个类添加p lu s 和plusAssign 运算。
一元运算符
运算符 | 方法名称 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dee |
算术运算符
Kotlin 没有为标准数字类型定义任何位运算符,因此,也不允许你为自定义类型定义它们。相反,它使用支持中级调用语法的常规函数,可以为自定义类型定义相似的函数。
运算符 | 函数名 |
---|---|
shl | 带符号左移 |
shr | 带符号右移 |
ushr | 无符号右移 |
and | 按位与 |
or | 按住或 |
xor | 按住异或 |
inv | 按住取反 |
运算符例子,也是可以自己重写对应的操作符提供方便的操作
println(0x0F and 0xF0)
比较运算符
data 类已经默认生成了对应比较的方法了。
与算术运算符一样,在Kotlin 中,可以对任何对象使用比较运算符(==、!=、> 、<等),而不仅仅限于基本数据类型。不用像Java 那样调用equals 或compareTo函数,可以直接使用比较运算符。
相等比较
如果在Koti in 中使用 ==
和 !=
运算符,它将被转换成equals 方法的调用。
注意,和所有其他运算符不同的是,==和 !=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null 。比较a == b 会检查a 是否为非空,如果不是,就调用a.equals (b )
a?.equals(b) ?: (b == null)
=== 运算符不能被重载。
还要注意,equals 不能实现为扩展函数,因为继承自Any 类的实现始终优先于扩展函数。
排序运算符: compareTo
Kotlin 支持相同的Comparable 接口。但是接口中定义的compare To 方法可以按约定调用,比较运算符(<,>,<=和>=) 的使用将被转换为compareTo
集合与区间
在Kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为get 运算符方法的调用,井且写入元素将调用set 。Map 和MutableMap 的接口己经定义了这些方法。
我们自己的类也可以重写对应的get和set方法。
例如下面定义Point的get方法,就可以使用 point[0] 表示 point.x,point[1] 表示 point.y。 同理set可以做相同的处理。
operator fun Point.get( index: Int) : Int {
return when (index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("index error!")
}
}
另一个 in
操作,只需要实现 contains
方法就行。
区间
开区间是不包括最后一个点的区间。例如,如果用10..20 构建一个普通的区间(闭区间),该区间则包括10 到20 的所有数字, 包括20 。开区间10 until 20 包括从10 到凹的数字,但不包括20。
1..10 这种操作对应的方法就是 rangeTo, 我们可以定义类的 rangeTo方法进行区间操作。
解构声明和组件函数
解构声明。这个功能允许你展开单个复合值,并使用它来初始化多个单独的变量。一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。
下面例子,接收不需要使用Point,会自动解析成对应两个值
fun tt():Point { return Point(1,2) }
var(x, y) = tt()
data类可以自动解构,如果不是data类,需要定义 componentN 方法
class Book( var x: Int , var y: Int) {
operator fun component1(): Int {
return x
}
operator fun component2(): Int {
return y
}
}
接口声明还有一个常用的例子,就是解析map
for ((key, value) in map) {....}
高阶函数:Lambda作为形参和返回值
函数类型可以帮助去除重复代码。如果你禁不住复制粘贴了一段代码,那么很可能这段重复代码是可以避免的。使用lambda ,不仅可以抽取重复的数据,也可以抽取重复的行为。
一些广为人知的设计模式可以用函数类型和lambda 表达式进行简化。比如策略模式。没有lambda 表达式的情况下,你需要声明一个接口,并为每一种可能的策略提供实现类。使用函数类型,可以用一个通用的函数类型来描述策略,然后传递不同的lambda 表达式作为不同的策略。
内联函数:消除lambda 带来的运行时开销
我们知道在kotlin中lambda 表达式会被正常地编译成匿名类。这表示每调用一次lambda 表达式,一个额外的类就会被创建。并且如果lambda 捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。这会带来运行时的额外开销,导致使用lambda 比使用一个直接执行相同代码的函数效率更低。
如果使用 inline
修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。
内联函数的限制
如果( lambda )参数被调用,这样的代码能被容易地内联。但如果( lambda )参数在某个地方被保存起来,以便后面可以继续使用, lambda 表达式的代码将不能被内联, 因为必须要有一个包含这些代码的对象存在。
一般来说,参数如果被直接调用或者作为参数传递给另外一个inline 函数,它是可以被内联的。否则,编译器会禁止参数被内联并给出错误信息“ Illegal usage of inline-parameter ”(非法使用内联参数)。
可以用 noinline
修饰符来标记不内联。
从lambda 返回:使用标签返回
可以在lambda 表达式中使用局部返回。lambda 中的局部返回跟for循环中的break 表达式相似。它会终止lambda 的执行,并接着从调用lambda 的代码处执行。要区分局部返回和非局部返回, 要用到标签。想从一个lambda 表达式处返回你可以标记它,然后在return 关键字后面引用这个标签。
要标记一个lambda 表达式,在lambda 的花括号之前放一个标签名(可以是任何标识符),接着放一个e 符号。要从一个lambda 返回,在return 关键字后放一个@符号,接着放标签名。
listOf(1, 3).forEach label@{
if(it == 1) return@label
}
println("haha")
上面例子中,定义了lable标记,return 会结束forEach,如果直接使用return,会导致方法放回,不执行后面的输出。
匿名函数:默认使用局部返回
匿名函数是一种不同的用于编写传递给函数的代码块的方式。匿名函数看起来跟普通函数很相似,除了它的名字和参数类型被省略了外。
listOf(1, 3).forEach(fun (p:Int){
if(p == 1) return
})
这条规则很简单: return 从最近的使用fun 关键字声明的函数返回。lambda 表达式没有使用fun关键字,所以lambda 中的return 从最外层的函数返回。匿名函数使用了fun ,因此,在前一个例子中匿名函数是最近的符合规则的函数。所以, return 表达式从匿名函数返回,而不是从最外层的函数返回。
泛型
实化类型参数允许你在运行时的内联函数调用中引用作为类型实参的具体类型( 对普通的类和函数来说,这样行不通,因为类型实参在运行时会被擦除)。声明点变型可以说明一个带类型参数的泛型类型,是否是另一个泛型类型的子类型或者超类型,它们的基础类型相同但类型参数不同。例如,它能调节是否可以把List < Int > 类型的参数传给期望List<Any>的函数。使用点变型在具体使用一个泛型类型时做同样的事,达到和Java 通配符一样的效果。
和Java 一样, Kotlin 通过在类名称后加上一对尖括号,井把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。
类型形参上界
把冒号放在类型参数名称之后, 作为类型形参上界的类型紧随其后。在Java中, 用的是关键字extends 来表达一样的概念:
fun <T:Number> List<T>.getFirst(): T = first()
有时上界是多个时,可以使用其他语法来定义,但是上界只能有一个是类,可以多个接口
fun <T> List<T>.getFirst1() where T: Number, T: Appendable = first()
一般,如果没有指定上界,默认是 Any?
,是可空的。如果想要非空的,需要显式指定 Any
运行时的泛型:擦除和实化类型参数
JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。
和Java 一样, Kotlin 的泛型在运行时也被擦除了。这意味着泛型类实例不会携带用于创建它的类型实参的信息。例如,如果你创建了一个List<String >并将一堆字符串放到其中,在运行时你只能看到它是一个List.
Kotlin 不允许使用没有指定类型实参的泛型类型。那么你可能想知道如何检查一个值是否是列表,而不是set 或者其他对象。可以使用特殊的 星号投影
语法来做这种检查:
if ( l is List<*>) {}
注意, Kotlin 编译器是足够智能的,在编译期它已经知道相应的类型信息时,is 检查是允许的。
子类型和超类型
任何时候如果需要的是类型A 的值,你都能够使用类型B 的值(当作A 的值), 类型B就称为类型A的子类型。举例来说, Int 是Number 的子类型,但Int不是String 的子类型。这个定义还表明了任何类型都可以被认为是它自己的子类型
。
超类型
是子类型的反义词。如果A 是B 的子类型,那么B 就是A 的超类型。
如果对于任意两种类型A 和B,MutableList<A>既不是MutableList<B > 的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。Java 中所有的类都是不变型的。
协变:保留子类型化关系
如果A 是B 的子类型,那么List就是List<B>的子类型。这样的类或者接口被称为协变的。
如果A 是B 的子类型,那么Producer<A>就是Producer的子类型。我们说子类型化被保留了。
在Kotlin 中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out 关键字即可。
注解
在Kotlin 中使用注解的方法和Java 一样。要应用一个注解,以@字符作为(注解)名字的前缀,并放在要注解的声明最前面。
@JvmStatic
fun getClient(): OkHttpClient? { return client}
@ Deprecated 注解。它在Kotlin 中的含义和Ja va 一样,但是Kotlin 用replace W ith 参数增强了它,让你可以提供一个替代者的(匹配)模式,以支持平滑地过渡到API 的新版本。
Kotlin 支持的使用点目标的完整列表如下:
• property 一Java 的注解不能应用这种使用点目标。
• field 一为属性生成的字段。
• get 一属性的getter 。
• set 一 )寓’性的setter 。
• receiver 一扩展函数或者扩展属性的接收者参数。
• param一构造方法的参数。
• setparam一属性se 忧er 的参数。
• delegate 一为委托属性存储委托实17u 的字段。
• file 一包含在文件中声明的顶层函数和属性的类。
任何应用到file 目标的注解都必须放在文件的顶层,放在package 指令之前。@JvmName 是常见的应用到文件的注解之一,它改变了对应类的名称。
@file:JvmName("StringName")
注意,和Java 不一样的是, Kotlin 允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。最常见的例子就是@ Suppress 注解,可以用它抑制被注解的表达式的上下文中的特定的编译器警告。
元注解
和Java 一样,一个Kotlin 注解类自己也可以被注解。可以应用到注解类上的注解被称作元注解。标准库中定义了一些元注解,它们会控制编译器如何处理注解。标准库定义的元注解中最常见的就是@Target 。说明了注解可以被应用的元素类型。如果不使用它,所有的声明都可以应用这个注解。
@Retention 注解,它被用来说明你声明的注解是否会存储到.class 文件,以及在运行时是否可以通过反射来访问它。Java 默认会在.class 文件中保留注解但不会让它们在运行时被访问到。大多数注解确实需要在运行时存在,所以Kotlin 的默认行为不同:注解拥有RUNTIME保留期。
反射
反射是,简单来说,一种在运行时动态地访问对象属性和方法的方式。
当在Kotlin 中使用反射时,你会和两种不同的反射API 打交道。
第一种是标准的Java 反射,定义在包j ava.lang.reflect 中。因为Kotlin 类会被编译成普通的Java 字节码, Java反射API 可以完美地支持它们。
第二种是Kotlin 反射API , 定义在包kotlin.reflect 中。它让你能访问那些在Java 世界里不存在的概念,诸如属性和可空类型。但这一次它没有为Java 反射API 提供一个面面俱到的替身,而且不久你就会看到,有些情况下你仍然会回去使用Java 反射。
注意:为了降低大小, Kotlin 反射API 被打包成了单独的.jar 文件,即kotlin-reflect.jar。它不会被默认地添加到新项目的依赖中。
kotlin 反射主要的类
Kotlin 反射API 的主要入口就是KClass ,它代表了一个类。KClass 对应的是java.lang.class。
要在运行时取得一个对象的类,首先使用 javaClass
属性获得它的Java 类,这直接等价于Java 中的j ava.lang.Object.getClass() 。然后访问该类的 .kotlin
扩展属性,从Java 切换到Kotlin 的反射API:
val kClass = person.javaClass.kotlin
println(kClass.simpleName)
KCallable是函数和属性的超接口。它声明了call 方法, 允许你调用对应的函数或者对应属性的getter。
val kClass = person.javaClass.kotlin
println(kClass.members.first { it.name == "name" }.call(person))
对应的setter
val name = kClass.memberProperties.first { it.name == "name" }
if (name is KMutableProperty<*>) {
name.setter.call(person1, "Jam")
}
也有另一种用法,使用方法引用
val age = Person::age
age.set(person, 29)
println(age.get(person))
KFunctionN : 合成的编译器生成类型
看下面例子
private fun f(i: Int) {
println("not implemented")
}
var ff = ::f
ff(1)
ff.invoke(1)
你会发现 ff 的类型是 Function1,在reflect 包中是没有这个类型的。
像KFunctionl 这样的类型代表了不同数量参数的函数。每一个类型都继承了KFunction 并加上一个额外的成员invoke ,它拥有数量刚好的参数。例如, KFunction2 声明了。operator fun invoke (p1:P2, p2:P2): R,其中P1和P2 代表着函数的参数类型,而R 代表着函数的返回类型。这样有个好处是减少了反射包的大小,同时避免了对函数类型参数数量的人为限制。
欢迎关注个人公众号