文章目录
- 类的定义
- 创建类的实例
- 构造函数
- 主构造函数
- 次构造函数
- init语句块
- 数据类的定义
- 数据类定义了componentN方法
- 继承
- Any
- Any:非空类型的根类型
- Any?:所有类型的根类型
- 覆盖
- 方法覆盖
- 属性覆盖
- 抽象类
- 接口:使用interface关键字
- 函数:fun
- Unit:让函数调用皆为表达式
- 表达式函数体
- 类头格式化
类的定义
类可以包含:
- 构造函数和初始化块
- 函数
- 属性
- 嵌套类和内部类
- 对象声明
你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以
拥有自己独有的属性值。例如,每个Dog对象都有自己的名称、重量和品种属性,每个Dog的属性值都可以是不同的。
class Dog(val name: String, var weight: Int, val breed: String){
fun woo() {
}
}
如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号:
class Dog(val name: String, var weight: Int, val breed: String)
创建类的实例
val myDog = Dog("Fido", 70, "Mixed" )
上面的类有一个默认的构造函数。
注意:创建类的实例不用new
。
构造函数
在Kotlin
中的一个类可以有一个主构造函数和一个或多个次构造函数。
主构造函数
主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后:
class Person constructor(name: String, surname: String) {
}
如果主构造函数没有任何注解或者可见性修饰符,可以省略constructor
关键字:
class Person(name: String, surname: String) {
}
主构造函数不能包含任何的代码。初始化的代码可以放到以init
关键字作为前缀的初始化块中:
class Person constructor(name: String, surname: String) {
init {
print("init")
}
}
如果构造函数有注解或可见性修饰符,那么constructor
关键字是必需的,并且这些修饰符在它前面
次构造函数
类也可以声明前缀有constructor
的次构造函数:
class Person{
constructor(name: String) {
print("name is $name")
}
}
如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。
委托到同一个类的另一个构造函数用this
关键字即可:
class Person constructor(name: String) {
constructor(name: String, surName: String) : this(name) {
print( "name is : $name surName is : $surName")
}
}
init语句块
Kotlin引入了一种叫作init
语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。构造方法在类的外部,它只能对参数进行赋值。
如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如:
class Bird(weight: Double, age: Int, color: String) {
init {
println("the weight is ${weight}")
}
}
当没有val或者var的时候,构造函数的参数可以在init语句块被直接调用。除此之外,不能在其他地方使用。以下是一个错误的用法:
class Bird(weight: Double, age: Int, color: String) {
fun printWeight() {
print(weight) // Unresolved reference: weight
}
}
事实上,我们的构造方法还可以拥有多个init,他们会在对象被创建时按照类中从上到下的顺序先后执行。例如:
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
:
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
类中xoliu
的age
:
data class Person(val name: String,val age: Int)
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),这对于没有超类型声明的类是默认超类:
class Person // 从 Any 隐式继承
Any
不是java.lang.Object
。它除了equals()
、hashCode()
和toString()
外没有任何成员。
在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符open
,如果不加open,那它在转化为Java代码时就是final的:
所以Kotlin中所有的类默认都是不可继承的(final
)
所以我们只能继承那些明确声明open
或者abstract
的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后:
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
关键字初始化其基类型,或委托给另一个构造函数做到这一点。
注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
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的直接子类型。如:
class Animal(val weight: Double)
Any?:所有类型的根类型
如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。
覆盖
方法覆盖
只能重写显示标注可覆盖的方法:
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
,则子类中不能定义相同的方法: (不能重写,但能重载)
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
关键字:
open class SuperPerson(num: Int) : Person(num) {
final override fun changeName(name: String) {
super.changeName(name)
}
}
属性覆盖
属性覆盖与方法覆盖类似,只能覆盖显式标明open
的属性,并且要用override
开头:
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
标注一个抽象类或者函数——因为这不言而喻,这些属性一定需要去实现的
我们可以用一个抽象成员覆盖一个非抽象的开放成员:
open class Base {
open fun f() {}
}
abstract class Derived : Base() {
override abstract fun f()
}
接口:使用interface关键字
接口可以让你在父类层次结构之外定义共同的行为,接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点,而且使用起来更加灵活。
interface FlyingAnimal {
fun fly()
}
虽然Kotlin接口支持属性声明,然而它在Java源码中是通过一个get方法来实现的。在接口的属性并不能像Java接口那样,被直接赋值一个常量。如以下这样是错误的:
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提供了另外一种方式来实现这种效果:
interface Flyer {
val height
get() = 1000
}
一个类实现接口时:
class Bird() : Flyer {
// ...
}
接口的后面不用加上括号,因为它没有构造函数可以去调用。
函数:fun
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
如果你没有指定它的返回值,它就会返回Unit
,Unit
与Java
中的void
类似,但是Unit
是一个类型,而void只是一个关键字。Unit
可以省略。
fun maxOf(a: Int, b: Int): Int {
if (a > b) {
return a
} else {
return b
}
}
Unit:让函数调用皆为表达式
如果函数返回Unit
类型,该返回类型应该省略:
fun foo() { // 省略了 ": Unit"
}
之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如:
void foo() {
System.out.println("return nothing")
}
所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit
来替代Java中的void关键字。
Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。
表达式函数体
如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号:
fun add(x: Int,y: Int) : Int = x + y // 省略了{}
Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体
的情况下我们可以不声明返回值类型,这进一步简化了语法。
我们可以给参数指定一个默认值使的它们变的可选,这是非常有帮助的。这里有一个例子,在Activity
中创建了一个函数用来Toast
一段信息:
fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, length).show()
}
上面代码中第二个参数length
指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数:
toast("Hello")
toast("Hello", Toast.LENGTH_LONG)
类头格式化
有少数几个参数的类可以写成一行:
class Person(id: Int, name: String)
具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。此外,右括号应该另起一行。如果我们使用继承,
那么超类构造函数调用或者实现接口列表应位于与括号相同的行上:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) {
// ……
}
对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker {
// ……
}