Kotlin学习笔记(一)

0.052字数 4185阅读 3283

本文介绍了Kotlin入门应该知道一些基本语法概念。包括变量、常量、函数、空安全、类定义、类继承、数据类、接口定义、冒号、可见性、扩展函数、Anko、对象表达式和声明、Lambda表达式、when表达式、with函数、内联函数、Kotlin Android Extensions等。

本文首发:http://yuweiguocn.github.io/

《送孟浩然之广陵》
故人西辞黄鹤楼,烟花三月下扬州。
孤帆远影碧空尽,唯见长江天际流。
—唐,李白

本文所有用例基于Android Studio 3.0.1、Kotlin 1.2版本。

引入

在项目根目录下 build.gradle 文件中添加 kotlin 插件依赖:

buildscript {
    ext.gradle_plugin_version = '3.0.1'
    ext.kotlin_version = '1.2.0'
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_plugin_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

在主 module 下 build.gradle 文件中添加 kotlin 依赖:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
...
...
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

如果开启了 Data Binding,还需要添加如下依赖:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
    dataBinding {
        enabled = true
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    kapt "com.android.databinding:compiler:$gradle_plugin_version"
}

变量

在 kotlin 中一切皆为对象,没有像 Java 中的原始基本类型。在 kotlin 中使用 var 修饰的为变量。例如我们定义一个 Int 类型的变量并赋值为1:

var a: Int = 1
a += 1

由于 kotlin 编译器可以自动推断出变量的类型,所以我们通常不需要指定变量的类型

var s = "String" //类型为String
var a = 1 //类型为Int

在 kotlin 中分号不是必须的,不使用分号是一个不错的实践

常量

在 kotlin 中使用 val 修饰的为常量。这和 java 中的 final 很相似。在 kotlin 中有一个重要的概念是:尽可能地使用 val

val s = "String" //类型为String
val ll = 22L //类型为Long
val d = 2.5 //类型为Double
val f = 5.5F //类型为Float

函数

定义一个函数接受两个 Int 型参数,返回值类型为 Int :

fun sum(a: Int, b: Int): Int {
    return a + b
}

只有一个表达式作为函数体,以及自推导型的返回值:

fun sum(a: Int, b: Int) = a + b

函数的参数可以指定默认值:

fun sum(a: Int, b: Int = 10) = a + b

var c = sum(10) //调用

Unit 表示无返回值,对应 java 中 void:

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

Unit 的返回类型可以省略:

fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}")
}

空安全

在 kotlin 中,默认定义的变量不能为 null 的,这可以避免很多的 NullPointerException。

var a: String ="abc"
a = null //编译错误

指定一个变量可null是通过在类型的最后增加一个问号

var b: String? = "abc"
b = null

当变量声明为可空时,在调用它的属性时无法通过编译:

var b: String? = "abc"
val l = b.length //编译错误

在这种情况下,我们可以使用安全操作符 ?.

var b: String? = "abc"
val l = b?.length 

如果 b 不为空则返回长度,否则返回空,这个表达式的的类型是 Int?

我们还可以使用 ?: 操作符,当前面的值不为空取前面的值,否则取后面的值,这和java中三目运算符类似。

val a:Int? = null
val myString = a?.toString() ?: ""

因为在Kotlin中 throw 和 return 都是表达式,他们可以用在Elvis operator操作符的右边:

val myString = a?.toString() ?: return false
val myString = a?.toString() ?: throw IllegalStateException()

如果你确定该变量不为空,可以使用 !! 操作符:

var b: String? = "abc"
val l = b!!.length 

使用 !! 操作符可以跳过限制检查通过编译,此时如果变量为空会抛出空指针异常。如果大量使用此操作符,显然不是很好的处理。

类定义

使用 class 定义一个类。类的声明包含类名,类头(指定类型参数,主构造函数等等),以及类主体,用大括
号包裹。类头和类体是可选的;如果没有类体可以省略大括号。

class MainActivity{
}

在 Kotlin 中类可以有一个主构造函数以及多个二级构造函数。主构造函数是类头的一部分:跟在类名后面(可以有可选的类型参数)。

class Person constructor(firstName: String) {
}

如果主构造函数没有注解或可见性说明,则 constructor 关键字是可以省略:

class Person(name: String, surname: String)

构造函数的函数体可以写在 init 块中:

class Customer(name: String) {
    init {
        logger.info("Customer initialized with value ${name}")
    }
}

注意主构造函数的参数可以用在初始化块内,也可以用在类的属性初始化声明处:

class Customer(name: String) {
    val customerKry = name.toUpperCase()
}

事实上,声明属性并在主构造函数中初始化,在 Kotlin 中有更简单的语法:

class Person(val firstName: String, val lastName: String, var age: Int) {
}

就像普通的属性,在主构造函数中的属性可以是可变的( var )或只读的( val )。

类继承

Kotlin 中所有的类都有共同的父类 Any,它是一个没有父类声明的类的默认父类:

class Example // 隐式继承于 Any

Any 不是 java.lang.Object ;事实上它除了 equals() , hashCode() 以及 toString() 外没有任何成员了。

默认情况下,kotlin 中所有的类都是不可继承 (final) 的,所以我们只能继承那些明确声明为 openabstract 的类,当我们只有单个构造器时,我们需要在从父类继承下来的构造器中指定需要的参数。这是用来替换Java中的 super 调用的。

open class Example(name: String)

class MyExample(name: String, age: Int) : Example(name)

数据类

数据类是一种非常强大的类,它可以让你避免创建Java中的用于保存状态但又操作非常简单的POJO的模版代码。它们通常只提供了用于访问它们属性的简单的getter和setter。定义一个新的数据类非常简单:

data class Forecast(val date: Date, val temperature: Float, val details: String)

编译器会自动根据主构造函数中声明的所有属性添加如下方法:

  • equals(): 它可以比较两个对象的属性来确保他们是相同的。
  • hashCode(): 我们可以得到一个hash值,也是从属性中计算出来的。
  • toString(): 格式是 "User(name=john, age=42)"
  • copy(): 你可以拷贝一个对象,可以根据你的需要去修改里面的属性。
  • componentN()函数 对应按声明顺序出现的所有属性

定义数据类需要注意的地方:

  • 主构造函数应该至少有一个参数。
  • 数据类的变量属性只能是 varval 的。
  • 数据类不能是 abstract,open,sealed,或者 inner 。

复制数据类并修改某一属性值:

val f1 = Forecast(Date(), 27.5f, "Shiny day")
val f2 = f1.copy(temperature = 30f)

映射对象的每一个属性到一个变量中,这个过程就是我们知道的多声明。这就是为什么会有 componentN 函数被自动创建。使用上面的 Forecast 类举个例子:

val f1 = Forecast(Date(), 27.5f, "Shiny day")
val (date, temperature, details) = f1

上面这个多声明会被编译成下面的代码:

val date = f1.component1()
val temperature = f1.component2()
val details = f1.component3()

这个特性背后的逻辑是非常强大的,它可以在很多情况下帮助我们简化代码。举个例子, Map 类含有一些扩展函数的实现,允许它在迭代时使用key和value:

for ((key, value) in map) {
    Log.d("map", "key:$key, value:$value")
}

接口定义

Kotlin 的接口很像 java 8。它们都可以包含抽象方法,以及方法的实现。和抽象类不同的是,接口不能保存状态。可以有属性但必须是抽象的,或者提供访问器的实现。
接口用关键字 interface 来定义:

interface Bar {
    fun bar()
    fun foo() {
        //函数体是可选的
    }
}

冒号

在冒号区分类型和父类型中要有空格,在实例和类型之间是没有空格的:

interface Foo<out T : Any> : Bar {
    fun foo(a: Int): T
}

可见性

在 kotlin 中,默认修饰符为 public

修饰符 说明
private 当前类可见
protected 成员自己和继承它的成员可见
internal 当前 module 可见
public 所有地方可见

扩展函数

扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。这是一个在缺少有用函的类上扩展的方法。在Java中,通常会实现很多带有static方法的工具类。Kotlin中扩展函数的一个优势是我们不需要在调用方法的时候把整个对象当作参数传入。扩展函数表现得就像是属于这个类的一样,而且我们可以使用 this 关键字和调用所有public方法。

举个例子,我们可以创建一个toast函数,这个函数不需要传入任何context,它可以被任何Context或者它的子类调用,比如Activity或者Service:

fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

这个方法可以在Activity内部直接调用:

toast("Hello world!")
toast("Hello world!", Toast.LENGTH_LONG)

