类、对象和接口
一.定义类继承结构
kotlin中的接口
使用interface关键字
interface clickable{
fun click()
}
实现接口的方法
class Button : Clickable{
override fun click() = println("button click")
}
可以看到kotlin使用 : 代替了extends和implements关键字实现继承和实现。和Java一样只能实现单继承和实现多个接口
override关键字在kotlin中用来标注被重写的父类方法,是强制要求的。
接口中默认方法的实现没有特殊注解,直接提供方法体即可。
interface Clickable{
fun click()//空方法
fun showOff() = println("default method")//默认实现的方法
}
如果两个接口中有同名的默认方法并且同时实现了这两个类,必须在子类中显式的实现该方法或者通过类名调用
interface A{
fun test() = print("A")
}
interface B{
fun test() = print("B")
}
class AB : A,B{
override fun test() {
super<A>.test()
super<B>.test()
}
}
<分析>由于kotlin兼容Java 6,所以并不支持接口的默认方法。因此它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。
访问修饰符
kotlin中默认的修饰符是final
如果要允许被继承,则需要显式的声明open修饰符
open class RichButton : Clickable{//这个类是可继承的
fun disable(){}//默认是final,不能在子类中重写该方法
open fun animate(){}//可以在子类中重写
override fun click(){}//重写了一个open的函数,因此也是open的
}
如果想阻止类中重写的函数在子类被重写,需要显式的添加final字段
open class RichButton : Clickable{
final override fun click(){}//没有final的override意味着open
}
抽象类同样是使用abstract修饰,抽象方法也是使用abstract修饰且必须在子类中实现
接口中的成员始终是open的,不能声明为final,不能使用abstract、open或者final
类中访问修饰符的意义
修饰符 | 相关成员 | 评注 |
---|---|---|
final | 不能被重写 | 类中成员默认使用 |
open | 可以被重写 | 需要明确的表示 |
abstract | 必须被重写 | 只能在抽象类中使用,抽象成员不能有实现 |
override | 重写父类或者接口中的成员 | 如果没有使用final表明,重写的成员默认是open |
可见性修饰符
修饰符 | 类成员 | 顶层声明 |
---|---|---|
public | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | |
private | 类中可见 | 文件中可见 |
需要注意的是internal表示一个模块(一组一起编译的kotlin文件等),提供了对模块实现细节的真正封装
protected成员只在类和他的子类中可见
类的扩展函数不能访问它的private和protected成员
<注>kotlin中的可见性修饰符在编译成字节码时,private会被编译成包私有声明,internal会变成public
内部类和嵌套类
Kotlin中默认是嵌套类,对应Java中static修饰的内部类
类A在另一个类B中声明 | 在Java中 | 在kotlin中 |
---|---|---|
嵌套类(不存储外部类的引用) | static class A | class A |
内部类(存储外部类的引用) | class A | inner class A |
class Outer{
inner class Inner{
fun getOuterReference() : Outer = this@Outer
}
}
密封类
通过sealed关键字修饰的类,对可能创建的子类作出严格的限制。所有的直接子类必须嵌套在父类中。sealed隐含open
sealed class Father{
class A : Father()
class B : Father()//将所有可能的类作为嵌套类列出
}
fun test(f : Father) : Father =
when(f){
is A -> A
is B -> B//when表达式涵盖了所有可能的情况,不再需要额外的else分支
}
在when中使用sealed类并且添加一个新的子类的时候,有返回值的when表达式会编译失败
二.声明一个带非默认构造方法或属性的类
主构造方法
class User(val name : String)//被括号围起来的语句块叫做主构造方法
明确来写是如下写法
class User constructor (_name : String){//constructor用来开始一个主构造方法或构造方法的声明
val name : String
init {//初始化语句块
name = _name
}
}
class User(_name : String){//带一个参数的主构造方法
val name = _name//用参数来初始化属性
}
继承过程中
open class People(val name : String)
class Man(name : String) : People(name)
open class Button//没有声明参数,生成默认构造方法
如果不想类被实例化,需要给构造器添加private修饰符
class User private constructor(){}
同样的,也可以添加默认值
class User(val name :String , val isMan : Boolean = true)
具体初始化过程和Java类似
open class People(var name : String) {
init {
println(name + "people")
}
open fun one(){
println("People")
}
}
class Man constructor(_name : String): People("People"){
init {
println(name + "Man")
name = _name
println(name + "Man")
}
override fun one(){
print("Man")
println(name)
}
}
在main函数中
Man("hello").one()
最后输出
1 Peoplepeople
2 PeopleMan
helloMan
3 Manhello
分析:
1)首先是父类的初始化init调用
2)接着调用子类init方法,此时可以看到子类的构造方法并不是val name : String的形式,如果这么写编译器会报错,提示隐藏了父类中的参数
3)多态,同Java
其他构造方法
open class View{
constructor(ctx : Context){}
constructor(ctx : Context, attr : AttributeSet){}//没有声明主构造方法,但是声明了两个从构造方法
}
子类的继承
class Button : View{
constructor(ctx : Context) : super(ctx){}
constructor(ctx : Context, attr : AttributeSet) : super(ctx,attr){}//调用父类的构造方法
也可以通过this调用另一个构造方法
//constructor(ctx : Context) : this(ctx,MY_STYLE)
}
如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做的构造方法
实现在接口中声明的属性
interface User{
val nickname : String
}
//主构造方法属性,实现了来自User的抽象属性,需要标记为override
class PrivateUser(override val nickname : String) : User
//自定义getter,这个字段没有支持字段来存储,只有一个getter在每次调用时获得数值
class EmailUser(val email : String) : User{
override val nickname : String
get() = email.substringBefore('@')
}
//属性初始化
class QQUser(val openId : Int) : User{
override val nickname = getOpenId(openId)
}
通过getter/setter访问支持字段
class CustomSetter(val name : String){
var address : String = "unknown"
set(value : String){
println("""
Address was changed for $name:
"$field"->"$value".//field用来访问支持字段的值
""".trimIndent())
field = value
}
}
在main方法中调用CustomSetter("XXD").address = "123"
会输出
Address was changed for XXD:
"unknown"->"123".
修改访问器的可见性
通过在get/set关键字前面添加可见性修饰符来修改
三.编译器生成的方法:数据类和类委托
通用对象方法
open class User(val user : String , val phone : String){
override fun toString() = "user : $user , phone : $phone"
override fun hashCode() = user.hashCode() * 31 + phone.hashCode()
override fun equals(other: Any?): Boolean {
if (other == null || other !is User)
return false
else
return user == other.user && phone == other.phone
}
}
很明显的三个重写方法,其中需要注意的是在kotlin中,==用来比较内容是否相等(equals),使用=来比较引用
数据类:自动生成通用方法的实现
但是如果每个类都要重写这些方法,相对于Java而言,kotlin并没有优势,所以通过添加data修饰符
data class User(val user : String , val phone : String)
//此时得到了一个重写所有Java标准的方法
//equals和hashCode方法会将所有在主构造方法中生命的属性纳入考虑
//同时还会生成copy方法,用于在copy实例的时候修改某些属性
val user = User("xxd","123")
println(user.copy(phone="234"))
==>user : xxd , phone : 234
类委托:使用by关键字
class testCollection<T> (innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList{
//重写或者不重写
}
接口的实现被委托给了另一个对象innerList,编译器会默认生成各种方法;当需要修改行为时,可以重写相应的方法
object关键字,将声明一个类与创建一个实例结合起来
对象声明
通过关键字object引入,与普通类不同的是没有构造方法
其在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法
object MyComparator : Comparator<String>{
override fun compare(str1 : String, str2 : String): Int {
return str1.compareTo(str2)
}
}
在Java中调用时,使用MyComparator.INSTANCE.compare(str1,str2)的形式
同样可以在类中声明对象
伴生对象
在类中定义的对象可以使用关键字companion,这样可以通过容器类名称来直接访问这个对象的方法和属性
class Student private constructor(val name : String){
companion object {
fun getStudentByScore(score : Int) = Student(queryByScore(score))
fun getStudentById(id : Long) = Student(queryById(id))
}
}
简单来说就是一个工厂方法的使用
作为普通对象使用的伴生对象
可以给伴生对象起一个名字诸如 companion object Head,省略了则默认分配Companion
对象表达式:改变写法的匿名内部类
window.addMouseListener(object : MouseAdapter(),Serializable{
//重写的方法
})
可以看到,和Java匿名内部类不同的是,kotlin的匿名对象可以实现多个接口或者不实现接口
同时访问的变量并没有限制为final