结构器

主结构器

咱们之前现已了解了Kotlin中constructor的写法:

class User {
    var name: String
    constructor(name: String) {
        this.name = name
    }
}

其实Kotlin中还有更简略的写法来写结构器:

class User constructor(name: String) {
    //                   这儿与结构器中的name是同一个
    var name: String = name
}

这儿有几处不同点:

  • constructor结构器移到了类名之后
  • 类的特点name能够引证结构器中的参数name

这个写法叫「主结构器primary constructor」。与之相对的在第二篇,写在类中的结构器被称为「次结构器」。在Kotlin中一个类最多只能有一个主结构器(也能够没有),而此结构器是没有个数限制。

主结构器中的参数除了能够在类的特点中运用过,还能够再init代码块中运用:

class User constructor(name: String) {
    var name: String
    init {
        this.name = name
    }
}

其间init代码块是紧跟在主结构器之后履行的,这是由于主结构器本身没有代码体,init代码块就充当了主结构器代码体的功用。

别的,假如类中有主结构器,那么其他的次结构器都需求经过this关键字调用该主结构器,能够直接调用或许经过其他次结构器直接调用。假如不调用IDE就会报错:

class User constructor(var name: String) {
    constructor(name: String, id: Int) {
    //这样写会报错,Primary constructor call expected
    }
}

为什么当类中有主结构器的时分就强制要求次结构器调用主结构器呢?

  • 有必要性:创立类的目标时,不论运用那个结构器,都需求主结构器的参与
  • 第一性:在类的初始化进程中,首要滞后性的便是主结构器

这也便是主结构器的命名由来。
当一个类中一同有主结构器与次结构器的时分,需求这样写:

class User constructor(var name: String) {
                                      //  直接调用主结构器
    constructor(name: String, id: Int) : this(name) {
    }
                                                 // 经过上一个次结构器,直接调用主结构器
    constructor(name: String, id: Int, age: Int) : this(name, id) {
    }
}

在运用次结构器创立目标时,init代码块是先于次结构器履行的。假如把主结构器当作身体的头部,那么init代码块便是颈部,次结构器就相当于身体其余部分。

细心的你或许会发现这儿又呈现:符号,它还有其他场合呈现过,例如:

  • 变量的声明: var id: Int
  • 类的继承: class MainActivity: AppCompatActivity() {}
  • 接口的完成:class User: Impl{}
  • 匿名类的创立:object: ViewPager.SimpleOnPageChangeListener() {}
  • 函数的回来值:fun sum(a: Int, b: Int): Int

能够看出:符号在Kotlin中十分高频呈现,它其实表明了一种依靠关系,在这儿表明依靠于主结构器。

通常状况下,主结构器中的constructor关键字能够省掉:

class User(name: String) {
     var name: String = name
}

但有些场景,constructor是不能够省掉的,例如在主结构器上运用「可见性润饰符」或许「注解」:

  • 可见性润饰符咱们之前现已讲过,它润饰一般函数与润饰符结构器的用法是相同的,
class User private constructor(name: String) {
    // 主结构器被润饰符为私有的,外部就无法调用该结构器
}

已然主结构器能够简化类的初始化进程,那咱们就帮人帮到底,送佛送到西,用主结构器把特点的初始化也一同给简化了。

主结构器里声明特点

之前咱们讲了主结构器中的参数能够在特点中进行赋值,其实还能够在主结构器中直接声明特点:

class User(var name: String) {
}
//等价于:
class User(name: String) {
    var name: String = name
}

假如在主结构器的声明时加上var或许val,就等价于在类中创立了该称号的特点(property),并且初始值便是主结构器中该参数的值。
以上讲了一切关于主结构器相关的常识,让咱们总结一下的初始化写法:

  • 首要创立一个User类:
class User {
}
  • 添加一个参数为nameid的主结构器
class User (name: String, id: String) {
}
  • 将主结构器中的nameid声明为类的特点
class User(val name: String, val id: String) {
}
  • 然后在init代码块中添加一些初始化逻辑:
class Useer(val name: String, val id: String) {
    init {
        ...
    }
}
  • 最终再添加其他次结构器:
class User(val name: String, val id: String) {
    init {
        ...
    }
    constructor(person: Person) : this(person.name, person.id) {
    }
}

当一个类有多个结构器时,只需求把最根本、最通用的那个写成主结构器就行了。这儿咱们选择将参数为nameid的结构器作为主结构器。
到这儿,整个类的初始化就完成了,类的初始化次序就和上面的过程相同。
除了结构器,一般函数也是有许多简化写法的。

函数简化

运用 = 衔接回来值

咱们现已知道了Kotlin中的函数的写法:

fun area(width: Int, height: Int): Int {
    return widthh * heighht
}

其实,这种只要一行代码的函数,还能够这么写:

fun area(width: Int, height: Int): Int = width * height

{}return没有了,运用=符号衔接回来值。
咱们之前讲过,Kotlin有「类型推断」的特性,那么这儿函数的回来类型还能够隐藏掉:

//                                省掉了回来类型
fun area(width: Int, height: Int) = width * height

不过,在实际开发中,仍是推荐显式地讲回来类型写出来,增加代码可读性。
以上是函数有回来值的状况,关于没有回来值的状况,能够理解为回来值是Unit:

fun sayHi(name: String) {
    println("Hi " + name)
}

因此也能够简化成下面这样:

fun sayHi(name: String) = println("Hi " + name)

简化完函数体,咱们再来看看前面的参数部分。

关于Java中的办法重载,咱们都不生疏,那Kotlin中是否有更便利的重载办法呢?
接下来咱们看看Kotlin中的「参数默许值」的用法

参数默许值

Java中,答应在一个类中界说多个名字相同的办法,可是参数的类或个数有必要不同,这便是办法重载:

public void sayHi(String name) {
    System.out.println("Hi "+ name);
}
public void sayHi() {
    say("world");
}

在Kotlin中,也能够运用这样的办法进行函数重载,不过哦还有一种更简略的办法,那便是「参数默许值」:

fun sayHi(name: String = "world") = println("Hi " + name)

这儿world是参数name的默许值,当调用该函数时不穿参数,就会运用该默许值。

这就等价于上面Java写的重载办法,当调用sayHi函数时,参数是可选的:

sayHi("kaixue.io")
sayHi()//运用了默许值"world"

已然与重载函数的效果相同,那Kotlin中的参数默许值有什么好处呢?仅仅仅仅少写了一些代码吗?

其实在Java中,每个重载办法的内部完成能够各不相同,这就无法确保重载办法内部设计上的一致性,而Kotlin的参数默许值就处理了这个问题。

不过参数默许值在调用时也不是完全能够放飞自我的。

来看下面这段代码,这儿函数中有默许值的参数在无默许值参数的前面:

fun sayHi(name: String = "world", age:Int) {
    ...
}
sayHi(10)
//这时想运用默许值进行调用,IDE会报一下两个过错
// The integer literal does not conform to the expected type String
// No value passed for parameter 'age'

这个过错便是告知你参数不匹配,阐明咱们的「打开办法」不对,其实Kotlin里是经过「命名参数」来处理这个问题的。

命名参数

具体用法如下:

fun sayHi(name: String = "world", age: Int) {
    ...
}
sayHi(age = 21)

在调用函数时,显示地指定了参数age的称号,这便是「命名参数」。Kotlin中的每一个函数参数都能够作为命名参数。

再来看一个十分多参数的函数的比方:

fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
    ...
} 

当函数十分多的参数时,调用该函数就会写成这样

sayHi("world", 21, false, true, false)

当看到后边一长串的布尔值时,咱们很难分清楚每个参数的用途,可读性很差。经过命名参数,咱们就能够这么写:

sayHi(name = "wo", age = 21, isStudent = false, isFat = true, isTall = false)

与命名参数相对应的一个概念被称为「方位参数」,也便是按方位是次序进行参数填写。

当一个函数被调用时,假如混用方位参数与命名参数,那么一切的方位参数都应该放在第一个命名参数之前:

fun sayHi(name: String = "world", age: Int) {
    ...
}
sayHi(name = "wo", 21) //IDE会报错, Mixing named and positioned arguments is not allowed
sayHi("wo", age = 21)//这是正确的写法

本地函数(嵌套函数)

首要来看下这段代码,这是一个简略的登录的函数:

fun login(user: String ,password: String, illegalStr: String) {
    // 验证user是否为空
    if (user.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    //验证password是否为空
    if (password.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
}

该函数中,查看参数这个部分有些冗余,咱们又不想将这段逻辑作为一个独自的函数对外露出。这时能够运用嵌套函数,在login函数内部声明一个函数。

fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String, illegalStr: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(illegalStr)
        }
    }
    validate(user, illegalStr)
    validate(password, illegalStr)
}

这儿咱们将共同的验证逻辑放进了嵌套函数validate中,并且login函数之外的其他地方无法访问这个嵌套函数。

这儿的illegalStr是经过参数的办法传进嵌套函数中的,其实完全没有这个必要,由于嵌套函数中能够访问在它外部的一切常量和变量,例如类中的特点、当时函数中的参数与变量等。

咱们稍加改善:

fun login(user: String, password: String, illegalStr: String) {
    funvalidate(value: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(illegalStr)
        }
    }
}

这儿省去了嵌套函数中的illegalStr参数,在该嵌套函数内直接运用外层函数login的参数illegalStr

上面login函数中的验证逻辑,其实还有另一种更简略的办法:

fun login(user: String, password: String, illegalStr: String) {
    require(user.isNotEmpty()) { illegalStr }
    require(password.isNoEmpty()) { illegalStr }
}

其间用到了lambda表达式以及Kotlin内置的require函数。

字符串

字符串模板

在Java中,字符串与变量之间是运用+符号进行拼接的,Kotlin中也是如此:

val name = "world"
println("Hi "+ name)

可是当变量比较多的时分,可读性会变差,写起来也比较麻烦。

Java给出的处理办法是String.format

System.out.print(String.format("Hi %s", name));

Kotlin为咱们供给了一种愈加便利的写法:

val name = "world"
// 用'$'符号加参数的办法
println("Hi $name")

这种办法便是把name从后置改为前置,简化代码的一同增加了字符串的可读性。

除了变量,$后还能够跟表达式,但表达式是一个整体,所以咱们要用{}给它包起来:

val name = "world"
println("Hi ${name.length}")

其实就跟四则运算的括号相同,进步语法上的优先级,而单个变量的场景能够省掉{}

字符串模板还支持转义字符,比方运用转义字符\n进行换行操作:

val name = "world!\n"
println("Hi $name")//会多大一个空行

字符串模板的用法关于咱们Android工程师来说,其实一点都不生疏。

首要,Gradle所用的Groovy语言就现已有了这种支持:

 def name = "world"
 println "Hi ${name}"

在Android的资源文件中,界说字符串也有类似用法:

<string name="hi">Hi %s</string>
getString(R.id.hi, "world")

raw string(原生字符串)

有时分咱们不期望写过多的转义字符,这种Kotlin状况经过「原生字符串」来完成。

用法便是运用一堆"""将字符串括起来:

val name = "world"
val myName = "kotlin"
val text = """
    Hi $name!
  My name is $myName.\n
"""
println(text)

这儿有几个留意点:

  • \n并不会被转义
  • 最终输出的内容与写的内容完全一致,包含实际的换行
  • $符号引证变量依然生效

这便是「原神字符串」。输出成果如下:

    Hi world!
  My name is kotlin.\n

但对其办法看起来不太优雅,原生字符串还能够经过trimMargin()函数去除每行前面的空格:

val text = """
    |Hi world!
  |My name is kotlin.
