Kotlin基础学习

背景

Google I/O 2017,Kotlin正式成为Android的官方开发语言。

Kotlin语言主要有以下几个特点:

  • 高度兼容Java:Kotlin也可以调用传统Java语言的各种类库。由于最终都是编译为JVM字节码运行,Kotlin可以与Java存在于同一个项目中,互相调用。开发者可以部分改造项目中的代码,尝试其新语言特性。官方提供Java代码到Kotlin代码的转换工具。开发者可以把现有的Java/Android项目一键转换为Kotlin项目,实现零成本的改造。
  • 代码简洁高效:语法精炼,无处不在的类型推断,自动生成通用方法,支持Lamda表达式,使Kotlin的代码行数远低于传统Java代码。而代码量的压缩,客观上会提高开发效率。
  • 空指针安全:通过符号“?”定义对象的nullable,让开发者不用为了避免NPE而写太多防御性代码。
  • 函数式编程:支持让函数作为参数或者返回的高阶函数用法,支持流式API,支持扩展方法。

语法速看

1. 变量、类型与符号

基础类型

java中存在int,float,boolean等基础类型,这些基础类型在Kotlin里将全部以对象的形式继续存在。随之而来使用上也发生一些变化,有几点需要注意下:

// Int无法自动转换为Double, 需要自己先做类型转换(as Double, toDouble(), 方式很多)
var a: Int = 2
var b: Double = a.toDouble()
// Char不能直接等值于其对应的ASCII码值,需要类型转换
var c: Char = 'a'
var ca: Int = c.toInt()

变量声明

Kotlin中使用var定义变量(和js很像),使用val定义常量(相当于java当中的final)。定义变量时既可以指定类型,也可以不指定,Kotlin支持类型推断。

var name = "me" // 类型为String
var name: String = "me" // 类型为String
val TAG: String = "KotClassA"

需要注意的是,你不能直接写一个var num丢在代码里不初始化值,编译器会无法推断它的类型

var name // 编译器报错
var name: String // 类型为String

行尾标识

与Java中以“;”符号区分每行的结尾不同,Kotlin中不再需要在每行代码的结束位置写“;”。当然如果你一时改不了写Java时的习惯,IDE也并不会报错,只会友好地把“;”置灰并下划线,提示你这是多余之举。

类型声明

Java语言中,“:”符号主要出现于三元运算符“A ? B : C”中,for循环时遍历列表项和switch的每种情形分支。而在Kotlin语言中,“:”被广泛用于变量类型的定义。

// 定义变量类型
fun common() {
    var ka: KClassA
    var kb: KClassB
}
// 定义函数的参数和返回值
fun helloStr(str: String): String {
    var strHello: String = "hello"
    strHello += str
    return strHello
}

“:”还被用于声明类继承或接口实现。

interface KInterfaceA {
    ...
}
open class KClassA(n: Int): KInterfaceA {
    ...
}
class KClassB(n: Int): KClassA(n) {
    ...
}

此外,如果你想在Kotlin代码中使用Java类,也需要用到“:”符号。连续两个“:”表明对Java类的调用。

val intent = Intent(this, MainActivity::class.java)

类型检测

Java语言中使用instanceof来判断某变量是否为某类型,而Kotlin中使用更短小更直观的is来进行类型检测。

if (num instanceof Double) { ... }  // Java代码
if (num is Double) { ... }  // Kotlin代码

字符串模板

Java中使用字符串模板会比较麻烦,而且不太直观。而Kotlin里使用则异常便捷好用。

// Java中字符串模板两种常用形式
String name = "我";
int age = 25;
String.format("%s今年%d岁", name, age);
MessageFormat.format("{0}今年{1}岁", name, age);
// Kotlin中更直观的字符串模板形式
var name = "我"
var age = 25
"${name}今年${age}岁"

2.函数与方法

Kotlin中的函数通过关键字fun定义的,具体的参数和返回值定义结构如下。

fun test(para1: Int, para2: String): String { ... }

Kotlin中的函数可以是全局函数,成员函数或者局部函数,甚至还可以作为某个对象的扩展函数临时添加,这个作为Kotlin的一大实用特性,下文会有具体讲解。

Kotlin函数参数还有默认值和可变参数的特性,分别来看一下:

对Kotlin函数中的某个参数可以用“=”号指定其默认值,调用函数方法时可不不传这个参数,但其他参数需要用“=”号指定。下文例子中没有传递参数para2,其实际值为默认值"para2"

fun test(para1: Int, para2: String = "para2", para3: String): String { ... }
test(22, para3 = "hello")

可变参数值的话,需要用关键字vararg来定义。这里需要注意的是,一个函数仅能有一个可变参数。该可变参数不一定得是最后一个参数,但当这种情况下时,调用该方法,需要给其他未指明具体值的参数传值。

fun test(vararg para1: String, para2: String): String { ... }
test("para1", "para4", "para5", para2 = "hello")

3.类与继承

Kotlin中也使用class关键字定义类,所有类都继承于Any类,类似于Java中Object类的概念。类实例化的形式也与Java一样,但是去掉了new关键字。

构造函数

类的构造函数分为primary constructor和secondary constructor,前者只能有一个,而后者可以有多个。如果两者都未指定,则默认为无参数的primary constructor。

primary constructor是属于类头的一部分,用constructor关键字定义,无可见性字段定义时可省略。初始化代码需要单独写在init代码块中,方法参数只能在init代码块和变量初始化时使用。

secondary constructor也是用constructor关键字定义,必须要直接或间接代理primary constructor。

class Student(name: String) { // primary constructor
    var mName: String = name
    init {
        println("Student is called " + name)
    }

    constructor(age: Int, name: String):this(name) {
        println("Student is ${age} years old")
    }
}

继承

类继承使用符号“:”表示,接口实现也一样,不做原本Java中的extends和implement关键字区分。Kotlin有一点与Java大为不同,即Java中类默认可被继承,只有被final关键字修饰的类才不能被继承。而Kotlin中直接取消了final关键字,所有类均默认不可被继承。神马?这还怎么面向对象编程?先别急,Kotlin中新增了open关键字,仅有被open修饰的类才可以被继承。

单例与伴随对象

日常开发中写一个单例类是很常见的行为,Kotlin中直接将这种设计模式提升到语言级别,使用关键词object定义单例类。这里需要注意,是全小写。Kotlin中区分大小写,Java中原本指所有类的父类Object已弃用。单例类访问直接使用类名,无构造函数。

object Shop(name: String) {
    fun buySomething() {
        println("Bought it")
    }
}
Shop.buysomething()

Java中使用static标识一个类里的静态属性或方法,可以被这个类的所以实现使用。Kotlin改为使用伴随对象,用companion修饰单例类object,来实现静态属性或方法功能。

class Mall(name: String) {
    companion object Shop {
        val SHOP_NAME: String = "McDonald" // 等同于Java中写public static String
        fun buySomething() { // 等同于Java中写public static void
            println("Bought it")
        }
    }
}
Mall.buySomething()

4.逻辑语句

if-else语句

Kotlin中的if-else语句与Java一致,结构上都是if (条件A) { 条件A为真要执行的操作 } else { 条件A为假要执行的操作 }

这里主要要介绍的是Kotlin的一个特点,即if-else语句可以作为一个逻辑表达式使用。不仅如此,逻辑表达式还可以以代码块的形式出现,代码块最后的表达式作为该块的值。

// 逻辑表达式的使用
fun maxNum(x: Int, y: Int): Int {
    var max = if (x > y) x else y
    return max
}
// 代码块形式的逻辑表达式
fun maxNumPlus(x: Int, y: Int): Int {
    var max = if (x > y) {
        println("Max number is x")
        x
    } else {
        println("Max number is y")
        y
    }
    return max
}

when语句