扩展函数也可以是一个属性。所以我们可以通过相似的方法来扩展属性。下面的例子展示了使用他自己的getter/setter生成一个属性的方式。Kotlin由于互操作性的特性已经提供了这个属性,但理解扩展属性背后的思想是一个很不错的练习:

public var TextView.text: CharSequence
    get() = getText()
    set(v) = setText(v)

扩展函数并不是真正地修改了原来的类,它是以静态导入的方式来实现的。扩展函数可以被声明在任何文件中,因此有个通用的实践是把一系列有关的函数放在一个新建的文件里。

Anko

Anko是JetBrains开发的一个强大的库。它主要的目的是用来替代以前XML的方式来使用代码生成UI布局。Anko包含了很多的非常有帮助的函数和属性来避免让你写很多的模版代码。通过查看Anko源码学习kotlin语言是一种不错的方法。Anko能帮助我们简化代码,比如,实例化Intent,Activity之间的跳转,Fragment的创建,数据库的访问,Alert的创建等等。

github地址:https://github.com/Kotlin/anko

添加Anko的依赖:

// 主工程目录下build.gradle文件中声明版本
buildscript {
    ext.anko_version = '0.10.0'
}

// module的下build.gradle文件中添加依赖
dependencies {
    compile "org.jetbrains.anko:anko:$anko_version"
}

例如执行Activity的跳转:

startActivity<MainActivity>()

//传递Intent参数
startActivity<NewHomeActivity>("name1" to "value1","name2" to "value2")

在Activity中显示Toast:

toast("Hello world!")
longToast(R.id.hello_world)

线程切换:

async {
    val response = URL("http://yuweiguocn.github.io").readText()
    uiThread {
        textView.text = response
    }
}

对象表达式和声明

有时候我们想要创建一个对当前类有一点小修改的对象,但不想重新声明一个子类。java 用匿名内部类的概念解决这个问题。Kotlin 用对象表达式和对象声明巧妙的实现了这一概念。

window.addMouseListener(object: MouseAdapter () {
    override fun mouseClicked(e: MouseEvent) {
        //...
    }
})

像 java 的匿名内部类一样,对象表达式可以访问闭合范围内的变量 (和 java 不一样的是,这些变量不用是 final 修饰的)

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }
        override fun mouseEntered(e: MouseEvent){
            enterCount++
        }
    })
}

单例模式是一种很有用的模式,Kotln 中声明它很方便,其中init代码块对应java中static代码块。

object DataProviderManager {
    init { //对应java中static代码块
    
    }

    fun registerDataProvider(provider: DataProvider) {
        // ...
    }
    val allDataProviders: Collection<DataProvider>
    get() = // ...
}

这叫做对象声明,跟在 object 关键字后面是对象名。和变量声明一样,对象声明并不是表达式,而且不能作为右值用在赋值语句。想要访问这个类,直接通过名字来使用这个类:

// in kotlin
DataProviderManager.registerDataProvider(...)

// in java
DataProviderManager.INSTANCE.registerDataProvider(...)

注意:对象声明不可以是局部的(比如不可以直接在函数内部声明),但可以在其它对象的声明或非内部类中进行内嵌入。

我们需要一个类里面有一些静态的属性、常量或者函数,我们可以使用伴随对象。这个对象被这个类的所有对象所共享,就像java中的静态属性或者方法。在类声明内部可以用 companion 关键字标记对象声明:

class MyClass {
    companion object Factory {
        val URL = "http://yuweiguocn.github.io/"
        fun create(): MyClass = MyClass()
    }
}

伴随对象的成员可以通过类名做限定词直接使用:

// in kotlin
val instance = MyClass.create()
val url = MyClass.URL

// in java
MyClass c = MyClass.INSTANCE.create()
String url = MyClass.INSTANCE.getURL()

在使用了 companion 关键字时,伴随对象的名字可以省略。

class MyClass {
    companion object {
        fun create(): MyClass = MyClass()
    }
}

注意:在kotlin中没有 new 关键字

对象表达式和声明的区别:

  • 对象表达式在我们使用的地方立即初始化并执行的
  • 对象声明是懒加载的,是在我们第一次访问时初始化的。
  • 伴随对象是在对应的类加载时初始化的,和 Java 的静态初始是对应的。

Lambda表达式