""".trimMargin()
println(text)

输出的成果如下:

Hi world!
my name is kotlin.

这儿的trimMargin()函数有以下几个留意点:

  • |符号为默许的鸿沟前缀,前面只能有空格,不然不会生效
  • 输出的|符号以及它前面的空格都会被删除
  • 鸿沟前缀还能够运用其他字符,比方trimMargin("/"),只不过上方的代码生死用的参数默许值的调用办法。

数组和调集

数组与调集的操作符

在之前的文章中,咱们现已知道了数组和调集的根本概念,其实Kotlin中,还为咱们供给了许多使数组与调集操作起来愈加便利的函数。

首要声明如下IntArrayList

val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")

接下来,对它们的操作函数进行讲解:

  • forEach:遍历每一个元素
// lambda表达式,i表明数组的每个元素
intArray.forEach { i ->
    print(i + " "_
}
//输出:1 2 3

除了「lambda」表达式,这儿也用到了「闭包」的概念,这又是另一个话题了,这儿先不打开。

  • filter:对每个元素进行过滤操作,假如lambda表达式中的条件成立则留下该元素,不然踢出,最终生成新的调集
// [1, 2, 3]

// {2, 3}
// 留意,这儿变成了List
val newList: List = intArray.filter {i ->
    i != 1//过滤掉数组中等于1的元素
}
  • map:遍历每个元素并履行给定表达式,最终方式新的调集。
// [1, 2, 3]

// {2, 3, }
val newList: List = intArray.map{ i ->
    i +1 // 每个元素加1
}
  • flatMap:遍历每个元素,并为每个元素创立新的调集,最终合并到一个调集中.
// [1, 2, 3]

// {"2","a","3","a", "4", "a"}
intArray.floatMap {i -> 
    listOf("${i + 1}", "a"/)
}

这儿是以数组intArray为例,调集strList也同样有这些操作函数。Kotlin还有许多类似的操作函数。

除了数据和调集,Kotlin中还有另一种常用的数据类型:Range.

Range

在Java语言中并没有Range的概念,Kotlin中的Range表明区间的意思,也便是范围。区间的常见写法如下:

val range: IntRange = 0..1000

这儿的0..1000就表明从0到1000的范围,包含1000,数学上称为闭区间[01000]。除了这儿的IntRange,还有CharRange以及LongRange

Kotlin中没有纯的开区间的界说,不过有半开区间的界说:

val range: IntRange = 0 until 1000

这儿的0 until 1000表明从0到1000,但不包含1000,这便是半开区[0,1000)。

Range这个东西,天然生成便是用来遍历的:

val range = 0..1000
// 默许步长为1, 输入:0,1,2,3,4,5,6,7....1000
for (i in range) {
    print("$i, ")
}

这儿的in关键字能够与for循环结合运用,表明挨个遍历range中的值。关于for循环操控的运用,后边会做具体讲解。

除了运用默许的步长1,还能够经过step设置步长:

val range = 0..1000
// 步长为2, 输出:0,2,4,6,8,10....1000
for(i in range step 2) {
    print("$i, ")
}

以上试试递加区间,Kotlin还供给了递减区间downTo,不过递减没有半开区间的用法:

// 输出:4,3,2,1
for (i in 4 downTo 1) {
    print('$i, ")
}

其间4 downTo 1就表明递减的闭区间[4,1].这儿的downTo以及上面的step都叫做「中缀式表达」,之后的文章会做介绍。

Sequence

序列Sequence又被称为「慵懒调集操作」,关于什么是慵懒,我没听你经过下面的比方来解释:

val sequence = sequenceOf(1, 2, 3, 4)
val ressult: Sequence<Int> = sequence
    .map {i -> 
        println("Map $i")
        i * 2
    }
    .filter { i ->
        println("Filter $i")
        i % 3 == 0
    }

println(result.first())// 只取调集的第一个元素
  • 慵懒的概念首要便是说在「」标注之前的代码运行时不会当即履行,它仅仅界说了一个履行流程,只要result被运用品德时分才会履行。
  • 当「」的println履行时是数据处理流程是这样的:
    • 取出元素1 -> map为2->filter判别2是否能被3整除
    • 取出元素2 ->map为4->filter判别4是否能被3整除

慵懒指当呈现满意条件的第一个元素的时分,Sequence就不会履行后边的元素遍历了,即跳过了4的遍历。
List是没有慵懒的特性的:

List是没有慵懒的特性的:

val list = listOf(1, 2, 3, 4)
val result: List = list
    .map {i ->
        println("Map $i")
        i * 2
    }
    .filter { i ->
        println("Filter $i")
        i %3 == 0
    }

println(ressult.first())//只取调集的第一个元素

包含两点:

  • 声明之后当即履行
  • 数据处理流程如下:
    • {1, 2, 3, 4,} -> {2, 4, 6, 8}
    • 遍历判别是否能被3整除

Squence这种类似懒加载的完成有下面这些优点:

  • 一旦满意遍历退出的条件,就能够省掉后续不必要的遍历进程。
  • List这种完成Iterable接口的调集类,每调用一次函数就会生成一个新的Iterable,下一个函数再基于新的Iterable履行,每次函数调用发生的暂时Iterable会导致额定的内存耗费,而Sequence在整个流程中只要一个。

因此,Sequence这种数据类型能够在数据量比较大或许数据量不知道的时分,作为流式处理的处理方案。

条件操控

比较Java的条件操控,Kotlin中对条件操控进行了许多的优化及改善。

if/else

首要来看下Java中的if/else写法:

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

在Kotlin中,这么写当然也能够,不过,Kotlin中if句子还能够作为一个表达式赋值给变量:

val max = if (a > b) a else b

别的,Kotlin中启用了三元运算符(条件 ?然后:不然),不过咱们能够运用if/else来代替它。

上面的if/else的分支中是一个变量,其实还能够是一个代码块,代码块的最终一行会作为成果回来:

val max = if (a > b) {
    println("max: a")
    a //回来a
} else {
    println("max: b")
    b //回来b
}

when

在Java中,用switch句子老判别一个变量与一系列之中某个值是否持平:

switch (x) {
    case 1: {
        System.out.println("1");
        break;
    }
    case 2: {
        System.out.println("2");
        break;
    }
    default: {
        System.out.println("default");
    }
}

在Kotlin中变成when

when (x) {
    1 -> { println("1") }
    2 -> { println("2") }
    else -> { println("else") }
}

这儿与Java比较的不同点有:

  • 省掉了casebreak,前者比较好理解,后者的意思便是Kotlin主动为每个分支加上了break的功用,避免咱们像Java那样写错
  • Java中的默许分支运用的是default关键字,Kotlin中是运用的是else

if/else相同,when也能够作为表达式进行是运用,分支中最终一行的成果只作为回来值。需求留意的是,这时就有必要要有else分支,使得无论怎样都会有成果回来,除非现已列出了一切的状况:

val value: Int = when (x) {
    1-> { x + 1 }
    2-> { x * 2 }
    else -> { x + 5 }
}

在Java中,当多种状况履行同一份代码时,能够这么写:

switch (x) {
    case 1:
    case 2:{
        System.out.println("x== 1 or x == 2);
        break;
    }
    default: {
        System.out.println("default");
    }
}

而Kotlin中多种状况履行同一份代码时,能够将多个分支条件放在一同,用,符号隔开,表明这些状况都会履行后边的代码:

when (x) {
    1, 2 -> print("x == 1 or x == 2")
    else -> print("else")
}

when句子中,咱们还能够运用表达式作为分支的判别 条件:

  • 运用in检测是否在一个区间或许调集中:
when (x) {
    in 1..10 -> print("x 在区间 1..10 中")
    in listOf(1, 2) -> print("x 在调集中"// not in
    !in 10..20 -> print("x 不在区间 10..20中")
    else -> print("不在任何区间上")
}

-或许运用is进行特定类型的检测:

val isString = when(x) {
    is String -> true
    else -> false
}

还能够省掉when后边的参数,每一个分支条件都能够是一个布尔表达式:

when {
    str1.contains("a") -> print("字符串 str1 包含 a")
    str2.length == 3 -> print("字符串str2的长度为 ")
}

当分支的判别条件为表达式时,哪一条件先为true就履行哪个分支的代码块。

for

咱们知道Java对一个调集或数组能够这样遍历:

int[] array = {1, 2, 3, 4};
for (int item : array) {
    ...
}

而Kotlin中对数组的遍历是这么写的:

val array = intArrayOf(1, 2, 3, 4)
for (item in array) {
    ...
}

这儿与Java有几处不同:

  • Kotlin中,表明单个元素的item,不必显示的声明类型
  • Kotlin运用的是in关键字,表明itemarray里边的一个元素

别的,Kotlin的in后边的变量能够是任何完成Iterable接口的目标。

在Java中,咱们还能够这么写for循环:

for (int i = 0; i <= 10; i++) {
    //遍历从010
}

但Kotln中没有这样的写法,那应该怎样完成一个0到10的遍历呢?
其实运用书上面讲过的区间就能够完成啦,代码如下:

for (i in 0..10) {
    println(i)
}

try-catch

关于try-catch咱们都不生疏,在平常开发中不免都会遇到反常需求处理,那么在Kotlin中是怎样处理的呢,先看下Kotlin中捕获反常的代码:

 try {
     ...
 }
 catch (e: Exception) {
     ...
 } finally {
     ...
 }

能够发现Kotlin反常处理与Java的反常处理根本相同,但也有几个不同点:

  • 咱们知道在Java中,调用一个抛出反常的办法时,咱们需求对反常进行处理,不然就会报错:
public class User {
   void sayHi() throws IOException {
   }
   void test() {
       sayHi();
       // IDE报错,Unhandled exception: java.io.IOException
   }
}

但在Kotlin中,调用上方User类的sayHi办法时:

val user = User()
user.sayHi()// 正常调用,IDE不会报错,但运行时会报错

为什么这儿不会报错呢?由于Kotlin中的反常是不会被查看的,只要在运行时假如sayHi抛出反常,才会出错。

  • Kotlin中try-catch句子也能够是一个表达式,答应代码块的最终一行作为回来值:
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

?.和?:

咱们在之前的文章中现已讲过Kotlin的控安全,其实还有别的一个常用的复合符号能够让你判别空时愈加便利,那便是Elvis操作符?:

咱们知道控安全调用?.,在目标非空时会履行后边的调用,目标为空时就会回来null。假如这时将该表达式赋值给一个不行空的变量:

val str: String? = "Hello"
val length: Int = str?.length
// IDE报错,Type mismatch, Required:Int. Found:Int?

报错的原因便是str为null时咱们没有值能够回来给length
这时就能够运用Kotlin中额度Elvis操作符?:来兜底:

val str: String? = "Hello"
val length: Int = str?.length ?: -1

它的意思是假如左边表达式str?.length成果为空,则回来右侧的值-1
Elvis操作符还有别的一种常见用法,如下:

fun validate(user: Usser) {
    val id = user.id ?: return//验证user.id是否为空,为空时return
}
// 等同于
fun validate(user: User) {
    if (user.id == null) {
        return
    }
    val id = user.id
}

看到这儿,想必你对Kotlin的控安全有了更深入的了解了,下面咱们再看看Kotlin的持平比较符。

== 和 ===

咱们知道在Java中,==比较的假如是根本类型数据则判别值是否持平,假如比较的是String则表明引证地址是否持平,String字符串的内容比较运用的是euqlas()

String str1 = "123", str2 = "123";
System.out.println(str1.euqlas(str2));
System.out.println(str1 == str2);

Kotlin中也有两种持平比较办法:

  • ==:能够对根本数据类型以及String等类型进行内容比较,相当于Java中的queals
  • ===:对引证的内存地址进行比较,相当于Java中的==

能够发现,Java中的equals,在Kotlin中与之相对应的是==,这样能够使咱们的代码愈加简练。

下面再来看看代码实例:

val str1 = "123"
val str2 = "123"
println(str1 == str2) // 内容持平,输出:true
val str1 = "字符串"
val str2 = str1
val str3 = str1
print(str2 == str3)// 引证地址持平,输出true

其实Kotlin中的queals函数是==的操作符重载。

版权声明

本文首发于:rengwuxian.com/kotlin-basi…

微信大众号:扔物线