gitbook/朱涛 · Kotlin编程第一课/docs/472154.md
2022-09-03 22:05:03 +08:00

27 KiB
Raw Permalink Blame History

01 | Kotlin基础语法正式开启学习之旅

你好我是朱涛。从今天开始我们就正式踏上Kotlin语言学习与实践的旅途了。这节课我想先带你来学习下Kotlin的基础语法包括变量、基础类型、函数和流程控制。这些基础语法是程序最基本的元素。

不过如果你有使用Java的经验可能会觉得今天的内容有点多余毕竟Kotlin和Java的基础语法是比较相似的它们都是基于JVM的语言。但其实不然Kotlin作为一门新的语言它包含了许多新的特性由此也决定着Kotlin的代码风格。如果你不够了解Kotlin的这些新特性你会发现自己只是换了种方式在写Java而已。

并且在具备Java语言的知识基础上这节课的内容也可以帮你快速将已有的经验迁移过来。这样的话针对相似的语法你可以直接建立Kotlin与Java的对应关系进而加深理解。当然即使你没有其他编程经验也没关系从头学即可Kotlin的语法足够简洁也非常适合作为第一门计算机语言来学习。

在课程中我会用最通俗易懂的语言来给你解释Kotlin的基础知识并且会结合一些Java和Kotlin的代码案例来帮助你直观地体会两种语言的异同点。而针对新的语法我也会详细解释它存在的意义以及都填补了Java的哪些短板让你可以对Kotlin新语法的使用场景做到心中基本有数。

开发环境

在正式开始学习基础语法之前我们还需要配置一下Kotlin语言的环境因为直接从代码开始学能给我们带来最直观的体验。

那么要运行Kotlin代码最快的方式就是使用Kotlin官方的PlayGround。通过这个在线工具我们可以非常方便地运行Kotlin代码片段。当然这种方式用来临时测试一小段代码是没有问题的但对于复杂的工程就有些力不从心了。

另一种方式,也是我个人比较推荐的方式,那就是安装IntelliJ IDEA。它是Kotlin官方提供的集成开发工具也是世界上最好的IDE之一如果你用过Android Studio你一定会对它很熟悉因为Android Studio就是由IntelliJ IDEA改造的。

如果你的电脑没有Java环境在安装完最新版的IntelliJ IDEA以后通过“File -> Project Structure -> SDKs”然后点击“加号按钮”就可以选择第三方提供的OpenJDK 1.8版本进行下载了。

图片

当然,这里我更推荐你可以自己手动从Oracle官网下载JDK 1.6、1.7、1.8、11这几个版本然后再安装、配置Java多版本环境。这在实际工作中也是必备的。

需要注意的是IntelliJ IDEA分为Ultimate付费版和Community免费版对于我们的Kotlin学习来说免费版完全够用。

这样在配置好了开发环境之后我们就可以试着一边敲代码一边体会、思考和学习Kotlin语言中这些最基础的语法知识了。那么下面我们就来看下在Kotlin语言中是如何定义变量的吧。

变量

在Java/C当中如果我们要声明变量我们必须要声明它的类型后面跟着变量的名称和对应的值然后以分号结尾。就像这样

Integer price = 100;

而Kotlin则不一样我们要使用“val”或者是“var”这样的关键字作为开头,后面跟“变量名称”,接着是“变量类型”和“赋值语句”,最后是分号结尾。就像这样:

/*
关键字     变量类型
 ↓          ↓           */
var price: Int = 100;   /*
     ↑            ↑
   变量名        变量值   */

不过像Java那样每写一行代码就写一个分号其实也挺麻烦的。所以为了省事在Kotlin里面我们一般会把代码末尾的分号省略就像这样

var price: Int = 100

另外由于Kotlin支持类型推导,大部分情况下,我们的变量类型可以省略不写,就像这样:

var price = 100 // 默认推导类型为: Int

还有一点我们要注意就是在Kotlin当中我们应该尽可能避免使用var尽可能多地去使用val

var price = 100
price = 101

val i = 0
i = 1 // 编译器报错

