本章内容包括: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 任务执行所编译的一套文件。