本章我们将介绍Kotli编程主要知识点,分别是变量(val、var)和函数(fun main()和语法糖)、逻辑控制语句(if、when条件语句和for-in循环、while循环)、面向对象编程(类和对象、继承(open和:)、构造函数(主和次构造函数)、接口(interface和修饰符)、数据类(data)和单例类(object))、Lambda编程(集合创建与遍历(listof和mutableListOf)、集合函数式API(maxBy、map和filter)和Java函数式API使用)、空指针检查(?.、?:和let函数)、字符串内嵌表达式($)和函数参数默认值(str:String=”hello”)。
2.1.Kotlin语言简介
        Kotlin是JetBrains公司2011年发布的,2017年android将其作为一级开发语言。
        编译型语言和解释型语言:前者是将源代码一次性编译诚计算机可识别的二进制文件,然后再执行;eg:c/c++;后者是一行行读取我们编写的源代码,将源代码实时转换为二进制后执行,效率差。eg:JS、Python。Java的class文件并不是二进制文件,而是交由JVM解释为二进制数据,因此是解释型语言。Kotlin也是编译为class文件交由JVM虚拟机进行处理。
        Kotlin相较于Java,语法简洁、语法更高级、安全性很高(几乎杜绝空指针)。
2.2.如何运行Kotlin
      1.IDEA天然支持;2.AndroidStudio中作为函数。

package com.company
fun main(){
    println("hello,kotlin")
}

2.3.变量和函数
2.3.1.变量

      Java支持基本数据类型和引用类型;基本数据类型有整数类型byte、short、int、long;浮点数类型float,double;字符类型char和布尔类型boolean。引用类型有String和array。Kotlin中定义了一个变量,只允许有在前面声明两种关键字,val和var。前者是不可变的变量(value),对应Java中的final关键字;后者声明可变的变量(variable),初始赋值之后仍可以被重新赋值。                                       《第一行代码》第三版之Kotlin编程入门上篇(二)_Kotlin

      Kotlin具有出色的类型推导机制。需要注意的是,Kotlin代码结尾是不加分号的。
      然而类型推导机制并不总是正常运行的,譬如延迟赋值;因此我们有时需要显式的声明变量类型。语法标志如下:var b: Int = 10,这时不会进行类型推导。Kotlin完全摒弃基本数据类型,全部使用了对象数据类型。int是关键字,Int是类。

fun main() {
    val a = 10
    var b: Int = 10
    b = b * 10
    println("b = " + b)
}

      什么时候使用val和var?优先使用val声明一个变量,当val无法满足需求时再使用var。
2.3.2.函数
       函数的格式如下所示:

fun 函数名(参数名:参数类型):参数类型{
    函数体
}

       在这里多使用编译器的代码提示和补全功能,因为可以自动补全代码和导包。求两个数中最大的数代码示例如下:

import kotlin.math.max
fun main() {
    val a = 10
    var b: Int = 10
    b = b * 10
    println("the larger is:"+largerNumber(a,b))
}
fun largerNumber(num1: Int, num2: Int): Int {
    return max(num1, num2)
}

       在这里介绍语法糖,只有一行代码时,Kotlin无需函数体,直接将唯一的一行代码写在函数尾部,中间以等号连接,等号表示了返回值,类型推导机制推导出返回类型必然是Int值,语法糖示例如下:

fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

2.4.程序的逻辑控制
         程序执行语句主要分为3种,顺序语句、条件语句和循环语句。Kotlin条件语句主要有两种方式:if和when。
2.4.1.if条件语句
         Kotlin中if的用法和Java中if的用法几乎没有区别。

fun largerNumber(num1: Int, num2: Int): Int {
    var value = 0
    if (num1 > num2) {
        value = num1
    } else {
        value = num2
    }
    return value
}

         不同的地方:if语句是有返回值的,返回值是将if语句每一个条件中最后一行代码作为返回值。因此上述的代码示例可以简化为如下代码:

fun largerNumber(num1: Int, num2: Int): Int {
    var value = if (num1 > num2) {
        num1
    } else {
        num2
    }
    return value
}

         将value再行简化,再利用语法糖,最后简化为:

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2

2.4.2.when条件语句
       when条件语句类似于Java的switch语句,但却比switch强大许多。Swtich仅能处理整形或者短语整形的变量、字符串变量。而且用法繁琐,每个都有break。
       代码的功能为查询考试成绩,输入为学生姓名,返回学生分数。if语句实现功能的代码如下:

fun getScore(name: String) = if (name == "Tom") {
    86
} else if (name == "Jim") {
    77
} else if (name == "Jack") {
    95
} else if (name == "Lily") {
    100
} else {
    0
}

        当判断语句过多时,可以考虑使用when语句。简化后代码如下所示,when和if一样是可以有返回值的。代码格式为:匹配值->{执行逻辑},一行可以省略{}。

fun getScore1(name: String) = when (name) {
    "Tom" -> 86
    "Jim" -> 77
    "Jack" -> 95
    "Lily" -> 100
    else -> 0
}

        除了精准匹配,when还可以做类型匹配。如下代码所示,is关键字是类型匹配核心,相当于instanceof关键字。首先函数接受一个Number类型(Kotlin内置类)的参数,Int、Long等为它的子类。

fun checkNum(num:Number){
    when(num){
        is Int -> println("number is int")
        is Double -> println("number is double")
        else -> println("not support")
    }
}

       可以在when语句中不传入参数,这种用法将表达式完整地写在when的结构体当中。

fun getScore2(name: String) = when {
    name == "Tom" -> 86
    name == "Jim" -> 77
    name == "Jack" -> 95
    name == "Lily" -> 100
    else -> 0
}

       这种写法看起来比较冗余,但如果Tom打头的人成绩都为86分,带参when语句就无法实现,使用不带参可以这样写,灵活了许多。

fun getScore3(name: String) = when {
    name.startsWith("Tom") -> 86
    name == "Jim" -> 77
    name == "Jack" -> 95
    name == "Lily" -> 100
    else -> 0
}

2.4.3.循环语句
       Java提供了while循环和for循环两种。Kotlin也提供了这两种:前者与Java用法完全相同,for循环有所不同。
       Kotlin提出了区间的概念,val range = 0..10。创建了一个0-10的区间,数学表示为[0,10],两个端点包含其中。最简单的for循环遍历如下所示。

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

       上面提到”..”是左闭右闭的区间,程序员都知道,在数组使用过程中,左闭右开的区间其实更常用,这里使用until关键字来表示左闭右开的区间。如果在循环中想跳过一些数字可以使用step关键字。以下代码表示为遍历[0,10)区间,会在区间范围内递增2。

  for (i in 0 until 10 step 2) {
        println(i)
  }

      前面提到的两个关键字都是左端必须小于等于右端,创建降序区间可以使用downTo关键字,用法如下。创建[10,1]的降序区间代码如下所示。

   for (i in 10 downTo  1 ) {
        println(i)
   }

      如果for不能达到我们的要求,可以使用while进行处理。
2.5.面向对象编程
       面向对象不同于面向过程的语言(譬如C),可以创建类,类是对事物的一种封装,譬如将汽车封装为一个类。类名通常为名词,类有自己的字段和函数,字段是该类所拥有的属性,譬如汽车的品牌和价格,字段名一般为名词,函数是该类有哪些行为,譬如汽车可以驾驶和保养,函数名一般为动词。创建类的对象之后调用该对象的字段和函数。面向对象有封装、继承和多态三个特性。
2.5.1.类与对象
       创建一个类,包括字段和函数。

//类中加入name和age字段,以及一个eat函数,人有名字和年龄,也需要吃饭。
class Person {
    var name = ""
    var age = 0
    fun eat() {
        println(name + " is eating. He is " + age + " years old")
    }
}

       将事物封装成具体类,将属性和能力定义类中字段和函数,接下来对类进行实例化,调用类中的字段和函数。

fun main(){
    //Kotlin的实例化与Java很类似,只是去掉了new关键字而已。
    // 因为你调用某个类的构造函数时,你的意图只可能是对这个类进行实例化。
    val p = Person()
    p.name ="Jack"
    p.age = 19
    p.eat()
}

2.5.2.继承
2.5.2.1.简单使用

        定义Student类,每个学生都有自己的学号sno和年纪grade子段,但同时学生也是人,也会有姓名年龄,也需要吃饭。因此Student类继承Person类拥有Person类中的字段和函数是减少冗余的一个好方法。
       为了使Student类继承自Person类,我们必须做两件事:(1)使得Person类可以被继承。Kotlin任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键字,防止后续修改而带来的风险,抽象类本身无法创建实例,必须子类继承后才能创建实例,与Java并无区别。Effective Java中有云,若某类不是专门为继承而设计,在主动加上final关键字禁止其被继承。在Person类前面加上open关键字即可实现;(2)要让Student类继承Person类,Java中使用extends,而Kotlin中变成一个冒号。需要注意的是Person后面还要加()。因此继承后的Student类名为class Student : Person(){...}。
2.5.2.2.主构造和次构造函数
       Kotlin将构造函数分为两种:主构造函数和次构造函数。主构造函数是常用的,每个类默认会有一个不带参数的主构造函数,当然你也可以显式标明。由于构造参数的实在创建实例的时候传入的,不需要重新赋值,可将参数声明为val。因此可以采用如下写法:

class Student(val sno: String, val grade: Int) : Person() {
}

       主构造函数没有函数体,如果想在其中编写一些逻辑,可以使用init结构体,所有逻辑都可写在其中。

class Student(val sno: String, val grade: Int) : Person() {
    //主构造函数中如需编写逻辑,使用init结构体
    init {
        println("sno is " + sno)
        println("grade is " + grade)
    }
}

       如何在子类构造函数中调用父类的构造函数?一种是init结构体中调用,但这种是不太好的,因为绝大多数场景无需init结构体;另一种是括号,Person后的括号意味着Student类的主构造函数在初始化时也会调用Person类的无参构造函数。将Person类的姓名和年龄放入主构造函数,与此同时,在Student类的主构造函数中加入age和name两个参数,并将这两个参数传给Person类的构造函数,这两个参数不加关键字,因为加上会和父类中的name和age字段发生冲突。此时的Person类和Student类如下所示:

//类中加入name和age字段,以及一个eat函数,人有名字和年龄,也需要吃饭。
open class Person(val name: String, val age: Int) {
    fun eat() {
        println(name + " is eating. He is " + age + " years old")
    }
}
//主构造函数特点是没有函数体,直接定义在类名后面即可。
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    //主构造函数中如需编写逻辑,使用init结构体
    init {
        println("sno is " + sno)
        println("grade is " + grade)
    }
}

      下面我们谈谈次构造函数,一个类可以有一个主构造函数和多个次构造函数,后者可以用于实例化一个类,只不过它是由函数体的。Kotlin规定,当一个类两者都有时,所有的次构造函数必须调用主构造函数(间接调用也可以)。
       次构造函数是通过Constructor定义的,在这里定义了两个,第一个次构造函数接受name和age参数,通过this关键字调用了主构造函数,并将sno和grade赋值;第二个构造函数不接受任何参数,它通过this关键字调用了我们刚才定义的第一个构造参数,并将name和age赋值成初始值,第二个构造函数间接调用了主构造函数,因此是合法的。

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) {}
}

      Main函数中的调用是这样的:

val student1 = Student()
val student2 = Student("Tom",20)
val student3 = Student("a123", 5, "Jack", 19)

      下面介绍一种非常特殊的情况,类中只有次构造函数,没有主构造函数。当一个类没有显式定义主构造函数且定义了次构造函数时,它是没有主构造函数的。既然没有,那么继承Person类无需括号,次构造函数直接调用父类的构造函数,将this关键字变为super关键字即可。

class Student2 : Person {
    constructor(name: String, age: Int) : super(name, age) {}
}

2.5.3.接口
       接口是实现多态的重要组成部分,Java是单继承却可以实现多个接口,Kotlin同样如此,接口中的函数不要求有函数体。
补充Java中接口的特点:1.接口中只能包含方法。(方法、属性、索引器、事件);2.接口中的方法不能有任何实现3.接口中的成员不能有任何访问修饰符(哪怕是public);4.接口不能被实例化;5.实现接口的类,必须实现接口的所有成员(这里跟抽象类一样);6.类不能多继承,所以在某些情况下,只能用接口来代替。接口可以多实现(一个类可以继承多个接口,而只能继承一个类);7.接口的主要目的就是为了实现多态。

interface Study {
    fun readBooks()
    fun doHomeWork()
}

      Java中继承使用的关键字是extends,实现接口用的是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。接口后面无需括号,因为没有构造函数可以调用。

class stu_pu(name: String, age: Int) : Person(name, age), Study {
    override fun readBooks() {
        println(name + " is reading.")
    }
    override fun doHomeWork() {
        println(name + " is doing homework.")
    }
}

       为了调用刚才定义的函数,我们首先创建Student类的实例,本可直接调用,但作者为了骚,将其传至doStudy函数,doStudy函数接受一个Study类型的参数,由于Student实现Study接口了,因此Student的实例是可以传过去的。接下来调用了接口的两个函数,这就被称为多态。