原因其实很简单:

  • val声明的变量我们叫做不可变变量它的值在初始化以后就无法再次被修改它相当于Java里面的final变量。
  • var声明的变量我们叫做可变变量它对应Java里的普通变量。

基础类型

了解了变量类型如何声明之后我们再来看下Kotlin中的基础类型。

基础类型,包括我们常见的数字类型、布尔类型、字符类型,以及前面这些类型组成的数组。这些类型是我们经常会遇到的概念,因此我们把它统一归为“基础类型”。

一切都是对象

在Java里面基础类型分为原始类型Primitive Types和包装类型Wrapper Type。比如整型会有对应的int和Integer前者是原始类型后者是包装类型。

int i = 0; // 原始类型
Integer j = 1; // 包装类型

Java之所以要这样做是因为原始类型的开销小、性能高但它不是对象无法很好地融入到面向对象的系统中。而包装类型的开销大、性能相对较差但它是对象可以很好地发挥面向对象的特性。在 JDK源码当中我们可以看到Integer作为包装类型它是有成员变量以及成员方法的这就是它作为对象的优势。

然而在Kotlin语言体系当中是没有原始类型这个概念的。这也就意味着在Kotlin里一切都是对象。

实际上从某种程度上讲Java的类型系统并不是完全面向对象的因为它存在原始类型而原始类型并不属于对象。而Kotlin则不一样它从语言设计的层面上就规避了这个问题类型系统则是完全面向对象的。

我们看一段代码来更直观地感受Kotlin的独特之处

val i: Double = 1.toDouble()

可以发现由于在Kotlin中整型数字“1”被看作是对象了所以我们可以调用它的成员方法toDouble()而这样的代码在Java中是无法实现的。

空安全

既然Kotlin中的一切都是对象那么对象就有可能为空。也许你会想到写这样的代码

val i: Double = null // 编译器报错

可事实上以上的代码并不能通过Kotlin编译。这是因为Kotlin强制要求开发者在定义变量的时候指定这个变量是否可能为null。对于可能为null的变量我们需要在声明的时候在变量类型后面加一个问号“?”:

val i: Double = null // 编译器报错
val j: Double? = null // 编译通过

并且由于Kotlin对可能为空的变量类型做了强制区分这就意味着“可能为空的变量”无法直接赋值给“不可为空的变量”当然反向赋值是没有问题的。

var i: Double = 1.0
var j: Double? = null

i = j  // 编译器报错
j = i  // 编译通过

Kotlin这么设计的原因也很简单如果我们将“可能为空的变量”直接赋值给了“不可为空的变量”这会跟它自身的定义产生冲突。而如果我们实在有这样的需求也不难实现只要做个判断即可

var i: Double = 1.0
val j: Double? = null

if (j != null) {
    i = j  // 编译通过
}

在了解了Kotlin和Java这两种语言的主要区别后下面就让我们来全面认识下Kotlin的基础类型。

数字类型

首先在数字类型上Kotlin和Java几乎是一致的包括它们对数字“字面量”的定义方式。

val int = 1
val long = 1234567L
val double = 13.14
val float = 13.14F
val hexadecimal = 0xAF
val binary = 0b01010101

这里我也来给你具体介绍下:

  • 整数默认会被推导为“Int”类型
  • Long类型我们则需要使用“L”后缀
  • 小数默认会被推导为“Double”我们不需要使用“D”后缀
  • Float类型我们需要使用“F”后缀
  • 使用“0x”来代表十六进制字面量
  • 使用“0b”来代表二进制字面量。

但是对于数字类型的转换Kotlin与Java的转换行为是不一样的。Java可以隐式转换数字类型而Kotlin更推崇显式转换。

举个简单的例子在Java和C当中我们经常直接把int类型赋值给long类型编译器会自动为我们做类型转换如下所示

int i = 100;
long j = i;