Lambda表达式是一种很简单的方法,去定义一个匿名函数。Lambda是非常有用的,因为它们避免我们去写一些包含了某些函数的抽象类或者接口,然后在类中去实现它们。在Kotlin,我们把一个函数作为另一个函数的参数。

我们用Android中非常典型的例子去解释它是怎么工作的: View.setOnClickListener() 方法。如果我们想用Java的方式去增加点击事件的回调,我首先要编写一个 OnClickListener 接口:

public interface OnClickListener {
    void onClick(View v);
}

然后我们要编写一个匿名内部类去实现这个接口:

view.setOnClickListener(new OnClickListener(){
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "Click", Toast.LENGTH_SHORT).show();
    }
});

我们将把上面的代码转换成Kotlin(使用了Anko的toast函数):

view.setOnClickListener(object : OnClickListener {
    override fun onClick(v: View) {
        toast("Click")
    }
}

Kotlin允许Java库的一些优化,Interface中包含单个函数可以被替代为一个函数。如果我们这么去定义了,它会正常执行:

fun setOnClickListener(listener: (View) -> Unit)

一个lambda表达式通过参数的形式被定义在箭头的左边(普通圆括号包围),然后在箭头的右边返回结果值。当我们定义了一个方法,我们必须使用大括号包围。如果左边的参数没有用到,我们甚至可以省略左边的参数。

view.setOnClickListener({ view -> toast("Click")})

view.setOnClickListener({ toast("Click") })

如果这个函数只接收一个参数,我们可以使用it引用,而不用去指定左边的参数:

view.setOnClickListener({ toast("Click" + it.id)})

如果这个函数的最后一个参数是一个函数,我们可以把这个函数移动到圆括号外面:

view.setOnClickListener() { toast("Click") }

并且,最后,如果这个函数只有一个参数,我们可以省略这个圆括号:

view.setOnClickListener { toast("Click") }

When表达式

when 表达式与Java中的 switch/case 类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达式。与Java的 switch/case 不同之处是参数可以是任何类型,并且分支也可以是一个条件。

对于默认的选项,我们可以增加一个 else 分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块:

when (x){
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> {
        print("I'm a block")
        print("x is neither 1 nor 2")
    }
}

因为它是一个表达式,它也可以返回一个值。我们需要考虑什么时候作为一个表达式使用,它必须要覆盖所有分支的可能性或者实现 else 分支。否则它不会被编译成功:

val result = when (x) {
    0, 1 -> "binary"
    else -> "error"
}

with函数

with是一个非常有用的函数,包含在Kotlin的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为this一样使用所有它的public方法和属性。当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。

data class Person(val name: String, val age: Int)

val p = Person("growth",25)
with(p){
    var info = “$name - $age” 
}

内联函数

下面是with函数的定义:

inline fun <T> with(t: T, body: T.() -> Unit) { t.body() }

这个函数接收一个 T 类型的对象和一个被作为扩展函数的函数。它的实现仅仅是让这个对象去执行这个函数。因为第二个参数是一个函数,所以我们可以把它放在圆括号外面,所以我们可以创建一个代码块,在这这个代码块中我们可以使用 this 和直接访问所有的public的方法和属性。

内联函数与普通的函数有点不同。一个内联函数会在编译的时候被替换掉,而不是真正的方法调用。这在一些情况下可以减少内存分配和运行时开销。举个例子,如果我们有一个函数,只接收一个函数作为它的参数。如果是一个普通的函数,内部会创建一个含有那个函数的对象。另一方面,内联函数会把我们调用这个函数的地方替换掉,所以它不需要为此生成一个内部的对象。

inline fun supportsLollipop(code: () -> Unit) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        code()
    }
}

它只是检查版本,然后如果满足条件则去执行。现在我们可以这么做:

supportsLollipop {
    window.setStatusBarColor(Color.BLACK)
}

Kotlin Android Extensions

Kotlin Android Extensions是另一个kotlin团队研发的可以让开发更简单的插件。该插件依赖于 kotlin 标准库,当前仅仅包括了view的绑定,这可以让我们省去findViewById操作。

使用该插件非常简单,修改module的build.gradle文件:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

例如在布局文件中定义一了个id为tvTest的TextView,在Activity的setContentView之后就可以直接使用该TextView了:

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvTest.text = "hello world"
    }
}

参考

推荐阅读更多精彩内容