从零开始的Kotlin—类和对象

209 阅读23分钟

Kotlin是一种现代编程语言,可编译为Java字节码。它是免费和开源的,它有望使Android的编码变得更加有趣。

上一篇文章中,你学到了函数的高级用法,如Kotlin中的扩展函数、闭包、高阶函数和内联函数。

在这篇文章中,你将通过学习类来了解Kotlin中的面向对象编程:构造函数和属性、铸造,以及Kotlin使之变得简单的更多高级类特性。

  1. Any和Nothing类型
  2. 可见性修改器
  3. 智能投递
  4. 明确的投射
  5. 对象
  6. 同伴对象

1.类

类是一个程序单元,它将函数和数据组合在一起以执行一些相关的任务。我们在Kotlin中使用class 关键字声明一个类--与Java类似。

class Book

前面的代码是最简单的类声明--我们只是创建了一个名为Book 的空类。即使这个类不包含主体,我们仍然可以使用它的默认构造函数来实例化这个类。

val book = Book()

正如你在上面的代码中所观察到的,我们没有使用new 关键字来实例化这个类--这在其他编程语言中很常见。new 在Kotlin中不是一个关键字。这使得我们的源代码在创建类实例时更加简洁。但是请注意,在Java中实例化一个Kotlin类将需要new 这个关键字。

// In a Java file
Book book = new Book()

类的构造函数和属性

让我们来看看如何为我们的类添加构造函数和属性。但首先,让我们看看Java中的一个典型类。

/* Java */
public class Book  {
    private String title;
    private Long isbn;
    public Book(String title, Long isbn) {
        this.title = title;
        this.isbn = isbn;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public Long getIsbn() {
        return isbn;
    }
    public void setIsbn(Long isbn) {
        this.isbn = isbn;
    }
}

看一下我们上面的Book 模型类,我们有以下内容。

  • 两个字段:titleisbn
  • 一个构造函数
  • 这两个字段的获取器和设置器(幸运的是,IntelliJ IDEA可以帮助我们生成这些方法)

现在让我们来看看如何用Kotlin来写前面的代码。

/* Kotlin */
class Book {
    var title: String
    var isbn: Long

    constructor(title: String, isbn: Long) {
        this.title = title
        this.isbn = isbn
    }
}

一个相当整洁的类!我们现在已经把代码行数从20行减少到了9行。constructor() 函数在Kotlin中被称为二级构造函数 这个构造函数相当于我们在实例化一个类时调用的Java构造函数。

在Kotlin中,没有你可能熟悉的字段的概念;相反,它使用 "属性 "的概念。例如,我们有两个用var 关键字声明的可变(读写)属性:titleisbnBook 类中。(如果你需要复习一下Kotlin中的变量,请访问本系列中的第一篇文章。变量、基本类型和数组)。

一个令人惊讶的事情是,这些属性的getters和setters是由Kotlin编译器自动生成的。请注意,我们没有为这些属性指定任何可见性修改器,所以默认情况下,它们是公开的。换句话说,它们可以从任何地方被访问。

让我们来看看Kotlin中同一个类的另一个版本。

class Book constructor(title: String, isbn: Long) {
    var title: String
    var isbn: Long

    init {
       this.title = title
       this.isbn = isbn
    }
}

在这段代码中,我们删除了二级构造函数。取而代之的是,我们在类头中声明了一个叫做初级构造函数的构造函数。主构造函数没有任何地方可以放置代码块,所以我们利用init 修改器来初始化来自主构造函数的传入参数。请注意,当类实例被创建时,init 代码块会立即执行。

正如你所看到的,我们的代码仍然有很多模板。让我们进一步减少它。

class Book constructor(var title: String, var isbn: Long)

我们的Book 类现在只有一行代码了。这真是太酷了!请注意,在主构造函数的参数列表中,我们直接在主构造函数中用var 关键字定义了我们的可变属性:titleisbn

我们还可以在构造函数中为任何一个类的属性添加默认值。

class Book constructor(var title: String = "default value", var isbn: Long)

事实上,我们也可以省略constructor 关键字,但前提是它没有任何可见性修改器(public,private, 或protected )或任何注释。

class Book (var title: String = "default value", var isbn: Long)

我必须说,这是一个非常整洁的类!

我们现在可以像这样创建一个类的实例。

val book = Book("A Song of Ice and Fire", 9780007477159)
val book2 = Book(1234) // uses the title property's default value

访问和设置属性

在Kotlin中,我们可以通过类对象book ,接着是点状分隔符. ,然后是属性名称title ,来获得一个属性。这种访问属性的简明风格被称为属性访问语法。 换句话说,在Kotlin中,我们不必像在Java中那样调用属性getter方法来访问或调用setter来设置一个属性。

println(book.title) // "A Song of Ice and Fire"

因为isbn 属性是用var 关键字声明的(可读可写),我们也可以用赋值运算符= 来改变属性值。

book.isbn = 1234
println(book.isbn) // 1234

让我们看看另一个例子。

class Book (
    var title: String, 
    val isbn: Long
)

val book = Book("A Song of Ice and Fire", 9780007477159)
book.isbn = 1234 // error: read-only property
book.title = "Things Fall Apart" // reassigned title with value

在这里,我们通过使用val 关键字,将isbn 参数更新为不可变的(只读)。我们实例化了一个类的实例book ,并将title 属性重新赋值为 "Things Fall Apart"。请注意,当我们试图将isbn 属性值重新分配给1234 ,编译器抱怨了。这是因为该属性是不可改变的,它是用val 关键字定义的。

Java的互操作性

请注意,通过在主构造函数中用var 修饰符声明一个参数,Kotlin编译器(在幕后)已经帮助我们生成了两个属性访问器:getter和setter。如果你使用val ,它将只生成getter。

/* Kotlin */
class Book (
    var title: String, 
    val isbn: Long
)

这意味着Java调用者可以通过分别调用属性的setter或getter方法简单地获取或设置属性字段。记住,这取决于用于定义Kotlin属性的修改器:varval

/* Java */
Book book = new Book("A Song of Ice and Fire", 9780385474542)
println(book.getTitle()) // "A Song of Ice and Fire"
book.setTitle("Things Fall Apart") // sets new value
println(book.getTitle()) // "Things Fall Apart"

book.getIsbn() // 9780385474542
book.setIsbn(4545454) // won't compile

自定义获取器和设置器

在本节中,我将向你展示如何在Kotlin中为一个属性创建自定义访问器(getter和setter),如果你想的话。如果你想在设置一个类属性之前验证或核实一个值,创建一个自定义的setter是很有用的。而当你想改变或修改应该返回的值时,一个自定义的属性获取器会很有用。

创建一个自定义设置器

因为我们想为一个属性创建我们自己的自定义getter或setter,所以我们必须在类的主体中定义该属性,而不是在构造函数的头部。

class Book (val isbn: Long) {
    var title = "default value"
}

这就是为什么我们把可变(可读可写)的title 属性移到类的主体中,并给它一个默认值(否则就不能编译)。

class Book (val isbn: Long) {
    var title = "default value"
    set(value) {
        if (!value.isNotEmpty()) {
            throw IllegalArgumentException("Title must not be empty")
        }
        field = value
    }
}

你可以看到我们在属性定义下面为title 定义了我们自己的setter方法set(value) -注意你不能修改这个set() 方法签名,因为这是编译器期望的自定义属性设置函数。

传递给set 方法的参数value 代表用户分配给该属性的实际值--如果你愿意,你可以改变参数名称,但value 更为可取。我们通过检查该值是否为空来验证value 。如果是空的,就停止执行并抛出一个异常;否则,就把这个值重新分配给一个特殊的field 变量。

这个在set 方法里面的特殊field 变量字段是属性的支持字段的别名--支持字段只是一个字段,当你想修改或使用该字段数据时,属性会使用该字段。与value 不同,你不能重命名这个特殊的field 变量。

创建一个自定义获取器

在Kotlin中为一个属性创建一个自定义的getter是非常容易的。

class Book (val isbn: Long) {
    var title = "default value"
    //... set method
    get() {
        return field.toUpperCase()
    }
}

get 方法中,我们只需返回一个修改过的field- 在我们的例子中,我们返回的是大写的书名。

val book = Book(9780007477159)
book.title = "A Song of Ice and Fire"
println(book.title) // "A SONG OF ICE AND FIRE"
println(book.isbn) // 9780007477159

注意,每次我们给title 属性设置一个值时,它的set 方法块就会被执行--每次我们检索它时,get 方法也是如此。

如果你想了解Kotlin类的成员函数(定义在类、对象或接口内的那种函数),请访问本系列中的More Fun With Functions帖子。

关于构造函数的更多信息

正如我前面所讨论的,在Kotlin中我们有两种类型的构造函数:主函数和次函数。我们可以在一个类中自由地组合这两种类型--正如你在下面的例子中所看到的。

class Car(val name: String, val plateNo: String) {
    var new: Boolean = true

    constructor(name: String, plateNo: String, new: Boolean) : this(name, plateNo) {
        this.new = new
    }
}

请注意,我们不能在二级构造函数中声明属性,就像我们对一级构造函数所做的那样。如果我们想这样做,我们必须在类的主体中声明它,然后在二级构造函数中初始化它。

在上面的代码中,我们为类Car (记住,new 在Kotlin中不是一个关键字)设置了new 属性的默认值--然后我们可以使用二级构造函数来改变它。在Kotlin中,每个二级构造函数都必须调用一级构造函数,或者调用另一个调用一级构造函数的二级构造函数--我们使用this 这个关键字来实现。

还要注意的是,我们可以在一个类里面有多个二级构造函数。

class Car(val name: String, val plateNo: String) {
    var new: Boolean? = null
    var colour: String = ""

    constructor(name: String, plateNo: String, new: Boolean) : this(name, plateNo) {
        this.new = new
    }

    constructor(name: String, plateNo: String, new: Boolean, colour: String ) :
            this(name, plateNo, new) {
        this.colour = colour
    }
}

如果一个类扩展了一个超类,那么我们可以使用super 关键字(类似于Java)来调用超类构造函数(我们会在以后的文章中讨论Kotlin的继承问题)。

// directly calls primary constructor
val car1 = Car("Peugeot 504", "XYZ234")
// directly calls 1st sec. constructor
val car2 = Car("Peugeot 504", "XYZ234", false)
// directly calls last sec. constructor
val car3 = Car("Peugeot 504", "XYZ234", false, "grey") 

正如我前面所说,为了让我们在一个类的构造函数中明确包含可见性修改器,我们必须包含constructor 关键字--默认情况下,构造函数是公共的。

class Car private constructor(val name: String, val plateNo: String) {
//...

在这里,我们把构造函数变成了私有的--这意味着用户不能直接使用其构造函数来实例化一个对象。如果你想让用户调用另一个方法(工厂方法)来间接地创建对象,这可能很有用。

构造函数中的代码执行顺序和其他趣闻

我们知道,Kotlin中类的主构造函数不能有任何代码。然而,我们可以将我们想要执行的初始化代码放在初始化块中,这些初始化块是用init 关键字标记的。你可以在一个类体内有多个初始化块。它们将按照创建的顺序被执行。

你在主构造函数中传递的参数将在类体内的任何地方可用。例如,你可以在初始化块中使用它们,或者为其他属性设置值。正如你现在已经知道的,主构造函数中的参数可以在它们前面有valvar 关键字,以表示它们是只读的还是可变的。

有一件事可能会让初学者感到困惑,那就是二级构造函数的情况并非如此。你不能像在一级构造函数中那样在二级构造函数定义中使用valvar 关键字。这是因为这样做会为该类创建一个属性,而让一个对象的属性根据用来创建它的构造函数而变化,不是一个好主意。

一个类中的二级构造函数需要委托给一级构造函数,这发生在二级构造函数体的任何代码执行之前。由于初始化块内的代码被认为是主构造函数的一部分,它总是在二级构造函数主体内的代码之前被执行。如果初始化块被定义在二级构造函数之后,这并不重要。

import java.time.LocalDate;

class Person(val name: String, val birthYear: Int) {

    val currentDate: LocalDate = LocalDate.now()
    var age: Int = currentDate.getYear() - this.birthYear
    var old: Boolean = false
    var married: Boolean? = null
    var alive: Boolean? = null

    init {
        println("Executing first initializer block for ${this.name}.")
        if(this.age > 65) {
            this.old = true
            println("${this.name} is ${this.age} years old.")
        }
    }
 
    constructor(name: String, birthYear: Int, married: Boolean) : this(name, birthYear) {
        println("Calling first constructor for ${this.name}.")
        this.married = married
    }
 
    constructor(name: String, birthYear: Int, married: Boolean, alive: Boolean) : this(name, birthYear, married) {
        println("Calling second constructor for ${this.name}.")            
        this.alive = alive
        if(this.alive == true && this.married == true) {       
            println("${this.name} is alive and married inside second constructor!")
        }
    }

    init {
        println("Executing second initializer block for ${this.name}.")
        if(this.alive == null && this.married == null) {       
            println("Cannot confirm if ${this.name} is alive and married inside second initializer!")
        }
    }
}

fun main() {
    /*
    Executing first initializer block for Amanda.
    Amanda is 77 years old.
    Executing second initializer block for Amanda.
    Cannot confirm if Amanda is alive and married inside second initializer!
    Calling first constructor for Amanda.
    */
    val personOne = Person("Amanda", 1945, true)


    /*
    Executing first initializer block for Alex.
    Executing second initializer block for Alex.
    Cannot confirm if Alex is alive and married inside second initializer!
    Calling first constructor for Alex.
    Calling second constructor for Alex.
    Alex is alive and married inside second constructor!
    */
    val personTwo = Person("Alex", 1980, true, true)
}

在上面的代码中,我们传递三个参数来实例化一个Person "Amanda"。带有三个参数的二级构造函数定义调用了带有this(name, birthYear) 的一级构造函数。因为初始化块是主构造函数的一部分,所以它们接下来被执行,而且是按照我们定义的顺序执行。

当我们实例化第二个Person "Alex "时,我们经历了同样的过程。这一次,第二个二级构造函数调用了第一个二级构造函数,后者又调用了一级构造函数。你应该注意到,即使我们在实例化过程中把marriedalive 的值作为true ,但这些属性只有在第二个二级构造函数中的代码被执行时才成为true 。因此,在执行第二个初始化块时,它们仍然是null 。这就是为什么我们无法确认亚历克斯在此时是否结婚并活着。

一旦所有的初始化器代码被执行,Kotlin就会执行二级构造函数里面的代码。一旦我们执行了第二个二级构造函数里面的代码,我们就可以确认亚历克斯实际上既活着又已婚。

2.Any和Nothing类型

在Kotlin中,类型层次结构中最顶层的类型被称为Any 。这等同于Java的Object 类型。这意味着Kotlin的所有类都明确地继承自Any 类型,包括String,Int,Double, 等等。Any 类型包含三个方法。equals,toString, 和hashcode

我们还可以在总是返回异常的函数中利用Kotlin中的Nothing 类--换句话说,用于那些不正常终止的函数。当一个函数返回Nothing ,那么我们知道它将会抛出一个异常。在Java中不存在这种等价的类型。

fun throwException(): Nothing {
    throw Exception("Exception message)
}

在单元测试中测试错误处理行为时,这一点会很有用。

3.可见性修饰符

可见性修改器帮助我们限制我们的API对公众的可访问性。我们可以为我们的类、接口、对象、方法或属性提供不同的可见性修改器。Kotlin为我们提供了四个可见性修改器。

公共

这是默认的,任何有这个修饰符的类、函数、属性、接口或对象都可以从任何地方访问。

私有的

被声明为private 的顶层函数、接口或类只能在同一文件内被访问。

在一个类、对象或接口内声明为private 的任何函数或属性,只能对同一类、对象或接口的其他成员可见。

class Account {
    private val amount: Double = 0.0
}

受保护的

protected 修改器只能应用于类、对象或接口内的属性或函数--它不能应用于顶层函数、类或接口。带有这个修饰符的属性或函数只能在定义它的类和任何子类中访问。

内部

在有模块(Gradle或Maven模块)的项目中,在该模块内声明的、用internal 修饰符指定的类、对象、接口或函数,只能在该模块内访问。

internal class Account {
    val amount: Double = 0.0
}

4.智能投递

Casting指的是将另一种类型的对象转换为另一种对象类型。例如,在Java中,我们使用instanceof 操作符来确定一个特定的对象类型是否属于另一种类型,然后再进行投掷。

/* Java */
if (shape instanceof Circle) {
    Circle circle = (Circle) shape;
    circle.calCircumference(3.5); 
}

正如你所看到的,我们检查了shape 实例是否是Circle ,然后我们必须明确地将shape 引用转换为Circle 类型,以便我们可以调用circle 类型的方法。

Kotlin的另一个了不起之处在于它的编译器在涉及到投递时的聪明程度。现在让我们看看Kotlin的一个版本。

/* Kotlin */
if (shape is Circle) {
    shape.calCircumference(3.5)
}

很整洁啊!编译器很聪明,知道只有当shape 对象是Circle的实例时,if 块才会被执行--所以铸造机制已经在引擎盖下为我们完成。现在我们可以很容易地在if 块内调用Circle 类型的属性或函数。

if (shape is Circle && shape.hasRadius()) {
    println("Circle radius is {shape.radius}")
}

在这里,只有当第一个条件是trueif 头部的&& 之后的最后一个条件才会被调用。如果shape 不是Circle ,那么最后一个条件就不会被评估。

5.明确投射

在Kotlin中,我们可以使用as 操作符(或不安全的投掷操作符)将一个类型的引用显式地投掷到另一个类型。

val circle = shape as Circle
circle.calCircumference(4)

如果显式铸造操作是非法的,请注意,将抛出一个ClassCastException 。为了防止在投递时抛出异常,我们可以使用安全投递操作符(或可忽略投递操作符)as?

val circle: Circle? = shape as? Circle

as? 操作符将尝试投递到预定的类型,如果值不能被投递,它将返回null ,而不是抛出一个异常。请记住,类似的机制在本系列中的Nullability、Loop和Condition一节中讨论过。请阅读那里的内容进行复习。

6.对象

Kotlin中的对象比Java对象更类似于JavaScript对象。请注意,Kotlin中的对象不是一个特定类的实例!

对象与类非常相似。下面是Kotlin中对象的一些特征。

  • 它们可以有属性、方法和一个init 块。
  • 这些属性或方法可以有可见性修改器。
  • 它们不能有构造函数(主要或次要)。
  • 它们可以扩展其他类或实现一个接口。

现在让我们来探讨一下如何创建一个对象。

object Singleton {
    
    fun myFunc(): Unit {
        // do something
    }
}

我们把object 关键字放在我们要创建的对象的名字前面。事实上,当我们在Kotlin中使用object 构造创建对象时,我们正在创建单子,因为一个对象只存在一个实例。当我们讨论对象与Java的互操作性时,你会了解更多这方面的信息。

单子是一种软件设计模式,它保证一个类只有一个实例,并且由该类提供一个全局的访问点。任何时候多个类或客户端请求该类,他们都会得到该类的同一个实例。你可以查看我关于Java中的单体模式的文章,以了解更多关于它的信息。

你可以在项目的任何地方访问这个对象或单子--只要你导入了它的包。

Singleton.myFunc()

如果你是一个Java程序员,这就是我们通常创建单子的方式。

public class Singleton  {
 
    private static Singleton INSTANCE = null;
 
    // other instance variables can be here
     
    private Singleton() {};
 
    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return(INSTANCE);
    }
     
    // other instance methods can follow 
}

正如你所看到的,使用Kotlin的object 结构使得创建单子变得简洁和容易。

Kotlin的对象也可以用来创建常量。通常在Java中,我们在一个类中创建常量,把它变成一个公共的静态final字段,比如这样。

public final class APIConstants {
   
   public static final String baseUrl = "https://www.myapi.com/";

   private APIConstants() {}
}

Java中的这段代码可以更简洁地转换为Kotlin代码。

package com.chike.kotlin.constants

object APIConstants {
    val baseUrl: String = "https://www.myapi.com/"
}

在这里,我们在包com.chike.kotlin.constants 中用一个属性baseUrl 来声明常量APIConstants 。在引擎盖下,为我们创建了一个Java私有静态最终成员baseUrl ,并用字符串URL进行初始化。

要在Kotlin的另一个包中使用这个常量,只需导入该包。

import com.chike.kotlin.constants.APIConstants

APIConstants.baseUrl

Java的互操作性

Kotlin在引擎盖下将一个对象转换为一个最终的Java类。这个类有一个私有的静态字段INSTANCE ,它持有该类的一个实例(一个单子)。下面的代码显示了用户如何简单地从Java中调用一个Kotlin对象。

/* Java */
Singleton.INSTANCE.myFunc()

在这里,一个名为Singleton 的Java类被生成,它有一个公共静态最终成员INSTANCE ,包括一个公共最终函数myFunc()

为了使Kotlin中的对象函数或属性成为生成的Java类的静态成员,我们使用了@JvmStatic 注解。下面是如何使用它的。

object Singleton {
    
    @JvmStatic fun myFunc(): Unit {
        // do something
    }
}

myFunc()通过对@JvmStatic 注解的应用,编译器将它变成了一个静态函数。

现在,Java调用者可以像正常的静态成员调用一样调用它。注意,使用INSTANCE 静态字段来调用成员仍然有效。

/* Java */
Singleton.myFunc()

7.同伴对象

现在我们已经了解了Kotlin中的对象是什么,让我们深入了解另一种对象,称为同伴对象。

因为Kotlin不支持像Java中的静态类、方法或属性,所以Kotlin团队为我们提供了一个更强大的替代方案,叫做同伴对象。伴生对象基本上是一个属于一个类的对象--这个类被称为对象的伴生类。这也意味着,我提到的对象的特性也适用于同伴对象。

创建一个同伴对象

与Java中的静态方法类似,同伴对象并不与类实例相关,而是与类本身相关--例如,工厂静态方法,它的工作是创建一个类实例。

class Person private constructor(var firstName: String, var lastName: String) {

    companion object {
        fun create(firstName: String, lastName: String): Person = Person(firstName, lastName)
    }
}

在这里,我们把构造函数private,这意味着类外的用户不能直接创建一个实例。在我们的同伴对象块中,我们有一个函数create() ,它创建了一个Person 对象并返回它。

调用一个同伴对象函数

companion 对象的实例化是懒惰的。换句话说,它只有在第一次需要时才会被实例化。当创建 类的实例或访问 对象的成员时, 对象的实例化就会发生。companion companion companion

让我们看看如何在Kotlin中调用一个同伴对象函数。

val person = Person.create("Cersei", "Lannister")
println(person.firstName) // prints "Cersei"

正如你所看到的,这就像在Java中正常调用一个静态方法一样。换句话说,我们只是调用类,然后调用成员。请注意,除了函数之外,我们还可以在我们的同伴对象中设置属性。

class Person private constructor(var firstName: String, var lastName: String) {
    init {
        count++
    }
    
    companion object {
        var count: Int = 0
        fun create(firstName: String, lastName: String): Person = Person(firstName, lastName)
        
        init {
            println("Person companion object created")
        }
    }

}

还要注意的是,companion 类可以不受限制地访问其同伴对象中声明的所有属性和函数,而同伴对象不能访问类成员。我们可以在一个companion 对象中设置一个init 代码块--当同伴对象被创建时,这个代码块会被立即调用。

Person.create("Arya", "Stark")
Person.create("Daenerys", "Targaryen")
println(Person.count)

执行上述代码的结果将是。

Person companion object created
2

记住,一个类companion 对象只能有一个实例存在。

我们还可以自由地为我们的同伴对象提供一个名字。

// ...
companion object Factory {
    var count: Int = 0
    fun create(firstName: String, lastName: String): Person = Person(firstName, lastName)
}
// ... 

在这里,我们给它起了一个名字,叫做Factory 。然后我们可以在Kotlin中这样调用它。

Person.Factory.create("Petyr", "Baelish")

这种方式很啰嗦,所以坚持使用之前的方式更合适。但在从Java中调用一个同伴对象的函数或属性时,这可能会很方便。

正如我前面所说,像对象一样,同伴对象也可以包括属性或函数,实现接口,甚至扩展一个类。

interface PersonFactory {
    fun create(firstName: String, lastName: String): Person
}

class Person private constructor(var firstName: String, var lastName: String) {
    
    companion object : PersonFactory {
        override fun create(firstName: String, lastName: String): Person {
            return Person(firstName, lastName)
        }
    }
}

在这里,我们有一个接口PersonFactory ,只有一个create() 函数。看看我们新修改的companion 对象,它现在实现了这个接口(你将在后面的文章中学习Kotlin中的接口和继承)。

Java的互操作性

在引擎盖下,同伴对象的编译方式与Kotlin对象的编译方式类似。在我们自己的例子中,为我们生成了两个类:一个最终的Person 类和一个内部的静态最终类Person$Companion

Person 类包含一个名为Companion的最终静态成员--这个静态字段是Person$Companion 内类的一个对象。Person$Companion 内层类也有自己的成员,其中一个是一个名为create() 的公共最终函数。

注意,我们没有给我们的同伴对象一个名字,所以生成的静态内类是Companion 。如果我们给它一个名字,那么生成的名字就是我们在Kotlin中给它的名字。

/* Java */
Person person = Person.Companion.create("Jon", "Snow");

在这里,Kotlin中的同伴对象没有名字,所以我们使用编译器提供的名字Companion ,以便Java调用者调用它。

应用在同伴对象成员上的@JvmStatic 注解,其作用与普通对象的作用类似。

伙伴对象的扩展

与扩展函数如何扩展类的功能类似,我们也可以扩展一个同伴对象的功能。(如果你想复习一下Kotlin的扩展函数,请访问本系列中的高级函数教程)。

class ClassA {

    companion object  {

    }
}

fun ClassA.Companion.extFunc() {
    // ... do implementation
}

ClassA.extFunc()

在这里,我们在伴生对象ClassA.Companion 上定义了一个扩展函数extFunc() 。换句话说,extfunc() 是同伴对象的一个扩展。那么我们就可以像调用同伴对象的一个成员函数一样调用这个扩展函数(它不是!)。

在幕后,编译器将创建一个静态实用函数extFunc() 。作为这个实用函数的参数的接收者对象是ClassA$Companion

总结

在本教程中,你了解了Kotlin的基本类和对象。我们涵盖了以下关于类的内容。

  • 类的创建
  • 构造函数
  • 属性
  • 可见性修改器
  • 智能铸造
  • 显式铸造

此外,你还了解了Kotlin中的对象和同伴对象如何轻松地取代你在Java中编码的静态方法、常量和单项。但这还不是全部!关于Kotlin中的类,还有更多的东西需要学习。在下一篇文章中,我将向你展示Kotlin在面向对象编程方面的更多很酷的功能。很快就会见到你了

要了解更多关于Kotlin语言的信息,我推荐你访问Kotlin文档。或者看看我们在Envato Tuts+上的其他一些Android应用开发的帖子!

本帖由Nitish Kumar提供更新。Nitish是一名网络开发人员,在各种平台上创建电子商务网站方面有丰富的经验。他的空闲时间都花在那些让他的日常生活更轻松的个人项目上,或者和朋友们一起在傍晚的时候散步。