这段代码按照Java的编程思维方式来看的确好像是OK的。但是你要注意虽然Java编译器不会报错可它仍然可能会带来问题因为它们本质上不是一个类型int、long、float、double这些类型之间的互相转换是存在精度问题的。尤其是当这样的代码掺杂在复杂的逻辑中时在碰到一些边界条件的情况下即使出现了Bug也不容易排查出来。

所以同样的代码在Kotlin当中是行不通的

val i = 100
val j: Long = i // 编译器报错

在Kotlin里这样的隐式转换被抛弃了。正确的做法应该是显式调用Int类型的toLong()函数:

val i = 100
val j: Long = i.toLong() // 编译通过

其实如果我们仔细翻看Kotlin的源代码会发现更多类似的函数比如toByte()、toShort()、toInt()、toLong()、toFloat()、toDouble()、toChar()等等。Kotlin这样设计的优势也是显而易见的我们代码的可读性更强了,将来也更容易维护了

布尔类型

然后我们再来了解下Kotlin中布尔类型的变量它只有两种值分别是truefalse。布尔类型支持一些逻辑操作,比如说:

  • “&”代表“与运算”;
  • “|”代表“或运算”;
  • “!”代表“非运算”;
  • “&&”和“||”分别代表它们对应的“短路逻辑运算”。
val i = 1
val j = 2
val k = 3

val isTrue: Boolean = i < j && j < k

字符Char

Char用于代表单个的字符比如'A''B''C',字符应该用单引号括起来。

val c: Char = 'A'

如果你有Java或C的使用经验也许会写出这样的代码

val c: Char = 'A'
val i: Int = c // 编译器报错

这个问题其实跟前面Java的数字类型隐式转换的问题类似所以针对这种情况我们应该调用对应的函数来做类型转换。这一点我们一定要牢记在心。

val c: Char = 'A'
val i: Int = c.toInt() // 编译通过

字符串String

字符串String顾名思义就是一连串的字符。和Java一样Kotlin中的字符串也是不可变的。在大部分情况下我们会使用双引号来表示字符串的字面量这一点跟Java也是一样的。

val s = "Hello Kotlin!"

不过与此同时Kotlin还为我们提供了非常简洁的字符串模板

val name = "Kotlin"
print("Hello $name!")
/*            ↑
    直接在字符串中访问变量
*/
// 输出结果:
Hello Kotlin!

这样的特性在Java当中是没有的这是Kotlin提供的新特性。虽然说这个字符串模板功能我们用Java也同样可以实现但它远没有Kotlin这么简洁。在Java当中我们必须使用两个“+”进行拼接,比如说("Hello" + name + "!")。这样一来,在字符串格式更复杂的情况下,代码就会很臃肿。

当然,如果我们需要在字符串当中引用更加复杂的变量,则需要使用花括号将变量括起来:

val array = arrayOf("Java", "Kotlin")
print("Hello ${array.get(1)}!")
/*            ↑
      复杂的变量,使用${}
*/
// 输出结果:
Hello Kotlin!

另外Kotlin还新增了一个原始字符串是用三个引号来表示的。它可以用于存放复杂的多行文本并且它定义的时候是什么格式最终打印也会是对应的格式。所以当我们需要复杂文本的时候就不需要像Java那样写一堆的加号和换行符了。

val s = """
       当我们的字符串有复杂的格式时
       原始字符串非常的方便
       因为它可以做到所见即所得。 """

print(s)

数组

最后我们再来看看Kotlin中数组的一些改变。

在Kotlin当中我们一般会使用**arrayOf()**来创建数组括号当中可以用于传递数组元素进行初始化同时Kotlin编译器也会根据传入的参数进行类型推导。

val arrayInt = arrayOf(1, 2, 3)
val arrayString = arrayOf("apple", "pear")

比如说针对这里的arrayInt由于我们赋值的时候传入了整数所以它的类型会被推导为整型数组对于arrayString它的类型会被推导为字符串数组。

而你应该也知道在Java当中数组和其他集合的操作是不一样的。举个例子如果要获取数组的长度Java中应该使用“array.length”但如果是获取List的大小那么Java中则应该使用“list.size”。这主要是因为数组不属于Java集合。

不过Kotlin在这个问题的处理上并不一样。虽然Kotlin的数组仍然不属于集合但它的一些操作是跟集合统一的。

val array = arrayOf("apple", "pear")
println("Size is ${array.size}")
println("First element is ${array[0]}")

// 输出结果:
Size is 2
First element is apple

就比如说以上代码中我们直接使用array.size就能拿到数组的长度。

函数声明

了解了Kotlin中变量和基础类型的相关概念之后我们再来看看它的函数是如何定义的。

在Kotlin当中函数的声明与Java不太一样让我们看一段简单的Kotlin代码

/*
关键字    函数名          参数类型   返回值类型
 ↓        ↓                ↓       ↓      */
fun helloFunction(name: String): String {
    return "Hello $name !"
}/*   ↑
   花括号内为:函数体
*/

可以看到,在这段代码中:

  • 使用了fun关键字来定义函数;
  • 函数名称,使用的是驼峰命名法(大部分情况下);
  • 函数参数,是以(name: String)这样的形式传递的这代表了参数类型为String类型
  • 返回值类型,紧跟在参数的后面;
  • 最后是花括号内的函数体,它代表了整个函数的逻辑。

另外你可以再注意一个地方前面代码中的helloFunction函数它的函数体实际上只有一行代码。那么针对这种情况我们其实就可以省略函数体的花括号直接使用“=”来连接,将其变成一种类似变量赋值的函数形式:

fun helloFunction(name: String): String = "Hello $name !"

这种写法,我们称之为单一表达式函数。需要注意的是在这种情况下表达式当中的“return”是需要去掉的。

另外由于Kotlin支持类型推导我们在使用单一表达式形式的时候返回值的类型也可以省略

fun helloFunction(name: String) = "Hello $name !"

看到这里你一定能体会到Kotlin的魅力。它的语法非常得简洁并且是符合人类的阅读直觉的我们读这样的代码就跟读自然语言一样轻松。

然而Kotlin的优势不仅仅体现在函数声明上在函数调用的地方它也有很多独到之处。

函数调用

以我们前面定义的函数为例子如果我们想要调用它代码的风格和Java基本一致

helloFunction("Kotlin")

不过Kotlin提供了一些新的特性那就是命名参数。简单理解,就是它允许我们在调用函数的时候传入“形参的名字”。

helloFunction(name = "Kotlin")

让我们看一个更具体的使用场景:

fun createUser(
    name: String,
    age: Int,
    gender: Int,
    friendCount: Int,
    feedCount: Int,
    likeCount: Long,
    commentCount: Int
) {
    //..
}

这是一个包含了很多参数的函数在Kotlin当中针对参数较多的函数我们一般会以纵向的方式排列,这样的代码更符合我们从上到下的阅读习惯,省去从左往右翻的麻烦。

但是如果我们像Java那样调用createUser代码就会非常难以阅读

createUser("Tom", 30, 1, 78, 2093, 10937, 3285)

这里代码中的第一个参数我们知道肯定是name但是到了后面那一堆的数字就会让人迷惑了。这样的代码不仅难懂同时还不好维护。

但如果我们这样写呢?

createUser(
    name = "Tom",
    age = 30,
    gender = 1,
    friendCount = 78,
    feedCount = 2093,
    likeCount = 10937,
    commentCount = 3285
)

可以看到,在这段代码中,我们把函数的形参加了进来,形参和实参用“=”连接建立了两者的对应关系。对比前面Java风格的写法这样的代码可读性更强了。如果将来你想修改likeCount这个参数也可以轻松做到。这其实就体现出了Kotlin命名参数的可读性易维护性两个优势。

而除了命名参数这个特性Kotlin还支持参数默认值,这个特性在参数较多的情况下同样有很大的优势:

fun createUser(
    name: String,
    age: Int,
    gender: Int = 1,
    friendCount: Int = 0,
    feedCount: Int = 0,
    likeCount: Long = 0L,
    commentCount: Int = 0
) {
    //..
}

