「码上开学——hencoder」Kotlin笔记(Kotlin的变量、函数和类型)

290 阅读17分钟

原文地址:【码上开学】Kotlin 的变量、函数和类型

我是二期课程的学员,最近正好补一下Kotlin的基础,发现rengwuxian的码上开学上的文章写的通俗易懂,我就摘抄来作为笔记。

原文写的太好了,我就整篇博客大部分摘抄下来作为记录,如果想看详细建议看原本,经过和扔物线老师的同意才发出来的。

一、变量

1.1、变量

image.png 这里有几处不同

  • 有一个var关键字
  • 类型和变量名位置互换
  • 中间是用过冒号分隔的
  • 结尾没有分号(对,Kotlin里面不需要分号)

image.png

  • 那什么是属性呢?这里我们可以简单类比Java的field来理解Kotin的Property,虽然它们其实有些不一样,Kotlin的Property功能会多些。
  • 变量居然还能声明成抽象的?嗯,这是Kotlin的功能,不过这里先不理它,后面会讲到。

属性为什么要初始化呢?因为Kotlin的变量是没有默认值的,这点不像Java,Java的field有默认值。

image.png 但这些Kotlin是没有的。不过其实,Java页只是field有默认值,局部变量也是没有默认值的,如果不给它初始值也会报错:

image.png 既然这样,那我们就给它一个默认值null吧,遗憾的是你会发现仍然报错

image.png

在Kotlin里面,所有的变量默认值都是不允许为空的,如果你给它赋值null,就会报错,像上面那样。

这种有点强硬的要求,其实是很合理额度:既然你声明了一个变量,就是要使用它对吧?那你把它赋值为null干嘛?要尽量让它有可用的值啊。Java在这方面很酷阿侬,我们成了习惯,但Kotlin更强的限制其实在你所熟悉了之后,是会减少很多运行时的问题的。

不过,还是有些场景,变量的值真的无法保证空与否,比如你要从服务器取一个JSON数据,并把它解析成了一个User对象:

image.png 这个时候,空值就是有意义的。对于这些可以为空值的变量,你可以在类型右边加一个?号,解除他的非空限制:

class User {
    var name: String? = null
}

加了问号之后,一个Kotlin变量就像Java变量一样没有非空的限制了,自由自在了。

你除了在初始化的时候可以给它赋值为空值,在代码里的任何地方都可以:

var name: String? = "Mike"
...
name = null // 原来不是空值,赋值为空值

这种类型之后加?的写法,在Kotlin里叫可空类型。 不过,当我们使用了可空类型的变量后,会有新的问题: 由于对空引用的调用会导致空指针异常,所以Kotlin在可空变量直接调用的时候IDE会报错:

var view: View? = null
view.setBackgroundColoe(Color.RED)
// 这样写会报错,Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type View?

「可能为空」的变量,Kotlin不允许用。那怎么办?我们尝试用之前检查一下,但似乎IDE不接受这种做法:

if (view != null) {
    view.setBackgroundColor(Color.RED)
    // 这样写会报错,Smart cast to 'View' is impossible, because 'view' is a mutable property that could have been changed by this time
}

这个报错的意思是即使你检查了给空也不能保证下面调用的时候是非空,因为在多线程情况下,其他线程可能把它再改成空的。

那么Kotlin里是怎么解决这个问题的呢?它用的不是.而是?.

view?.setBackgroundColor(Color.RED)

这个写法同样会对变量做一次非空确认之后再调用方法,这是Kotlin的写法,并且它可以做到线程安全,因此这种写法叫做「safe call」。

另外还有一种双感叹号的用法:

view!!.setBackgroundColor(Color.RED)

意思是告诉编辑器,我保证这里的view一定是非空的,编辑器你不要帮我做检查了,有什么后果我自己承担。这种「肯定不会为空」的断言式的调用叫做「non-null asserted call」。一旦用了非空断言,实际上和Java就没什么两样了,但也就享受不到Kotlin的控安全所涉及带来的好处(在编译时做检查,而不是运行时抛异常)了。

