【Kotlin】类与接口

文章目录

类的定义

类可以包含:

  • 构造函数和初始化块
  • 函数
  • 属性
  • 嵌套类和内部类
  • 对象声明

你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以

拥有自己独有的属性值。例如,每个Dog对象都有自己的名称、重量和品种属性,每个Dog的属性值都可以是不同的。

kotlin 复制代码
class Dog(val name: String, var weight: Int, val breed: String){
    fun woo() {
        
    }
}

如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号:

kotlin 复制代码
class Dog(val name: String, var weight: Int, val breed: String)

创建类的实例

kotlin 复制代码
val myDog = Dog("Fido", 70, "Mixed" )

上面的类有一个默认的构造函数。

注意:创建类的实例不用new

构造函数

Kotlin中的一个类可以有一个主构造函数和一个或多个次构造函数。

主构造函数

主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后:

kotlin 复制代码
class Person constructor(name: String, surname: String) {
}

如果主构造函数没有任何注解或者可见性修饰符,可以省略constructor关键字:

kotlin 复制代码
class Person(name: String, surname: String) {
}

主构造函数不能包含任何的代码。初始化的代码可以放到以init关键字作为前缀的初始化块中:

kotlin 复制代码
class Person constructor(name: String, surname: String) {
    init {
        print("init")
    }
}

如果构造函数有注解或可见性修饰符,那么constructor关键字是必需的,并且这些修饰符在它前面

次构造函数

类也可以声明前缀有constructor的次构造函数:

kotlin 复制代码
class Person{
    constructor(name: String) {
        print("name is $name")
    }
}

如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。

委托到同一个类的另一个构造函数用this关键字即可:

kotlin 复制代码
class Person constructor(name: String) {
    constructor(name: String, surName: String) : this(name) {
        print( "name is : $name surName is : $surName")
    }
}
init语句块

Kotlin引入了一种叫作init语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。构造方法在类的外部,它只能对参数进行赋值。

如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如:

kotlin 复制代码
class Bird(weight: Double, age: Int, color: String) {
    init {
        println("the weight is ${weight}")
    }
}

当没有val或者var的时候,构造函数的参数可以在init语句块被直接调用。除此之外,不能在其他地方使用。以下是一个错误的用法:

kotlin 复制代码
class Bird(weight: Double, age: Int, color: String) {
    fun printWeight() {
        print(weight) // Unresolved reference: weight
    }
}

事实上,我们的构造方法还可以拥有多个init,他们会在对象被创建时按照类中从上到下的顺序先后执行。例如:

kotlin 复制代码
class Bird(weight: Double, aget: Int, color: String) {
    val weight: Double
    val age: Int
    val color: String
    init {
        this.weight = weight
        this.age = age
    }
    init {
        this.color = color
    }
}

可以发现,多个init语句块有利于进一步对初始化的操作进行职能分离,这在复杂的业务开发中显得特别有用。

数据类的定义

数据类通常需要重写equals()hashCode()toString()这几个方法.

但是在Kotlin中你只需要一行代码。

数据类是一种非常强大的类:

使用Kotlin:

kotlin 复制代码
data class Artist(
    var id: Long,
    var name: String,
    var url: String,
    var mbid: String)

数据类自动覆盖它们的equals方法以改变操作符的行为,由此通过检查对象的每个属性值来判断是否相等。

例如,假设你创建了两个属性值完全相同的Artist对象,使用操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。

通过数据类,会自动提供以下函数:

  • 所有属性的get() set()方法
  • equals()
  • hashCode()
  • copy()
  • toString()
  • componentN()

如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的或者多个属性被修改的实例。

这个任务是非常重复且不简洁的。

举个例子,如果要修改Person类中xoliuage:

kotlin 复制代码
data class Person(val name: String,val age: Int)
kotlin 复制代码
val p1 = Person("xoliu", 19)
val p2 = p1.copy(age = 22)

如上,我们拷贝了对象然后只修改了age的属性而没有修改这个对象的其它状态。

如果你要在Kotlin声明一个数据类,必须满足以下几点条件:

  • 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的。
  • 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明
  • data class之前不能用abstract、open、sealed或者inner进行修饰

与任何其他类一样,你可以向数据类添加属性和方法,只需要将它们包含在类主体中。但是有一个大问题,就是在编译器生成数据类的方法实现时,

比如覆盖equals方法和创建copy方法,它仅包含在主构造函数中定义的属性。因此如果你在数据类主体中定义添加的属性,则它们不会被包含到任何编译器生成的方法中。

数据类定义了componentN方法

定义数据类时,编译器会自动向该类添加一组方法,你可以将其作为访问对象属性值的替代方法。它们被称为componentN方法,其中N表示被访问属性的编号(按声明排序)。多声明,也可以理解为变量映射

继承

Kotlin中所有类都有一个共同的超类Any(java是Object),这对于没有超类型声明的类是默认超类:

kotlin 复制代码
class Person // 从 Any 隐式继承

Any不是java.lang.Object。它除了equals()hashCode()toString()外没有任何成员。

在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符open,如果不加open,那它在转化为Java代码时就是final的:

所以Kotlin中所有的类默认都是不可继承的(final)

所以我们只能继承那些明确声明open或者abstract的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后:

kotlin 复制代码
open class Person(num: Int)
// 继承
class SuperPerson(num: Int) : Person(num)

冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。

调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。

请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。

假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。

注意: 上面在说到继承的时候class SuperPerson(num: Int) : Person(num)在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。

Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)

但是如果类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin

中是允许类中只有次构造函数,没有主构造函数的。当一个类没有显式的定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。

如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。