我们可以看到gender、friendCount、feedCount、likeCount、commentCount这几个参数都被赋予了默认值。这样做的好处就在于我们在调用的时候可以省很多事情。比如说下面这段代码就只需要传3个参数剩余的4个参数没有传但是Kotlin编译器会自动帮我们填上默认值。

createUser(
    name = "Tom",
    age = 30,
    commentCount = 3285
)

对于无默认值的参数编译器会强制要求我们在调用处传参对于有默认值的参数则可传可不传。Kotlin这样的特性在一些场景下就可以极大地提升我们的开发效率。

而如果是在Java当中要实现类似的事情我们就必须手动定义“3个参数的createUser函数”或者是使用Builder设计模式。

流程控制

在Kotlin当中流程控制主要有if、when、for、 while这些语句可以控制代码的执行流程。它们也是体现代码逻辑的关键。下面我们就来一一学习下。

if

if语句在程序当中主要是用于逻辑判断。Kotlin当中的if与Java当中的基本一致

val i = 1
if (i > 0) {
    print("Big")
} else {
    print("Small")
}

输出结果:
Big

可以看到由于i大于0所以程序会输出“Big”这很好理解。不过Kotlin的if并不是程序语句Statement那么简单它还可以作为表达式Expression来使用。

val i = 1
val message = if (i > 0) "Big" else "Small"

print(message)

输出结果:
Big

以上的代码其实跟之前的代码差不多它们做的是同一件事。不同的是我们把if当作表达式在用将if判断的结果赋值给了一个变量。同时Kotlin编译会根据if表达式的结果自动推导出变量“message”的类型为“String”。这种方式就使得Kotlin的代码更加简洁。

而类似的逻辑如果要用Java来实现的话我们就必须先在if外面定义一个变量message然后分别在两个分支内对message赋值

int i = 1
String message = ""
if (i > 0) {
    message = "Big"
} else {
    message = "Small"
}

print(message)

这样两相对比下我们会发现Java的实现方式明显丑陋一些不仅代码行数更多,逻辑也松散了

另外由于Kotlin当中明确规定了类型分为“可空类型”“不可空类型”因此我们会经常遇到可空的变量并且要判断它们是否为空。我们直接来看个例子

fun getLength(text: String?): Int {
  return if (text != null) text.length else 0
}

在这个例子当中我们把if当作表达式如果text不为空我们就算出它的长度如果它为空长度就取0。

但是如果你实际使用Kotlin写过代码你会发现在Kotlin中类似这样的判断逻辑出现得非常频繁如果每次都要写一个完整的if else分支其实也很麻烦。

为此Kotlin针对这种情况就提供了一种简写叫做Elvis表达式

fun getLength(text: String?): Int {
  return text?.length ?: 0
}

可以看到通过Elvis表达式我们就再也不必写“if (xxx != null) xxx else xxx”这样的赋值代码了。它在提高代码可读性的同时,还能提高我们的编码效率。

when

when语句在程序当中主要也是用于逻辑判断的。当我们的代码逻辑只有两个分支的时候我们一般会使用if/else而在大于两个逻辑分支的情况下我们使用when。

val i: Int = 1

when(i) {
    1 -> print("一")
    2 -> print("二")
    else -> print("i 不是一也不是二")
}

输出结果:
一

when语句有点像Java里的switch case语句不过Kotlin的when更加强大它同时也可以作为表达式,为变量赋值,如下所示:

val i: Int = 1

val message = when(i) {
    1 -> "一"
    2 -> "二"
    else -> "i 不是一也不是二" // 如果去掉这行,会报错
}

print(message)

另外与switch不一样的是when表达式要求它里面的逻辑分支必须是完整的。举个例子以上的代码如果去掉else分支编译器将报错原因是i的值不仅仅只有1和2这两个分支并没有覆盖所有的情况所以会报错。

循环迭代while与for

首先while循环我们一般是用于重复执行某些代码它在使用上和Java也没有什么区别

var i = 0
while (i <= 2) {
    println(i)
    i++
}

var j = 0
do {
    println(j)
    j++
} while (j <= 2)

输出结果:
0
1
2
0
1
2