以上就是Kotlin的空安全设计。

很多人在上手的时候都被声明搞懵,原因就是Kotlin的控安全设计所导致的这些报错:

  • 变量需要手动初始化,如果不初始化的话会报错;
  • 变量默认非空,所以初始化赋值null的话报错,之后再次赋值为null也会报错;
  • 变量用?设置为空的时候,使用的时候因为「可能为空」有报错。

明白了控安全设计的原理后,就很容易能够解决上面的问题了。 关于控安全,最重要的是记住一点:所谓「可空不可空」,关注的全是使用的时候,即「这个变量在使用时是否可能为空」

另外,Kotlin的这种控安全设计在与Java的互调上时完全兼容的,这里的兼容指:

  • Java里面的@Nullable注解,在Kotlin里调用时同样需要用?.

image.png

  • Java里面的@Nullable@NonNull注解,在转换成Kotlin后对应的就是可空变量和不可空变量,至于怎么将Java代码转换为Kotlin,Android Studio给我们提供了很方便的该工具(但并不完美),后面会讲。

image.png

控安全我们讲了这么多,但是有时候我们声明的变量是不会让它为空,比如view,其实在实际场景中,我们希望它一直是非空额度,可空并没有业务上的上实际意义,使用?.影响代码可读性。

但如果你在MainActivity里这么写:

class MainActivity: AppCompatActivity () {
    var view: View = findViewById(R.id.tvContent)
}

虽然编译器不会报错,但程序一旦运行起来就crash了, 原因是findViewById()是在onCreate之后才能调用。

那怎么办呢?其实我们很想告诉编辑器『我很确定我用时候绝对不为空,但第一时间我们法给它赋值』。 Kotlin给我们提供了一个选项:延迟初始化

1.2、延迟初始化

lateinit var view: View

这个lateinit的意思是:告诉编辑器我没法第一时间就初始化,但我肯定会在使用它之前完成初始化的。 它的作用就是让IDE不要对这个变脸检查初始化和报错。换句话说,加了这个lateinit关键字,这个变量的初始化就全靠你自己了,编辑器不帮你检查了。 然后我们就可以在onCreate中进行初始化了

lateinit var view: View
override fun onCreate(...) {
    ...
    view = findViewById(R.id.tvContent)
}

对了,延迟初始化对变量的赋值次数没有限制,你仍然可以在初始化之后再赋其他的值给view

1.3、类型判断

Kotlin有个很方便的地方是,如果你在声明的啥时候就赋值,那不写变量类型也行:

var name: String = "Mike"
var name = "Mike"

这个特性叫做「类型判断」,它跟动态类型是不一样的,我们不能像Groovy或者JavaScript那样使用Kotlin里这么写:

var name = "Mike"
name = 1
//会报错,The integger literal does not conform to the expected  type String
// Groovy
def a = "haha"
a = 1
// 这种先复制字符串在复制数字的方式在Groovy里是可以的

『动态类型』是指变量的类型在运行时可以改变;而「类型推断」是你在代码里不用谢变量类型,编译器在编译的时候会帮你补上。因此,Kotlin是一门静态语言。

除了变量赋值这个场景,类型推断的其他场景我们之后也会遇到。

1.4、val和var

声明变量的方式也不止var一种,我么你还可以使用val

val size = 18

val是Kotlin在Java「变量」类型之外,又增加了一种变量类型:只读变量。它只能赋值一次,不能修改,而var是一种可读可写变量

var是variable的缩写,val是value的缩写

val和Java中的final类似:

final int size = 18;

不过其实它们还是有些不一样的,这个我们之后再讲。总之直接进行重新赋值是不行的。

1.5、可见性

看到这里,我们似乎没有在Kotlin里看到类似Java里的publicprotectedprivate这些表示变量可见性的修饰符,因为在Kotlin里变量默认就是public的,而对于其他可见性修饰符,我们之后会讲,这里先不用关心。 至此,我相信你对变量这部分已经了解的差不多了,可以根据前面的例子动手尝试尝试。

