本章內容包括:java
- 類的基本要素
- 類的繼承結構
- 修飾符
- 接口
在上一篇的末尾,咱們提到了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會自動爲屬性生成get
和set
方法,因此不必去顯式的寫get
和set
方法,除非你須要自定義的get
和set
方法。可是因爲這個類的屬性都是val
,因此只會生成get
方法。
Kotlin的默認構造方法是直接寫在類名後面的
接下來咱們就把這個代碼進行分解,逐步來說解Kotlin的類。
與Java相似,Kotlin也是使用class
關鍵字來表示類。
class Person() {}
複製代碼
類聲明由類名、類頭(指定其類型參數、主構造函數等)以及由花括號包圍的類體構成。類頭和類體都是可選的,一個類若是沒有類體,能夠省略花括號:
class Person()
複製代碼
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) {
}
複製代碼
類也能夠單純的聲明次構造方法而不聲明主構造方法:
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 */
複製代碼
回憶一下上一篇講基本類型:
val one = 1 // Int
val threeBillion = 3000000000 // Long
複製代碼
Int
型參數,實際上就是new
了一個Int
這個類的對象val
和var
關鍵字的new
這個關鍵字的因此從上面咱們能夠推出若是在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
。可是一旦初始化以後就能夠正常使用。
Object
和Any
、extends
和:
咱們都知道,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)
}
複製代碼
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
,那麼var
的get()
方法會沒法處理。
在構造派生類的新實例的過程當中,第一步完成其基類的初始化(在以前只有對基類構造函數參數的求值),所以發生在派生類的初始化邏輯運行以前。
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
複製代碼
子類能夠經過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()
}
}
}
複製代碼
在 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()
}
}
複製代碼
Kotlin中的抽象類用abstract關鍵字。抽象成員能夠在本類中不用實現。
同時不用說的就是,在抽象類中不須要使用open標註。
open class Polygon {
open fun draw() {}
}
abstract class Rectangle : Polygon() {
abstract override fun draw()
}
複製代碼
屬性的定義咱們已經在前面說到過了,主要是val
和var
兩個關鍵字,而如今首先要說的,就是Getter
和Setter
。
Getter
和Setter
聲明一個屬性完整的語法是:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
複製代碼
其中,property_initializer
、getter
、setter
都是可選的,而且若是類型能夠從property_initializer
推斷出來,則也是可選的。
而對於不論是val
仍是var
,若是你訪問這個屬性的時候就直接返回這個屬性的值的話,getter
是能夠省略的,而若是你想返回的時候後作些操做的話,就能夠自定義getter
(比方說咱們如今有一個Person
類,裏面有name
、age
以及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
中改變這個屬性的值的話,就必須得把最終的值傳給field
,field
就至關因而這個屬性,而setter
中this.屬性
是沒有意義的,你寫了的話,IDEA反而會提示你讓你改爲field
。
若是隻讀屬性的值在編譯器是已知的,就可使用const
去修飾將其標記爲編譯器常量,這種屬性須要知足下列要求:
object
聲明 或companion object
的一個成員String
或原生類型值初始化getter
通常,屬性聲明爲非空類型就必須得在構造函數中去初始化。可是這樣也會不是很方便,例如像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
修飾的變量被初始化前,若是訪問這個變量的話,就會拋一個異常。
在Kotlin中使用interface來定義接口:
interface Food {
fun cook() {
// 可選的方法體
}
fun eat()
}
複製代碼
和Java同樣,一個類能夠實現多個接口:
class Person : Food, Action {
override fun eat() {
}
override fun walk() {
}
override fun play() {
}
}
複製代碼
和Java同樣,Kotlin的接口中也能夠存在屬性。
只不過若是想要在Kotlin中定義屬性,必須保證這個屬性要麼是抽象的,要麼指定了訪問器。
interface Food {
val isCookFinished: Boolean // 抽象的屬性
val isEatFinished: Boolean // 指定了訪問器的屬性
get() = isCookFinished
fun cook() {
// 可選的方法體
}
fun eat()
}
複製代碼
和類同樣,接口也能夠繼承自另一個接口,能夠在父接口的基礎上去添加新的方法或者屬性。
在Kotlin中,主要有4種修飾符:
若是沒有指定修飾符,默認是public
。
咱們以前提到過,Kotlin能夠直接直接在頂層聲明類、函數和屬性。
// 文件名:example.kt
package foo
private fun foo() { …… } // 在 example.kt 內可見
public var bar: Int = 5 // 該屬性隨處可見
private set // setter 只在 example.kt 內可見
internal val baz = 6 // 相同模塊內可見
複製代碼
對於在類或者接口內的方法或者屬性,咱們四種修飾符均可用:
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
:隨處可見。局部變量、函數和類不能夠有可見性修飾符。
可見性修飾符internal
意味着該成員只在相同模塊內可見。更具體地說, 一個模塊是編譯在一塊兒的一套 Kotlin 文件:
test
源集能夠訪問main
的internal
聲明);<kotlinc>
Ant 任務執行所編譯的一套文件。