但是对于for语句Kotlin和Java的用法就明显不一样了。

在Java当中for也会经常被用于循环经常被用来替代while。不过**Kotlin的for语句更多的是用于“迭代”。**比如以下代码就代表了迭代array这个数组里的所有元素程序会依次打印出“1、2、3”。

val array = arrayOf(1, 2, 3)
for (i in array) {
    println(i)
}

而除了迭代数组和集合以外Kotlin还支持迭代一个“区间”。

首先,要定义一个区间,我们可以使用“..”来连接数值区间的两端,比如“1..3”就代表从1到3的闭区间左闭右闭

val oneToThree = 1..3 // 代表 [1, 3]

接着我们就可以使用for语句来对这个闭区间范围进行迭代

for (i in oneToThree) {
    println(i)
}

输出结果:
1
2
3

甚至,我们还可以逆序迭代一个区间,比如:

for (i in 6 downTo 0 step 2) {
    println(i)
}

输出结果:
6
4
2
0

以上代码的含义就是逆序迭代一个区间从6到0每次迭代的步长是2这意味着6迭代过后到4、2最后到0。需要特别注意的是,逆序区间我们不能使用“6..0”来定义,如果用这样的方式来定义的话,代码将无法正常运行。

好了那么到目前为止Kotlin的变量、基础类型、函数、流程控制我们就都已经介绍完了。掌握好这些知识点我们就已经可以写出简单的程序了。当然我们的Kotlin学习之路才刚刚开始在下节课我会带你来学习Kotlin面向对象相关的知识点。

小结

学完了这节课现在我们知道虽然Kotlin和Java的语法很像但在一些细节之处Kotlin总会有一些新的东西。如果你仔细琢磨这些不同点你会发现它正是大部分程序员所需要的。举个例子作为开发者我们都讨厌写冗余的代码喜欢简洁易懂的代码。那么在今天学完了基础语法之后我们可以来看看Kotlin在这方面都做了哪些改进

  • 支持类型推导;
  • 代码末尾不需要分号;
  • 字符串模板;
  • 原始字符串,支持复杂文本格式;
  • 单一表达式函数,简洁且符合直觉;
  • 函数参数支持默认值替代Builder模式的同时可读性还很强
  • if和when可以作为表达式。

同时JetBrains也非常清楚开发者在什么情况下容易出错所以它在语言层面也做了很多改进

  • 强制区分“可为空变量类型”和“不可为空变量类型”,规避空指针异常;
  • 推崇不可变性val对于没有修改需求的变量IDE会智能提示开发者将“var”改为“val”
  • 基础类型不支持隐式类型转换,这能避免很多隐藏的问题;
  • 数组访问行为与集合统一不会出现array.length、list.size这种恼人的情况
  • 函数调用支持命名参数,提高可读性,在后续维护代码的时候不易出错;
  • when表达式强制要求逻辑分支完整让你写出来的逻辑永远不会有漏洞。

图片

这些都是Kotlin的闪光点,也是它最珍贵的地方。

这一切都得益于Kotlin的发明者JetBrains。作为最负盛名的IDE创造者JetBrains能深刻捕捉到开发者的需求。它知道开发者喜欢什么、讨厌什么它甚至知道开发者容易犯什么样的错误从而在语言设计的层面规避错误。站在这个角度看JetBrains能够创造出炙手可热的Kotlin语言就一点都不奇怪了。

以上这么多的“闪光点”还仅仅只是局限于我们这节课的内容,如果放眼全局,这样的例子更是数不胜数。**Kotlin对比Java的提升如果独立去看其中的某一个点都不足以让一个开发者心动。不过一旦这样的改善积少成多Kotlin的优势就会显得尤为明显。**这也是很多程序员表示“Kotlin用过了就回不去”的原因。

思考题

虽然Kotlin在语法层面摒弃了“原始类型”但有时候为了性能考虑我们确实需要用“原始类型”。这时候我们应该怎么办

欢迎在评论区分享你的思路,这个问题我会在第三节课给出答案,我们下节课再见。