二、函数

Kotlin除了变量声明外,函数的声明方式也和Java的方法不一样。Java的方法(method)在Kotlin里叫函数(function),其实也没啥区别,或者说其中的区别我们可以忽略掉。对任何编程语言来说,变量就是用来存储数据,而函数就是用来处理数据。

2.1、函数的声明

我们先来看看Java里的方法是怎么写的:

Food cook(String name) {
    ...
}

而到了Kotlin,函数的声明是这样:

fun cook(name: String): Food {
    ...
}
  • fun关键字开头
  • 返回值写在了函数和参数后面

那如果没有返回值该怎么办?Java里是返回void:

void main() {
    ...
}

Kotlin里是返回Unit,并且可以省略:

fun main(): Unit {}
// Uniit 返回类型可以省略
fun main() {}

函数参数也可以有可空的控制,根据前面说的控安全设计,在传递时需要注意:

// 可空变量传给不可空参数,报错
var myName : String? = "rengwuxian"
fun cook(name: String) : Food {}
cook(myName)

// 可空变量传给可空参数,正常运行
var myName : String? = "rengwuxian"
fun cook(name: String?) : Food {}
cook(myName)

// 不可空变量传给不可空参数,正常运行
var myName : String = "rengwuxian"
fun cook(name: String) : Food {}
cook(myName)

2.2、可见性

函数如果不加可见性修饰符,默认的可见范围和变量一样也是public的,但有一种情况例外,这里简单提一下,就是遇到override关键字的时候,下面会讲到、

2.3、属性的getter/setter函数

我们知道,在Java里面的field经常会导游getter/setter函数:

public class User {
    String name;
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

它们的作用就是可以自定义函数内部实现来达到「钩子」的效果,比如下面这种:

public class User {
    String name;
    public String getName() {
        return this.name + " nb";
    }
    public void setName(String name) {
        this.name = "Cute " + name;
    }
}

在Kotlin里,这种getter/setter是会怎么运作的呢?

class User {
    var name = "Mike"
    fun run() {
        name = "Mary"
        // 上面的写法实际上是这么调用的
        // setName("Mary")
        // 建议自己试试,IDE的代码补全功能会在你打出setn的时候直接提示name而不是setName
        
        println(name)
        //上面的写法实际上是这么调用的
        // println(getName())
        // IIDE的代码补全功能在你打出getn的时候直接提示name而不是getName
    }
}

那么我们如何来操作前面提到的「钩子」呢?看下面这段代码

class User {
    var name = "Mike"
    get() {
        return field + " nb"
    }
    set(value) {
        field = "Cute " + value
    }
}

格式上和Java有一些区别:

  • getter/setter函数有了专门的关键字get和set
  • getter/setter函数位于var所声明的变量下面
  • setter函数的参数是value

除此之外还多了一个叫field的东西。这个东西叫做「Backing Field」 ,中文翻译是幕后字段后备字段。具体来说,你的这个代码:

clas Kotlin {
    var name = "kaixue.io"
}

在编译后的字节码大致等价于这样的Java代码:

public final class Kotlin {
    @NotNull
    private String name = "kkaixue.io";
    
    @NotNull
    public final String getName() {
        return this.name;
    }
    
