Hello Kotlin
kotlin每一行代码的结尾不需要加分号。
编程之本
变量
关键词
语法规则
代码 | 原词 | 用法 |
---|---|---|
val | value | 声明一个不可变的变量,在初始赋值后不能在被重新赋值 |
var | variable | 声明一个可变的变量,在初始赋值后仍能在被重新赋值 |
代码实践
package com.example.hellokt
fun main(){
val a = 10
println("a = " + a)
}
数据类型
语法规则
Kotlin中使用了对象数据类型,是一个拥有自己的方法和继承结构的类。
数据类型 | 说明 |
---|---|
Int | 整型 |
Long | 长整型 |
Short | 短整型 |
Float | 单精度浮点型 |
Double | 双精度浮点型 |
Boolean | 布尔型 |
Char | 字符型 |
Byte | 字节型 |
代码实践
声明不可变整型变量
报错原因:声明a的关键字为val
变量,后续就不能再赋值。
声明可变整型变量
package com.example.hellokt
fun main(){
var a: Int = 10
a = a * 10
println("a = " + a)
}
函数
语法规则
//fun 函数名(参数1名: 参数类型, 参数2名: 参数类型): 函数返回值类型{}
fun methodName(param1: Int, param2: Int): Int{
return 0
}
- 参数的数量可以是任意多个
- 如果函数不需要返回任何数据,可以不写返回值类型
代码实践
返回较大数
package com.example.hellokt
import kotlin.math.max
fun main(){
val a = 37
val b = 40
val value = largerNumber(a,b)
println("larger number is " + value)
}
fun largerNumber(num1: Int, num2: Int): Int{
return max(num1,num2)
}
代码简化
如果函数中只有一行代码时,可以不必编写函数体,中间用等号链接就好
fun largerNumber1(num1: Int,num2: Int): Int = max(num1,num2)
Kotlin有出色的推导机制,因为max()
返回的是Int
值,所以通过等号与它相连的largerNumber2()
函数返回值也是Int
值,可以省略返回值类型
fun largerNumber2(num1: Int,num2: Int) = max(num1,num2)
程序的逻辑控制
if条件语句
语法规则
Kotlin中的if
相较与其他的编程语言有一个额外的功能:它可以有返回值!
返回值就是if
语句每一个条件中最后一行代码的返回值。
代码实践
返回较大数(基础版)
fun largerNumber(num1: Int, num2: Int): Int{
var value = 0
if (num1 > num2){
value = num1
} else {
value = num2
}
return value
}
返回较大数(简化版)
fun largerNumber1(num1: Int,num2: Int): Int {
var value = if (num1 > num2){
num1
} else {
num2
}
return value
}
返回较大数(精简版)
fun largerNumber2(num1: Int,num2: Int): Int {
return if (num1 > num2){
num1
} else {
num2
}
}
返回较大数(进一步精简版)
fun largerNumber3(num1: Int,num2: Int) = if (num1 > num2){
num1
} else {
num2
}
返回较大数(终极精简版)
fun largerNumber4(num1: Int,num2: Int) = if (num1 > num2) num1 else num2
when条件语句
精确匹配
语法规则
在需要很多判断条件时,可以使用when
语句
伪代码:
when (参数){
匹配值 -> {执行逻辑}
匹配值 -> {执行逻辑}
匹配值 -> {执行逻辑}
匹配值 -> {执行逻辑}
else -> {执行逻辑}
}
执行逻辑只有一行时,花括号可以省略
代码实践
类型匹配
语法规则
通过when
语句和is
关键词,对变量的类型进行判断
伪代码:
when (参数){
is 匹配类型 -> {执行逻辑}
is 匹配类型 -> {执行逻辑}
is 匹配类型 -> {执行逻辑}
else -> {执行逻辑}
}
执行逻辑只有一行时,花括号可以省略
代码实践
不带参用法
代码实践
字符串判断(补充)
语法规则
判断字符串和对象是否相等可以直接使用变量 == 关键字
字符串.startsWith(“xxx”)
,可以匹配所有以xxx
开头的字符串
代码实践
循环语句(for循环)
Kotlin中的 while
语法与java编程语言基本相同
双闭端区间
语法规则
val range = 0..10
创建一个双端闭区间,表示为
[
0
,
10
]
[0,10]
[0,10]
其中..
为关键字,在..
左右两边指定区间的左右端点
代码实践
左闭右开区间
语法规则
val range = 0 until 10
创建一个左闭右开区间,表示为
[
0
,
10
)
[0,10)
[0,10)
其中until
为关键字,在until
左右两边指定区间的左右端点
在for
循环中你还可以通过step
关键字,实现跳过一些元素的效果
代码实践
相当于for(int i = 0;i < 10;i += 2)
降序区间
语法规则
关键字a downTo b
,创建一个范围为
[
a
,
b
]
[a,b]
[a,b]的降序区间
代码实践
for-in循环
不仅可以用来遍历区间还可以遍历集合
面向对象编程
类和对象
语法规则
创建一个Person
类:
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
在main
函数中进行实例化
fun main(){
val p =Person()
p.name = "Jack"
p.age = 19
p.eat()
}
p
就是Person类
的一个实例,也可以成为一个对象
代码实践
继承
语法规则
父类:在类的前面加上open
关键字,则表示该类可以被继承
open class Person{...}
子类:继承的关键字为:
class Student : Person(){...}
代码实践
构造函数
主构造函数
语法规则
每一个类默认会有一个不带参数的主构造参数,当然也可以指明参数。
主构造参数的特点是没有函数体,直接定义在类名的后面即可。
class Student(val sno: String, val grade: Int) : Person(){
}
主构造函数中的逻辑语句写在init结构体
中
代码实践
子类中的构造函数必须调用父类中的构造函数
当父类中的构造函数有很多时可以在继承时通过父类后面的括号来指定
错误示范:
想要解决这个错误可以在Student类
的主构造函数中加上着它父类需要的参数,在将参数传递给父类
注意:在Student类的主构造函数中增加name和age这两个字段时, 不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数 将⾃动成为该类的字段,这就会导致和⽗类中同名的name和age字段造成 冲突。因此,这⾥的name和age参数前⾯我们不⽤加任何关键字,让它的作⽤域仅限定在主构造函数当中即可。
次构造函数
任何⼀个类只能有⼀个主构造函数,但是可以有多个次构造函数。次构造函数也可以⽤于实例化⼀个类,这⼀点和主构造函数没有什么 不同,只不过它是有函数体的。
Kotlin规定,当⼀个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调⽤主构造函数(包括间接调⽤)。
语法规则
关键字constructor
class Student(val sno: String,val grade: Int, name: String, age: Int) :
Person(name,age){
constructor(name: String, age: Int) : this("",0,name,age){}
constructor() : this("",0){}
}
- 第⼀个次构造函数接收
name
和age
参数,然后它⼜通过this
关键字调用了主构造函数,并将sno
和grade
这两个参数赋值成初始值; - 第⼆个次构造函数不接收任何参数,它通过this关键字调用了第⼀个次构造函数,并将
name
和age
参数也赋值成初始值。 - 第⼆个次构造函数属于间接调⽤主构造函数。
经过上面的代码现在有三种方式对Student
类进行实例化
val stu1 = Student()
val stu2 = Student("Jack", 19)
val stu3 = Student("a123", 5, "Jack", 19)
类中只有次构造函数时
因为Student类
没有主构造函数,继承Person类
的时候也就不需要再加上括号了
接口
语法规则
关键字 | 含义 |
---|---|
interface | 定义接口 |
override | 实现接口中的函数 |
: | 使用接口 |
, | 分割继承的父类和使用的接口 |
代码实现
Study接⼝
中定义了readBooks()
和doHomework()
这两个待实现函数, 因此Student类
必须实现这两个函数。doStudy()函数
接收⼀个Study
类型的参数, 由于Student类
实现了Study接⼝
,因此Student类
的实例是可以传递给doStudy()函数
的,接下来调用Study接⼝
的readBooks()
和doHomework()
函数,这种就叫作⾯向接⼝编程,也可以称为多态。
对接口中的函数进行默认实现
可见性修饰符
修饰符 | Kotlin中的含义 | Java中的含义 |
---|---|---|
public | 所有类可见(默认) | 所有类可见 |
private | 当前类可见 | 当前类可见 |
protected | 当前类、子类可见 | 当前类、子类、同一包路径下的类可见 |
default | 无 | 同一包路径下的类可见(默认) |
internal | 同一模块中的类可见 | 无 |
数据类
在⼀个规范的系统架构中,数据类通常占据着非常重要的角色,用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。
通常需要重写的方法
方法名 | 用途 |
---|---|
equals() | 判断两个数据类是否相等 |
hashCode() | equals() 的配套⽅法,也需要⼀起重写,否则会导致HashMap 、HashSet 等hash相关的系统类 ⽆法正常⼯作 |
toString | 提供更清晰的输入日志,否则⼀个数据类默认打印出来的就是一行内存地址。 |
代码对比
构建⼀个手机数据类,只有品牌和价格两个字段。
Java代码
public class CellphoneJ {
String brand;
double price;
public CellphoneJ(String brand, double price) {
this.brand = brand;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CellphoneJ) {
CellphoneJ other = (CellphoneJ) obj;
return other.brand.equals(brand) && other.price == price;
}
return false;
}
@Override
public int hashCode() {
return brand.hashCode() + (int) price;
}
@Override
public String toString() {
return "CellphoneJ(brand=" + brand + ", price=" + price + ")";
}
}
Kotlin代码
data class Cellphone(val brand: String, val price: Double)
在java
中大段的代码用kotlin
实现只需要一行
当在⼀个类前面声明了data
关键字时,就表明这个类是⼀个数据类,Kotlin会根据主构造函数中的参数将equals()
、hashCode()
、toString()
等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。
代码实现
补充:尝试了一下书中说的删除data
关键字,得到了截然不同的结果
单例类
单例模式是最常用、最基础的设计模式之⼀,它可以用于避免创建重复的对象。比如我们希望某个类在全局最多只能拥有⼀个实例,此时就可以使用单例模式。
代码对比
Java代码
public class SingletonJ {
private static SingletonJ instance;
private SingletonJ() {}
public synchronized static SingletonJ getInstance() {
if (instance == null) {
instance = new SingletonJ();
}
return instance;
}
public void singletonJTest() {
System.out.println("singletonJTest is called.");
}
}
在调用上述代码时:
SingletonJ singletonJ = SingletonJ.getInstance();
singletonJ.singletonTest();
Kotlin代码
object Singleton {
}
此时Singleton
就已经是⼀个单例类了
可以在其中直接编写需要的函数:
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}
调用方法:
Singleton.singletonTest()
Lambda编程
集合的创建与遍历
List
初始化方法1(基础)
val list = ArrayList<String>()
list.add("Apple")
list.add("Banana")
list.add("Orange")
list.add("Pear")
list.add("Grape")
初始化方法2(不可变)
val list = listOf("Apple","Banana","Orange","Pear","Grape")
初始化方法3(可变)
val list = mutableListOf("Apple","Banana","Orange","Pear","Grape")
list.add("Watermelon")
遍历集合
Set
Set
中不可以存放重复元素,如果存放了多个相同的元素,只会保留其中的一份。
用法与List
基本相同,只是创建集合的方式变成了setOf()
和mutableSetOf()
Map
Map
是⼀种键值对形式的数据结构
初始化方法1(传统)
传统的Map
⽤法是先创建⼀个HashMap
的实例,然后将⼀个个键值对
数据添加到Map
中。
val map = HashMap<String, Int>()
map.put("Apple", 1)
map.put("Banana", 2)
map.put("Orange", 3)
map.put("Pear", 4)
map.put("Grape", 5)
初始化方法2(“数组下标”)
val map = HashMap<String, Int>()
map["Apple"] = 1
map["Banana"] = 2
map["Orange"] = 3
map["Pear"] = 4
map["Grape"] = 5
在读取时:
val num = map["Apple"]
初始化方法3(不可变)
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
初始化方法4(可变)
val map = mutableMapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4)
遍历集合
集合的函数式API
不知是因为版本问题还是其他什么原因,这一小节的代码我在尝试的时候会报错,且暂时没有找到解决的方法,所以决定暂时跳过,预计于2021.11.21前解决问题并将这一段补上。不好意思
暂时在网上搜寻无果,如果有大佬看到这篇文章,且能根据报错信息提出修改建议,本人感激不尽
Java函数式API的使用
API的英文全程为:Application Programming Interface,中文译为应用程序编程接口。所以,API就是指接口。
所谓函数式接口,就是指那些只定义了一个待实现的抽象函数的接口。
在Kotlin
中调⽤Java方法
时也可以使⽤函数式API
,只不过这是有⼀定条件限制的。具体来讲,如果我们在Kotlin代码中调用了⼀个Java方法,并且该方法接收⼀个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有⼀个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。
示例1:Runnable接口
使用匿名类的写法,创建⼀个Runnable接口
的匿名类实例,并将它传给了Thread类
的构造方法,最后调用Thread类
的start()方法
执行这个线程。
Thread(object : Runnable{
override fun run() {
println("Thread is running")
}
}).start()
因为Runnable类
中只有⼀个待实现方法,即使这里没有显式地重写run()
⽅法,Kotlin
也能自动明白Runnable
后⾯的Lambda表达式就是要在run()方法
中实现的内容。所以就可以精简代码:
Thread(Runnable {
println("Thread is running")
}).start()
如果⼀个Java⽅法
的参数列表中有且仅有⼀个Java单抽象方法接口参数
,我们还可以将接口名进行省略。当Lambda表达式是方法的最后⼀个参数时,可以将Lambda表达式移到方法括号的外面。同时,如果Lambda表达式还是方法的唯⼀⼀个参数,还可以将方法的括号省略,最终简化结果如下:
Thread { println("Thread is running") }.start()
三段代码最终可以得到同样的运行结果:
示例1:OnClickListener接口
Kotlin代码:
button.setOnClickListener{
}
空指针检查
空指针是⼀种不受编程语⾔检查的运行时异常,只能由程序员主动通过逻辑判断来避免,但即使是最出色的程序员,也不可能将所有潜在的空指针异常全部考虑到。
可空类型系统
Kotlin利用编译时判空检查的机制几乎杜绝了空指针异常,将空指针异常的检查提前到了编译时期。
语法规则
可为空的类型系统就是在类名的后面加上⼀个问号?
。
Int
表示不可为空的整型,⽽Int?
就表示可为空的整型;
String
表示不可为空的字符串,⽽String?
就表示可为空的字符串。
代码实践
由于我们将参数改成了可为空的Study?
类型,此时调用参数的readBooks()
和doHomework()
方法都可能造成空指针异常,因此Kotlin在这种情况下不允许编译通过。
修改方法:
判空辅助工具
?.
语法规则
操作符:?.
当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。
代码实践
?:
语法规则
操作符:?:
操作符的左右两边都接收⼀个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。
代码实践
val c = a ?: b
相当于
val c = if (a ! = null) {
a
} else {
b
}
综合实践
改写后:
fun getTextLength1(text: String?) = text?.length ?: 0
非空断言工具
报错原因:printUpperCase()
函数并不知道外部已经对content
变量进行了非空检查,在调用toUpperCase()
方法时,还认为这里存在空指针风险,从而无法编译通过。
如果想要强行通过编译,可以使用非空断言工具,对象的后面加上!!
注意:使用这个工具要慎重,一定要确信它不会存在空指针异常!!!
let
语法规则
let
是⼀个函数,提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。
obj.let { obj2 ->
//编写具体的业务逻辑
}
代码实践
?.
操作符表示对象为空时什么都不做,不为空时就调用let
函数,而let
函数会将study
对象本身作为参数传递到Lambda表达式中,此时的study
对象肯定不为空了,就能放心地调用它的任意方法
精简代码:
当Lambda表达式的参数列表中只有⼀个参数时,可以不用声明参数名,直接使用it
关键字来代替即 可
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
Kotlin中的小魔术
字符串内嵌表达式
Kotlin允许在字符串⾥嵌⼊${}
这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容。
表达式中仅有一个变量的时候,可以将两边的大括号省略
函数的参数默认值
在定义函数的时候给任意参数设定⼀个默认值,这样当调⽤此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。
理想情况
不太理想情况
这时就需要用到键值对的方式传参
键值对的前后顺序无所谓!