如果类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其基类型,或委托给另一个构造函数做到这一点。

注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:

kotlin 复制代码
class MyView : View {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。那么既然没有主构造函数,继承View类

的时候也就不需要再在View类后加上括号了。

另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。

Any

我们都知道,Java并不能在真正意义上被称为一门"纯面向对象"语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。

但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。

Any:非空类型的根类型

与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如:

与Java不同的是,Kotlin不区分"原始类型"(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,

则该类型将是Any的直接子类型。如:

kotlin 复制代码
class Animal(val weight: Double)
Any?:所有类型的根类型

如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。

覆盖

方法覆盖

只能重写显示标注可覆盖的方法:

kotlin 复制代码
open class Person(num: Int) {
    open fun changeName(name: String) {

    }

    fun changeAge(age: Int) {

    }
}

class SuperPerson(num: Int) : Person(num) {
    override fun changeName(name: String) {
        // 通过super关键字调用超类实现
        super.changeName(name)
    }
}

SuperPerson.changeName()方法前面必须加上override标注,不然编译器将会报错。如果像上面Person.changeAge()方法没有标注open,则子类中不能定义相同的方法: (不能重写,但能重载)

kotlin 复制代码
class SuperPerson(num: Int) : Person(num) {
    override fun changeName(name: String) {
        super.changeName(name)
    }

    // 编译器报错
    fun changeAge(age: Int) {

    }
    // 重载是可以的
    fun changeAge(name: String) {

    }
    // 重载是可以的
    fun changeAge(age: Int, name: String) {

    }
}

标记为override的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,可以使用final关键字:

kotlin 复制代码
open class SuperPerson(num: Int) : Person(num) {
    final override fun changeName(name: String) {
        super.changeName(name)
    }
}
属性覆盖

属性覆盖与方法覆盖类似,只能覆盖显式标明open的属性 ,并且要用override开头:

kotlin 复制代码
open class Person(num: Int) {
    open val name: String = ""

    open fun changeName(name: String) {

    }

    fun changeAge(age: Int) {

    }
}

open class SuperPerson(num: Int) : Person(num) {
    override val name: String
        get() = super.name

    final override fun changeName(name: String) {
        super.changeName(name)
    }

}

每个声明的属性可以由具有初始化器的属性或者具有get方法的属性覆盖,如果某个属性在父类中被定义为val,你可以在子类中使用var属性覆盖它。

只需要覆盖该属性并将其声明为var即可。请注意,这只适用于这一种方式。如果尝试使用val覆盖var属性,编译器将会报错

抽象类

类和其中的某些成员可以声明为abstract。抽象成员在本类中可以不用实现。需要注意的是,我们并不需要用open标注一个抽象类或者函数------因为这不言而喻,这些属性一定需要去实现的

我们可以用一个抽象成员覆盖一个非抽象的开放成员:

kotlin 复制代码
open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

接口:使用interface关键字

接口可以让你在父类层次结构之外定义共同的行为,接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点,而且使用起来更加灵活。

kotlin 复制代码
interface FlyingAnimal {
    fun fly()
}

虽然Kotlin接口支持属性声明,然而它在Java源码中是通过一个get方法来实现的。在接口的属性并不能像Java接口那样,被直接赋值一个常量。如以下这样是错误的:

kotlin 复制代码
interface Flyer {
    val height = 1000 // error Property initializers are not allowed in interfaces
    val speed: Int
    // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的,Java8也开始支持了接口方法的默认实现
    fun fly() {
        println("I can fly")
    }
}

Kotlin提供了另外一种方式来实现这种效果:

kotlin 复制代码
interface Flyer {
    val height 
        get() = 1000
}

一个类实现接口时:

kotlin 复制代码
class Bird() : Flyer {
    //  ...
}

接口的后面不用加上括号,因为它没有构造函数可以去调用。

函数:fun

kotlin 复制代码
fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

如果你没有指定它的返回值,它就会返回UnitUnitJava中的void类似,但是Unit是一个类型,而void只是一个关键字。Unit可以省略。

kotlin 复制代码
fun maxOf(a: Int, b: Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}

Unit:让函数调用皆为表达式

如果函数返回Unit类型,该返回类型应该省略:

kotlin 复制代码
fun foo() { // 省略了 ": Unit"

}

之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如:

java 复制代码
void foo() {
    System.out.println("return nothing")
}

所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型 ,所以他们引入了Unit来替代Java中的void关键字。

Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。

表达式函数体

如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号:

kotlin 复制代码
fun add(x: Int,y: Int) : Int = x + y // 省略了{}

Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体

的情况下我们可以不声明返回值类型,这进一步简化了语法。

我们可以给参数指定一个默认值使的它们变的可选,这是非常有帮助的。这里有一个例子,在Activity中创建了一个函数用来Toast一段信息:

kotlin 复制代码
fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, length).show()
}

上面代码中第二个参数length指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数:

kotlin 复制代码
toast("Hello")
toast("Hello", Toast.LENGTH_LONG)

类头格式化

有少数几个参数的类可以写成一行:

kotlin 复制代码
class Person(id: Int, name: String)

具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。此外,右括号应该另起一行。如果我们使用继承,

那么超类构造函数调用或者实现接口列表应位于与括号相同的行上:

kotlin 复制代码
class Person(
    id: Int, 
    name: String,
    surname: String
) : Human(id, name) {
    // ......
}

对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中:

kotlin 复制代码
class Person(
    id: Int, 
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker {
    // ......
}
相关推荐
雨白16 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk16 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING17 小时前
RN容器启动优化实践
android·react native
恋猫de小郭19 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker1 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴1 天前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe2 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos