Kotlin入门(三)——类、对象、接口

本章内容包括:java

  • 类的基本要素
  • 类的继承结构
  • 修饰符
  • 接口

0. 前言

上一篇的末尾,咱们提到了Kotlin的包和导入。api

本来我是准备把这篇的内容也放在上一篇的,可是后来一想,这张的内容会颇有点多,放进去的话可能会致使上一篇太大了,因此就单独分红一篇了。ide

在说类以前,咱们先来看下一个类的Java版和Kotlin版的对比,这个会一会儿就让你对Kotlin感兴趣。函数

咱们如今有一个需求,须要定义一个JavaBean类Person,这个类中包含这我的的姓名、电话号码以及地址。工具

咱们先来看下Java的实现:ui

public class Person {
    private String firstName;
    private String lastName;
    private String telephone;
    private String address;

    public Person(String firstName, String lastName, String telephone, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.telephone = telephone;
        this.address = address;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}
复制代码

这是一个很基本的Java类,咱们定义了四个private属性,而后给定了一个构造函数,而后对每一个属性都给了get和set方法。this

相信你们学Java必定都写过这个类。可是咱们想一想,就写一个功能这么简单的类,Java却须要咱们写这么多内容,有的同窗会说:Idea和Eclipse都不是提供了自动生成代码的工具吗。可是若是你看了Kotlin的实现,必定会以为连自动生成工具都麻烦:spa

class Person(
    val firstName: String,
    val lastName: String,
    val telephone: String,
    val address: String
)
复制代码

对,你没有看错,Kotlin的类就是这么的简单:.net

  • 因为Kotlin的属性默认的修饰符就是public。可是因为咱们这个方法设置成了val,因此除了构造方法外,无法对这个属性的值进行更改。可是从Kotlin编译后会自动将val的属性转为private final String firstName;var的属性转为private String firstName;code

  • 因为Kotlin会自动为属性生成getset方法,因此不必去显式的写getset方法,除非你须要自定义的getset方法。可是因为这个类的属性都是val,因此只会生成get方法。

  • Kotlin的默认构造方法是直接写在类名后面的

接下来咱们就把这个代码进行分解,逐步来说解Kotlin的类。

1. 类与继承

1.1 类

与Java相似,Kotlin也是使用class关键字来表示类。

class Person() {}
复制代码

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头和类体都是可选的,一个类若是没有类体,能够省略花括号:

class Person()
复制代码

1.2 构造函数

1.2.1 主构造函数

Kotlin的一个类能够有一个主构造函数以及一个或者多个次构造函数。主构造函数是类头的一部分,跟在类名以后:

class Person constructor(val name: String){}
复制代码

若是主构造函数没有任何注解或者可见性修饰符,能够省略这个constructor关键字。

class Person(val name: String){}
复制代码

主构造方法主要有两种目的:代表构造方法的参数,以及定义使用这些参数初始化的属性。

可是主构造方法不容许直接有代码块,因此若是须要在主构造方法中添加初始化代码,能够放到init关键字的代码块中:

class Person(val _name: String) {
    val name: String
    init {
        name = _name
        println(name)
    }
}
复制代码

可是同时,这个例子中,_name赋值给name,这个语句能够放在name的定义中去,因此能够改为:

class Person(val _name: String) {
    val name: =  _name
    init {
        println(name)
    }
}
复制代码

可是,若是主构造方法须要添加注解或者修饰符的话,这个constructor是不能省略的:

class Person private constructor(val name: String) {
}
复制代码

1.2.2 次构造函数

类也能够单纯的声明次构造方法而不声明主构造方法:

class Person {
    val name: String

    constructor(_name: String) {
        name = _name
        println(name)
    }
}
复制代码

若是类有一个主构造函数,每一个次构造函数须要委托给主构造函数, 能够直接委托或者经过别的次构造函数间接委托。委托到同一个类的另外一个构造函数用 this 关键字便可:

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<>()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}
复制代码

值的注意下的是,初始化语句(init)实际上会成为主构造方法的一部分。委托给主构造函数会做为次构造函数的第一条语句,所以全部初始化块与属性初始化器中的代码都会在次构造函数体以前执行。即便该类没有主构造函数,这种委托仍会隐式发生,而且仍会执行初始化块。 简单点来讲就是,无论你有没有主构造方法,只要你有次构造方法,而且有init语句,他都会在执行次构造方法的函数体内的代码以前,先去执行init语句:

fun main() {
    val person = Person("1")
}

class Person {
    val name: String

    constructor(_name: String) {
        name = _name
        println(name)
    }

    init {
        println("init: 1")
    }
}

/* 输出结果为 init: 1 1 */
复制代码

1.3 类的实例

回忆一下上一篇讲基本类型:

val one = 1 // Int
val threeBillion = 3000000000 // Long
复制代码
  • 首先咱们说过Kotlin和Java不同,全部的东西都是对象,包括基本类型。因此说咱们建立的Kotlin的Int型参数,实际上就是new了一个Int这个类的对象
  • 其次Kotlin建立对象是经过valvar关键字的
  • 最后就是一点你们应该都发现了,Kotlin是没有new这个关键字的

因此从上面咱们能够推出若是在Kotlin中建立一个对象:

  • 首先,咱们须要根据对象须要的场景,选择究竟是val的对象仍是var的对象;
  • 而后写对象名;
  • 紧跟着对象名的就是对象的类型,可是因为Kotlin有类型推断,因此此处这个显式声明类型能够省略,由于他能够经过等号右边给定的内容自动推断出该对象的类型(此处暂时先不考虑lateinit或者其余的操做)
  • 以后就能够写等号=
  • 等号右边就是咱们须要赋值给这个对象的初始化语句了,可是Kotlin没有new关键字,因此是直接写,不须要加new
val person1: Person = Person("1") // Kotlin没有 new 关键字

val person2 = Person("1") // 因为等号右边已经给出了具体的内容,因此能够省略掉显式的指定类型
复制代码

固然也有特殊状况:

  • 使用lateinit关键字
lateinit var person3: Person
复制代码

在这种状况下必须显式的指定变量类型,由于使用了lateinit关键字,能够延迟初始化,可是从如今开始,直到初始化,期间若是使用了这个变量,运行后就会报错lateinit property person3 has not been initialized

  • 在方法或者类中定义变量
fun main() {
    val person4: Person
    println(person4) // 此时IDE就会报红,Variable 'person4' must be initialized
    person4 = Person("4")
    println(person4)
}
复制代码

在方法中或者类中还能够这样,先不初始化,先定义变量,可是此时必须显式指出其类型,而且在初始化以前都不可以使用变量,若是使用了,在编译前也就是还在编辑时,IDE就会报红,Variable 'person4' must be initialized。可是一旦初始化以后就能够正常使用。

1.4 继承

1.4.1 ObjectAnyextends:

咱们都知道,Java中存在着一个基类Object,全部的对象都会继承自这个类,哪怕你本身建立的对象没有指明具体继承自哪一个类,可是Java会让他继承自Object类。而这个类里面也有一些每一个类必有的方法如getClass()hashCode()equals()toString()等一系列方法。

一样的,Kotlin也有这样的基类,只不过叫作Any。可是不一样的是Kotlin的Any只有三个方法:hashCode()equals()toString()

而在Java中,想要继承某一个类的话,就须要在这个类的后面用extends关键字 + 超类名的方法去指明这个类继承自那个类:

class Staff extends Person{
}
复制代码

而在Kotlin中,就没有extends这个关键字了,取而代之的是咱们的老朋友:

open class Person(val name: String)
class Staff(name: String) : Person(name) 
复制代码

这个就表明了Staff类继承自Person类。同时基类(Person)必须得被open修饰符修饰,由于Kotlin默认全部的类都是final的,因此不能被继承,因此就须要open修饰符修饰它。

而且若是派生类有一个主构造函数,其基类能够(而且必须) 用派生类主构造函数的参数就地初始化。

若是派生类没有主构造函数,那么每一个次构造函数必须使用super关键字初始化其基类型,或委托给另外一个构造函数作到这一点。 注意,在这种状况下,不一样的次构造函数能够调用基类型的不一样的构造函数:

// 代码很是不规范,仅做为例子参考
open class Person {
    val name: String
    var address: String = ""
    constructor(_name: String) {
        name = _name
    }
    constructor(_name: String, _address: String) {
        name = _name
        address = _address
    }
}

class Staff : Person {
    constructor(name: String) : super(name)
    constructor(name: String, address: String) : super(name, address)
}
复制代码

1.4.2 override

覆盖方法

和Java同样,Kotlin也是经过override关键字来标明覆盖,只不过不一样的是Java是@override注解而Kotlin是override修饰符。

open class Person(val name: String) {
    open fun getName() {
        println("这是$this, name: $name")
    }
}

class Staff(name: String) : Person(name) {
    override fun getName() {
        println("这是$this, name: $name")
    }
}
复制代码

咱们能够看到Staff继承自Person并重写了getName()方法。 此时必须在Staff重写的getName()方法前加上override修饰符,不然编译器会报错。

同时,与继承类时同样,Kotlin默认方法也是final的,若是想让这个方法被重写,就须要加上open关键字。可是重写后的方法,也就是有override修饰的方法,默认是开放的,可是若是你想让他再也不被重写,就须要手动添加final修饰符:

class Staff(name: String) : Person(name) {
    final override fun getName() {
        println("这是$this, name: $name")
    }
}
复制代码

覆盖属性

这个就是Kotlin有可是Java没有的了。和覆盖方法同样,也就是在须要覆盖的属性前面加上override

open class Person {
    open val name = "123"
}

class Staff : Person() {
    override val name = "2"
}
复制代码

同时你可使用var属性去覆盖一个val的属性,可是反过来就不行了。由于var默认会有get()set()方法,而val只有get()方法,若是用val去覆盖var,那么varget()方法会没法处理。

1.4.3 初始化顺序

在构造派生类的新实例的过程当中,第一步完成其基类的初始化(在以前只有对基类构造函数参数的求值),所以发生在派生类的初始化逻辑运行以前。

open class Base(val name: String) {

    init { println("Initializing Base") }

    open val size: Int = 
        name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
    name: String,
    val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

fun main() {
    println("Constructing Derived(\"hello\", \"world\")")
    val d = Derived("hello", "world")
}
复制代码

运行结果是:

Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
Initializing size in Base: 5
Initializing Derived
Initializing size in Derived: 10
复制代码

1.4.5 调用超类实现

子类能够经过super关键字访问超类中的内容:

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }

    val fillColor: String get() = super.borderColor
}
复制代码

可是若是在一个内部类中访问外部类的超类的内容,能够经过外部类名限定的super关键字super@Outer来实现:

class FilledRectangle: Rectangle() {
    fun draw() { /* …… */ }
    val borderColor: String get() = "black"
    
    inner class Filler {
        fun fill() { /* …… */ }
        fun drawAndFill() {
            super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所实现的 borderColor 的 get()
        }
    }
}
复制代码

1.4.6 覆盖规则

在 Kotlin 中,实现继承由下述规则规定:若是一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其本身的实现(也许用继承来的其中之一)。 为了表示采用从哪一个超类型继承的实现,咱们使用由尖括号中超类型名限定的super,如super<Base>

open class Rectangle {
    open fun draw() { /* …… */ }
}

interface Polygon {
    fun draw() { /* …… */ } // 接口成员默认就是“open”的
}

class Square() : Rectangle(), Polygon {
    // 编译器要求覆盖 draw():
    override fun draw() {
        super<Rectangle>.draw() // 调用 Rectangle.draw()
        super<Polygon>.draw() // 调用 Polygon.draw()
    }
}
复制代码

1.5 抽象类

Kotlin中的抽象类用abstract关键字。抽象成员能够在本类中不用实现。

同时不用说的就是,在抽象类中不须要使用open标注。

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}
复制代码

2. 属性和字段

属性的定义咱们已经在前面说到过了,主要是valvar两个关键字,而如今首先要说的,就是GetterSetter

2.1 GetterSetter

声明一个属性完整的语法是:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
复制代码

其中,property_initializergettersetter都是可选的,而且若是类型能够从property_initializer推断出来,则也是可选的。

而对于不论是val仍是var,若是你访问这个属性的时候就直接返回这个属性的值的话,getter是能够省略的,而若是你想返回的时候后作些操做的话,就能够自定义getter(比方说咱们如今有一个Person类,里面有nameage以及isAdult三个属性,其中isAdult咱们须要去设置他的get方法,当age大于等于18的时候就返回true,不然返回false):

class Person(_name: String, _age: Int) {
    val name: String = _name
    val age: Int = _age
    val isAdult: Boolean
        get() = age >= 18
}
复制代码

而同时咱们须要在设置这我的的age的时候,作一个判断,若是输入的值小于0的话,就抛异常,不然才更改age的值。这个时候咱们就须要自定义set方法了:

class Person(_name: String, _age: Int) {
    val name: String = _name
    var age: Int = _age
        set(value) {
            if (value <= 0) {
                throw Exception("年龄必须大于0")
            } else {
                field = value
            }
        }
    val isAdult: Boolean
        get() = age >= 18
}

fun main() {
    try {
        val person = Person("314", 18)
        person.age = 0
    } catch (e: Exception) {
        println(e.message)
    }
}
复制代码

运行结果就是年龄必须大于0。可是这块有个小要点,就是属性的set方法在对象初始化的时候是不起做用的,也就是说,若是我给上面这个Person类建立对象的时候,给age传入0或者负数的话:

fun main() {
    try {
        val person = Person("314", 0)
        println(person.age)
    } catch (e: Exception) {
        println(e.message)
    }
}
复制代码

运行结果没有任何异常,输出0

不知道你们注意到了没有,咱们给setter传入的是value,而用field承接了传入的value

其实这个value是咱们自定义的,也就是说set()这个括号里面的名字你能够随便写,只要符合Kotlin命名规范。 可是这个field是不可变的,这个field至关因而this.属性,也就至关因而set的这个值自己,也就是说,若是你想在setter中改变这个属性的值的话,就必须得把最终的值传给fieldfield就至关因而这个属性,而setterthis.属性是没有意义的,你写了的话,IDEA反而会提示你让你改为field

1

2.2 编译器常量

若是只读属性的值在编译器是已知的,就可使用const去修饰将其标记为编译器常量,这种属性须要知足下列要求:

  • 位于顶层或者是object声明 或companion object的一个成员
  • String或原生类型值初始化
  • 没有自定义getter

2.3 延迟初始化属性与变量

通常,属性声明为非空类型就必须得在构造函数中去初始化。可是这样也会不是很方便,例如像Android中的view的对象(TextView、Button等view的对象,须要被findViewById)。在这种状况下,咱们无法去提供一个构造器去让其初始化,这个时候就可使用lateinit修饰符:

class MainActivity : AppCompatActivity() {

    private lateinit var mTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mTextView = findViewById(R.id.textView_base_model)
    }
}
复制代码

在被lateinit修饰的变量被初始化前,若是访问这个变量的话,就会抛一个异常。

3. 接口

在Kotlin中使用interface来定义接口:

interface Food {
    fun cook() {
        // 可选的方法体
    }
    fun eat()
}
复制代码

3.1 实现接口

和Java同样,一个类能够实现多个接口:

class Person : Food, Action {
    override fun eat() {
    }

    override fun walk() {
    }

    override fun play() {
    }
}
复制代码

3.2 接口中的属性

和Java同样,Kotlin的接口中也能够存在属性。

只不过若是想要在Kotlin中定义属性,必须保证这个属性要么是抽象的,要么指定了访问器。

interface Food {
    val isCookFinished: Boolean // 抽象的属性
    val isEatFinished: Boolean // 指定了访问器的属性
        get() = isCookFinished

    fun cook() {
        // 可选的方法体
    }

    fun eat()
}
复制代码

3.3 接口的继承

和类同样,接口也能够继承自另一个接口,能够在父接口的基础上去添加新的方法或者属性。

4. 可见性修饰符

在Kotlin中,主要有4种修饰符:

  • private
  • protected
  • internal
  • public

若是没有指定修饰符,默认是public

4.1 修饰符在包内

咱们以前提到过,Kotlin能够直接直接在顶层声明类、函数和属性。

// 文件名:example.kt
package foo

private fun foo() { …… } // 在 example.kt 内可见

public var bar: Int = 5 // 该属性随处可见
    private set         // setter 只在 example.kt 内可见
    
internal val baz = 6    // 相同模块内可见
复制代码
  • 若是不指定任何修饰符,默认为public,意味着随处可见;
  • 若是声明为private,它只能在声明他的文件内可见;
  • 若是声明为internal,它只能在相同的模块(模块咱们会在本文的4.4讲到)中可见;
  • 顶层中不可以使用protected(理由也很好想到——都没有类,怎么存在子类的概念)

4.2 修饰符在类和接口内

对于在类或者接口内的方法或者属性,咱们四种修饰符均可用:

open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // 默认 public
    
    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a 不可见
    // b、c、d 可见
    // Nested 和 e 可见

    override val b = 5   // “b”为 protected
}

class Unrelated(o: Outer) {
    // o.a、o.b 不可见
    // o.c 和 o.d 可见(相同模块)
    // Outer.Nested 不可见,Nested::e 也不可见
}
复制代码
  • private:只在这个类的内部可见;
  • protected:只在这个类的内部以及他的子类可见;
  • internal:只在这个模块内可见;
  • public:随处可见。

4.3 局部

局部变量、函数和类不能够有可见性修饰符。

4.4 Kotlin中的模块

可见性修饰符internal意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一块儿的一套 Kotlin 文件:

  • 一个 IntelliJ IDEA 模块;
  • 一个 Maven 项目;
  • 一个 Gradle 源集(例外是test源集能够访问maininternal声明);
  • 一次<kotlinc>Ant 任务执行所编译的一套文件。
相关文章
相关标签/搜索