fun main() {
    val student = stu_pu("kd",30)
    doStudy(student)
}
fun doStudy(study: Study) {
    study.readBooks()
    study.doHomeWork()
}

       Kotlin允许对接口中定义的函数进行默认实现,JDK1.8以后其实也支持。将接口中的代码改成如下所示。

interface Study {
    fun readBooks()
    fun doHomeWork(){
        println("do homework default implementation")
    }
}

      下面我们谈谈Kotlin中函数的可见修饰符,Java中有public、private、protected和default四种修饰符。Kotlin中也有四种,分别是public、private、protected和internel,用哪种直接定义在fun前面即可。private两者功能相同,均为仅对当前内部类可见;public功能相同,均为所有类可见,但在Kotlin中public是默认修饰符,Java中default所示默认修饰符;protected关键字在Java中是当前类、子类和同一包路径下的类可见,但在Kotlin中只对当前类和子类可见;Kotlin抛弃了Java的default(同一包路径可见),引入internel,功能是只对同一模块中的类可见,譬如我们开发了一个模块,但有些函数只允许模块内部调用,可以将其声明为internel。
2.5.4.数据类和单例类
        数据类将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持,MVC、MVP、MVVM的M就是数据类型。
       数据类常需要重写equals、hashcode和toString三种方法。Equals判断数据类是否相等,hashcode是前者配套方法,若不重写会导致hash相关系统类无法工作。toString确保提供清晰输出日志,否则输出的是内存地址。Java的手机数据类如下所示。

public class cellphone {
    String brand;
    double price;
    public cellphone(String brand, double price) {
        this.brand = brand;
        this.price = price;
    }

    @Override
    public boolean equals(Object obj) {
        //instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例。
       //编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
        //1.obj 必须为引用类型,不能是基本类型2、obj 为 null,可匹配所有;
        if (obj instanceof cellphone) {//判断obj是不是cellphone类。
            cellphone other = (cellphone) 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 "Cellphone(brand=" + brand + ",princce=" + price + ")";
    }
}

       代码我们可以看到相对冗余,许多代码没有实际意义。在Kotlin中相同的功能只需要一行代码就可以实现。在类前面声明data关键字,即你希望它是数据类,Kotlin会根据主构造参数帮你生成上述的三种方法,大大减少开发量。

data class cellphone1(val brand: String, val price: Double)

      调用部分的代码如下所示:

fun main() {
    val cellphone_1 = cellphone1("Sumsung", 1299.99)
    val cellphone_2 = cellphone1("Sumsung", 1299.99)
    println(cellphone_1)
    println("cellphone1 equals cellphone2? " + (cellphone_1 == cellphone_2))
}

     cellphone有data修饰输出如下所示:

  《第一行代码》第三版之Kotlin编程入门上篇(二)_构造函数_02
     没有data修饰输出如下所示:

  《第一行代码》第三版之Kotlin编程入门上篇(二)_java_03
     Java中有单例模式,功能是避免创建重复的对象,使得某个类在全局最多只能有一个实例。单例模式的Java常见写法如下所示。

public class Singleton {
    //声明为private禁止外部创建Singleton的实例
    private static Singleton instance;
    private Singleton(){}
    //为外部提供getInstance静态方法用于获取实例
    public synchronized static Singleton getInstance(){
        //先判断有没有缓存实例,若无,创建新实例,若有,返回缓存内存实例。
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }

    public void SingletonTest(){
        System.out.println("SingletonTest is called.");
    }
}
Main方法中的调用如下所示:
public class main {
    public static void main(String args[]) {
       Singleton singleton = Singleton.getInstance();
       singleton.SingletonTest();
    }
}

       Main方法中的调用如下所示:

public class main {
    public static void main(String args[]) {
       Singleton singleton = Singleton.getInstance();
       singleton.SingletonTest();
    }
}

       在Kotlin中只需要将class关键字改为object关键字,一个单例类就完成了。Kotlin在背后为我们自动创建了Singleton类实例,确保全局只有一个实例。

object singleton2 {
    fun sigletonTest() {
        println("singleton2 is called")
    }
}

      调用代码如下所示:

 singleton2.sigletonTest()