Kotlin中的when语句取代了Java中的switch-case语句,功能上要强大许多,可以有多种形式的条件表达。与if-else一样,Kotlin中的when也可以作为逻辑表达式使用。

// 逻辑表达式的使用
fun judge(obj: Any) {
    when (obj) {
        1 -> println("是数字1")
        -1, 0 -> println("是数字0或-1")
        in 1..10 -> println("是不大于10的正整数")
        "abc" -> println("是字符串abc")
        is Double -> println("类型是双精度浮点数")
        else -> println("其他操作")
    }
}

标签

Kotlin中可以对任意表达式进行标签标记,形式为abc@,xyz@。而这些标签,可以搭配return、break、continue等跳转行为来使用。

fun labelTest() {
    la@ for (i in 1..10) {
        println("outer index " + i)
        for (j in 1..10) {
            println("inner index " + j )
            if ( inner % 2 == 0) {
                break@la
            }
        }
    }
}

其他

for语句、while语句、continue语句和break语句等逻辑都与Java基本一致,这里不再赘述。

实用特性

1.空指针安全

在写Java代码时,最常出现在线上的crash问题大概就是NullPointerException了。技术上来说这样的问题修复起来很快,没什么难度,但往往由于日常开发中没有写足够多的防御性代码,导致此类问题一直困扰着我们。Java作为一个古老的语言并没有空指针安全功能,这使得当对象层级套用的时候,想要获取最里面的某个属性,需要从外到内依次做一遍非空判断来避免NPE。相似的场景太多,导致这在代码成本上是很大的。

Kotlin中,当我们定义一个变量时,其默认就是非空类型。如果你直接尝试给他赋值为null,编译器会直接报错。Kotlin中将符号“?”定义为安全调用操作符。变量类型后面跟?号定义,表明这是一个可空类型。同样的,在调用子属性和方法时,也可以用字符?进行安全调用。Kotlin的编译器会在写代码时就检查非空情况,因此下文例子中,当s2有前置条件判断为非空后,即便其本身是可空类型,也可以安全调用子属性或方法。对于ifelse结构的逻辑,Kotlin还提供了“?:”操作符,极大了简化了代码量又不失可读性。Kotlin还提供“!!”双感叹号操作符来强制调用对象的属性和方法,无视其是否非空。这是一个挺危险的操作符,除非有特殊需求,否则为了远离NPE,还是少用为妙。

var s1: String = "abc"
s1 = null // 这里编译器会报错
var s2: String? = "abc"
s2 = null // 编译器不会报错

var l1 = s1.length // 可正常编译
var l2 = s2.length // 没有做非空判断,编译器检查报错

if (s2 != null) s2.length // Java式的判空方案
s2?.length // Kotlin的安全调用操作符?。当s2为null时,s2?.length也为null
if (s2 != null) s2.length else -1 // Java式判空的ifelse逻辑
s2?.length ?: -1 // Kotlin的elvis操作符
s2!!.length // 可能会导致NPE

扩展方法和属性

相信作为一个Java/Android开发者,大家都写过很多Base类,继承原生父类的同时,封装一些通用方法,供子类使用。亦或是把这类通用方法,专门放置到一个XXUtils类里,作为工具类出现。这样做是为了代码结构的清晰,但也是一种无奈。由于无法修改原生类的内容,我们只能借助继承或者以面向方法的思维来写工具类。这点在Kotlin里得到了完美解决。Kotlin支持在包范围内对已存在的类进行方法和属性扩展。

我们以Android最常用的showToast方法举个扩展方法的例子,以lastIndex属性举个扩展属性的例子,如下。在这样强大的特性下,写代码都成了一种享受。

// 扩展方法
fun Context.showLongToast(msg: String) {
    Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
}
// 扩展属性
val <T> ArrayList<T>.lastIndex: Int get() = size -1

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        showLongToast("hello") // 给Context类扩展了showToast方法,可以在任何有context的地方直接调用了

        var mList = ArrayList<String>()
        mList.lastIndex // 任何ArrayList均可以调用该属性
        ...
    }
    ...
}

这里需要注意两点:1.扩展需要在包级范围内进行,如果写在class内是无效的。2.已经存在的方法或属性,是无法被扩展的,依旧会调用已有的方法。

3.简洁数据类

相信大家都写过数据类,或者自动化生成的数据model,通常都是由多个属性和对应的getter、setter组成。当有大量多属性的数据类时,不仅这些类会因为大量的getter和setter方法而行数爆炸,也使整个工程方法数骤增。Kotlin中也做了这层特性优化,提供了数据类的简单实现。不用再写get和set方法,这些都由编译器背后去做,你得到的是一个清爽干净的数据类。具体使用参考下面的例子。

data class Student (
    var name: String,
    var age: Int,
    var hobby: String?,
    var university: String = "NJU"
)
fun printInfo() {
    var s: Student = Student("Ricky", 25, "playing Gwent")
    println("${s.name} is from ${s.university}, ${s.age} years old, and likes ${s.hobby}")
}

4.Anko

Anko是Jetbrains官方提供的一个让Kotlin开发更快速简单的类库,旨在使代码书写更加清晰易懂,形式上为DSL编程。

No findViewById

Android开发过程一定都写过大量的findViewById。这本身就是个消耗资源的方法,编码时还极为不便,需要强制转换为具体的View类型才能调用其方法。通过引入支持注解的库,可以使这个过程略微简单化一些。

// 传统Android中的View内容初始化
TextView tvName = (TextView) this.findViewById(R.id.tv_name);
tvName.setText("Ricky");
// 注解方式
@BindView(R.id.tv_name)
TextView tvName;
tvName.setText("Ricky");

这样还是显得不够简洁,而Kotlin给出了一种最为简便的方式。

import kotlinx.android.synthetic.main.activity_main.* // activity_main为具体的布局xml文件名
...
tvName.text = "Ricky";

你只需要在具体的页面中按照上面的格式import下,就可以在整个页面里很方便的使用xml里的view操作了。不需要类型转换,不需要新建变量,不需要findViewById,孰优孰劣,相信大家心中都有了答案。

Simple startActivity

通常,在Android里,当我想打开一个新页面,并给它传递一些参数时,我需要按照如下的方式编码。

Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra("userid", 10001);
intent.putExtra("username", "ricky");
startActivity(intent);

而强大的Anko给我们提供了一种极为方便的写法。无论是无参还是有参,还是需要RequestCode,都非常简洁易懂,是不是一看就特别心动?

startActivity<MainActivity>()
startActivity<MainActivity>("userid" to 10001, "username" to "ricky")
startActivityForResult<MainActivity>(101, "userid" to 10001, "username" to "ricky")

DSL Layout

上面说到不再使用findViewById,还是基于使用xml来编写Android页面这个基础。倘若你想彻底革新写法,换一种更直观的DSL方式来写Android页面呢?Anko就提供了这样的方案。相比之下,除了可读性增加之外,也节约了解析xml文件消耗的资源。

inner class LoginAnkoUI : AnkoComponent<LoginActivity> {
    override fun createView(loginAnkoUI: AnkoContext<LoginActivity>): View {
        return with(loginAnkoUI) {
            verticalLayout {
                val textView = textView("用户名") {
                    textSize = sp(15).toFloat()
                    textColor = context.resources.getColor(R.color.black)
                }.lparams {
                    margin = dip(10)
                    height = dip(40)
                    width username= matchParent
                }
                val username = editText("输入...")
                button("登录") {
                    onClick { view ->
                        toast("Hello, ${username.text}!")
                    }
                }
            }
        }
    }
}

只要最后在Activity里加一句调用,便可以使用Anko写的页面了。是不是看上去更直观了呢?

LoginAnkoView().setContentView(this@LoginActivity)

results matching ""

    No results matching ""