    public final void setName(@NotNull String name) {
        this.name = name;
    }
}

上面的那个String name就是Kotlin帮我们自动创建的一个Java field。这个field对编码的人不可见,但会自动应用于getter和setter,因此它被命名为「Backing Field」(backing的意思是在后进行支持,例如你闯了大祸,我懂用能量来保住你的人头,我就是在back you)。

所以,虽然Kotlin的这个field本质上确实是一个Java中的field,但对于Kotlin的语法来讲,它和Java里面的field完全不是一个概念。在Kotlin里,它相当于每一个var内部的一个变量。

我们前面讲过val是只读变量,只读的意思就是说val声明的变量不能进行重新赋值,也就是说不能调用setter函数,因此,val声明的变量是不能重写setteer函数的,但它可以重写getter函数:

val name = "Mike"
    get() {
        return field + " nb"
    }

val所声明的只读变量,在取值的时候仍然可能被修改,这也是和Java里的final的不同之处。

关于「钩子」的作用,除了修改取值和赋值,也可以加一些自己的逻辑,就像我们在Activity的生命周期函数里做的事情一样。

三、类型

在Kotlin中,所有东西都是对象,Kotlin中使用的基本的类型有:数字、字符、布尔值、数组与字符串

var number : Int = 1 //还有Double Float Long Short Byte都类似
var c: Chhar = 'c'
var b: Boolean = true
var array: IntArray = intArrayOf(1, 2)// 类似的还有FloatArray DoubleArray CharArray等,intArrayOf是Kotlin的built-in函数
var str: String = "string"

这里有两个地方和Java不太一样:

  • Kotlin里的Int和Java里的int以及Integer不同,主要是在装箱方面不同。

Java里的int是unbox的,而Integer是box的:

int a = 1;
Integer b = 2; // 会被自动装箱autoboxing

Kotlin里,Int是否装箱根据场合来定:

var a: Int = 1// unbox
var b: Int? = 2 // box
var list: List<Int> = listOf(1, 2)// box

Kotlin在语言层面简化了Java中的int和Integer,但是我们对是否装箱的场景还是要有一个概念,因为这个牵涉到程序运行时的性能开销。 因此在日常的使用中,对于Int这样的基本类型,尽量用不可空变量。

  • Java中的数组和Kotlin中的数组的写法也有区别:
// Java 
int[] array = new int[] {1, 2};

而在Kotlin里,上面的写法是这样的:

var array: IntArray = intArrayOf(1, 2)
// 👆🏻这种也是unbox的

简单来说,原先在Java里的基本类型,类比到Kotllin里面,条件满足如下之一的就不装箱

  • 不可空类型
  • 使用IntArray、FloatArray等。

四、类和对象

现在可以来看看我们的老朋友MainActivity了,重新认识下它

class MainActivity : AppCompatActivity() {
    override fun onCreate(saveInstanceState: Bundle?) {
    
    }
}

我们可以对比Java的代码来看有哪些不同:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle saveInstanceState) {
        ...
    }
}
  • 首先是类的可见性,Java中的public在Kotlin中可以省略,Kotlin的类默认是public的。
  • 累的继承写法,Java里用的是extends,而在Kotlin里使用:,但其实:不仅可以表示继承,还可以表示Java中的implement.

举个例子,假设我们有一个interface叫Imple:

interface Impl {}

Kotlin里定义一个interface个Java没什么区别

// Java
public class Main2Activity extends AppCompatActivity implements Impl { }
// Kotlin
class MainActivity : AppCompatActivity(), Impl {}
  • 构造方法的写法不同
    • Java里省略了默认的构造函数:
    publiic class MainActivity extends AppCompatActivity {
        // 默认构造函数
        public MainActivity() {
        }
    }
    
    • Kotlin里我们注意到AppCompatActivity后面的(),这其实也是一种省略的写法,等价于
    class MainActivity constructor() : AppCompatActivity() {
    
    }
    
    不过其实更像Java的写法是这样的:
    // 注意这里AppCompatActivity后面没有'()'
    class MainActivity : AppCompatActivity {
        constructor() {
        }
    }
    
    Kotlin把构造函数单独用了一个constructor关键字来和其他的fun做区分。
  • override的不同
    • Java里面@Override是注解的形式
    • Kotlin里的override变成了关键字。
    • Kotlin省略了protected关键字,也就是说,Kotlin里的override函数的可见性是继承自父类的。

除了以上这些明显的不同之外,还有一些不同点从上面的代码里看不出来,但当你写一个类去继承MainActivity时就会发现:

  • Kotlin里的MainActivity无法继承
// 👇写法会报错,This ttype is final ,so it cannot be inherited from
class NewActivity: MainActivity() {
}

原因是Kotlin里的类默认是final的,而Java里只有加了final关键字的类才是final的。

那么有什么办法解除final限制呢?我们可以使用open来做这件事:

open class MainActivity: AppCompatActivity() {}

这样一来,我们就可以继承了

class NewActivity: MainActivity() {}

但是要注意,此时NewActivity仍然是final,也就是说,open没有父类到子类的遗传性。

而刚才说到override是有遗传性的:

class MainActivity : MainActivit() {
    // 👇onCreate仍然是override的
    override fun onCreate(saveInstanceState: Bundle?) {
        ...
    }
}

如果要关闭override的遗传性,只需要这样即可:

open class MainActivity: AppCompatActivity() {
    //👇加了final关键字,作用和Java里面一样,关闭了override的遗传性
    final override fun onCreate(saveInstanceState: Bundle?){
       ...
    }
}

Kotlin里除了新增的open关键字外,也有和Java一样的abstract关键字,这俩关键字的区别就是abstract关键字修饰的类无法直接实例化,并且通常来说会和abstract修饰的函数一起出现,当然,也可以没有这个abstract函数。 一起出现,当然,也可以没有这个abstract函数。

abstract class MainActivity: AppCompatActivity() {
    abstract fun test()
}

但是子类如果要实例化,还是需要实现这个abstract函数的:

class NewActivity : MainActivity() {
    override fun test() {}
}

当我们声明好一个类之后,我们就可以实例化它了,实例化在Java中使用new关键字:

void main() {
    Activity activity = new NewActivity();
}

而在Kotlin中,实例化一个对象更加简单,没有new关键字:

fun main() {
    var activity: Activity = NewActivity()
}

通过MainActivity的学习,我们知道了Java和Kotin中关于类的声明主要关注以下几个方面:

  • 类的可见性和开放性
  • 构造方法
  • 继承
  • override函数

五、类型的判断和强化

刚才讲的实例化的例子中,我们实际上是把子类对象赋值给父类的变量,这个概念在Java里叫多态,Kotlin也有这个特性,但在实际工作中我们很可能会遇到需要使用子类才有的函数。

比如我们先在子类中定义一个函数:

class NewActivity: MainActivity() {
    fun action() {}
}

那么接下来这么写是无法调用该函数的:

fun main() {
    var activity: Activity = NewActivity()
    // 👆activity是无法调用NewActivity的action方法的
}

在Java里,需要先使用instanceof关键字判断类型,再通过强转来调用:

void main() {
    Activity activity = new NewActivity();
    if (activity instanceof NewActivity) {
        ((NewActivity) activity).action();
    }
}

Kotlin里同样有类似解决发方法,使用is关键字进行「类型判断」,并且因为编辑器能够进行类型判断,可以帮助我们省略强转的写法:

fun main() {
    var activity: Activity = NewActivity()
    if (activity is NewActivity) {
        // 👇的强转由于类型判断被省略了
        activity.action()
    }
}

那么能不能进行类型判断,直接进行强转调用呢?可以使用as关键字:

fun main() {
    var activity: Activity = NewActivity()
    (activity as NewActivity).action()
}

这种写法如果强转类型操作是正确的当然没问题,但如果强转成一个错误的类型,程序就会抛出一个异常。

我们更希望能进行安全的强转,可以更优雅地处理强转出错的情况。 这一点,Kotlin在设计上自然也考虑到了,我们可以使用as?来解决:

fun main() {
    var activity: Activity = NewActivity()
    //👇'(activity as? NewActivity)'之后是一个可空类型的对象,所以没需要使用'?.'来调用
    (activity as? NewActivity)?.action()
}

它的意思就是说如果强转成功就执行之后的调用,如果强转不成功就不执行。

版权声明

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

微信公众号:扔物线

转载时请保留此声明