第四章 类、对象和接口
4.1 定义类继承结构
4.1.1 Kotlin中的接口
1.概念说明
1.1 接口声明
- Kotlin接口用interface关键字声明
- Kotlin接口可以有抽象方法,但也允许不存在抽象方法。
- Kotlin接口可以带默认实现的方法
1.2 类的实现
- 一个类可以实现多个接口,但只能继承一个类
- override修饰符用来标注被重写的父类或者接口的方法或属性,且是强制的。
- 接口里带默认实现的方法在类中可以省略不管,但当实现两个接口中有相同实现方法名称时,则必须重新实现。
- 可以调用一个接口中的的实现,形式是super<接口名>.方法名
- Java中是接口名.super.方法名
2.示例:
//接口声明
interface Clickable{
fun click()
fun showOff() = println("I'm clickable")
}
interface Focusable{
fun showOff() = println("I'mFocusable")
}
//实现
class Button:Clickable,Focuseable{
override fun click() = println("I was clicked")
override fun showOff(){
super<Clickable>.showOff()
super<Focuseable>.showOff()
}
}
//调用
val button = Button()
button.click()
button.showOff()
4.1.2 open、final和abstract修饰符:默认为final
1.概念说明
- Kotlin中的类和方法默认是final的,Java中则是默认open的。
- 默认final有利于规避脆弱基类问题和方便于智能转换中的类型判定
- 用open修饰的话就可以允许创建子类或者重写方法属性。
- 接口中的成员始终是open的 ,不能将其声明为final,否则声明接口没有意义。
- 如果重写了基类或者接口中的成员,重写的成员默认是open。当然也可以显式地将重写成员标注为final来禁止重写。
- abstract修饰类的时候,类就不能被实例化。用来修饰类中的成员时,则成员始终是open的。
2.示例
open class RichButton:Clickable{ //这个类可以被其他类继承,去掉open则不能
fun disable(){} //函数是final的,不能被重写
open fun animate(){} //可以被重写
override fun click(){} //重写函数默认是open的,可以被重写
final override fun click2(){} //不能被重写
}
abstract class Animated{ //不能被实例化
abstract fun animate() //必须被子类重写
open fun stopAnimate(){} //可以被重写
fun animateTwice(){} //默认final,不能被重写
}
4.1.3 可见性修饰符:默认为public
1.Kotlin的可见性修饰符
- public:默认。公开,可见性最大,哪里都可以引用。
- internal:内部,仅对模块内可见。
- 一个模块就是一组一起编译的Koltin文件。可能是一个IDEA模块、一个AS项目等。
- 优势在于提供了对模块实现细节的真正封装。
- protected:保护,相当于 private + 子类可见。
- private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
2.Java:可见性修饰符
- public:公开,可见性最大,哪里都可以引用。
- protected: 表示包内可见 + 子类可见
- default:包内可见,只有在同一个 package 内可以引用
- private :类中可见,作为内部类时对外部类「可见」
3.一个通用规则:类的基础类型和类型参数列表中用到的所有类,或者函数的签名,
都有与这个类或者函数本身相同的可见性。或者说高可见性的类不能引用低可见性的类。
4.1.4 内部类和嵌套类:默认是嵌套类
Kotlin可以在一个类中声明另一个类,但这个嵌套类不能访问外部类的实例。
原因是Kotlin中没有显式修饰符的嵌套类默认与Java中的static嵌套类是一样的。但注意不能直接按Java中调用static的形式去调用内部类的方法属性。
如果需要嵌套类来引用外部类的话,需要用inner修饰符,引入语法为this@外部类名
//声明
class Outer{
inner class Inner{//没有inner的话不能引用
fun getOuterRef():Outer = this@Outer
fun innerFun1() = println("this is inner fun1")
}
}
//调用
val inner = Outer.Inner()
inner.innerFun1()
//下面这种调用是错误的
Outer.Inner.innerFun1()
4.1.5 密封类:定义受限的类继承结构
sealed修饰符:修饰的类对可能创建的子类做出严格限制,所有子类必须嵌套在父类中。
- sealed修饰的类只有private构造方法。因此不能用于接口。
- sealed修饰的类隐含这个类是一个open类,可省略open修饰符。
示例:
sealed class Expr{
class Num(val value:Int):Expr()
class Sum(val left:Expr,val right:Expr):Expr()
}
//调用,可以省略when结构中的else了
fun eval(e:Expr):Int = when(e){
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
4.2 声明一个带非默认结构方法或属性的类
4.2.1 初始化类:主构造方法和初始化语句块
- 创建类的实例时,不需要new关键字
- 主构造方法:声明类时,constructor(可省略)修饰的放在类名后的部分
- 初始化语句块:声明类时,类中init修饰的代码块。会在类被创建时执行。
//完整版本
class User constructor(_nickname:String){//带一个参数的主构造方法
val nickname:String
init{//初始化代码块
nickname = _nickname
//this.nickname = _nickname也可以
}
}
- 如果主构造方法没有注解或可见性修饰符,可以去掉constructor关键字。
//简化contructor关键字,代码块用属性
class User(_nickname:String){
val nickname = _nickname
}
- 如果属性用相应的构造方法参数类初始化,可以通过把val等关键字加在参数前进行简化。
class User(val nickname:String)//val意味着相应的属性会用改造方法的参数来初始化
- 可以在构造方法里为参数声明默认值
- 如果所有构造方法参数都有默认值,编译器会生成一个额外的空构造方法来使用所有的默认值。
class User(val nickname:String = "hehe")
- 如果没有给一个类声明任何构造方法,将会生成一个不做任何事的默认构造方法
open class Button
- 当有父类时,主构造方法同样需要初始化父类。
- 父类名称后面需要一个括号,这是父类构造方法
- 实现接口时,不需要在接口名称后面加括号,因为接口没有构造方法
class TwitterUser(nickname:String):User(nickname){ ...... }
- 当把构造方法标记为private时,类就不能被实例化了
class Secretive private constructor(){}
4.2.2 从构造方法:用不同的方式来初始化父类
基本和Java的构造方法一样了
- 从构造方法:声明类时,在类中的contructor方法。
- 主构造方法只能有一个,从构造方法可以有多个
open class View{
constructor(ctx:Context){ ..... }
constructor(ctx:Context,attr:AttributeSet){ ..... }
}
- 继承这个类时,也可以用super()调用父类的构造方法,this()调用自己的构造方法
class MyButton:View{
contructor(ctx:Context):this(ctx,MY_STYLE){ ..... }
contructor(ctx:Context,attr:AttributeSet):super(ctx,attr){ ..... }
}
4.2.3 实现在接口中声明的属性
以代码为例演示如何实现接口中的属性。
interface User{
val nickname:String
}
主构造方法实现
class PrivateUser(override val nickname:String):User
从构造方法实现
class SubUser(val email:String):User{
override val nickname:String
get() = email.substringBefore('@') //每次调用nickname时都要调用这个函数
}
class FaceUser(val accountId:Int):User{
override val nickname = getFaceName(accountId)//有支持字段,只会执行函数一次
}
4.2.4 通过getter或setter访问支持字段
只有var变量有setter函数。
setter函数体中,可以用field来访问支持字段的值。
getter只能读取值,setter既能读取也能修改。
只有当显式地引用或者使用默认的访问器,属性才会有支持字段。
示例:
class User(val name:String){
var address:String = "haha"
set(value:String){
println("$name--$field--$value")
filed = value
}
}
4.2.5 修改访问器的可见性
可以通过在get或者set关键字前放置可见性修饰符来修改属性的访问器可见性。
当set私有后就不能在类外部修改这个属性,但还可以在类内部修改。
示例:
class LengthCouter{
var counter:Int = 0
private set
fun addWord(word:String){
counter += word.length
}
}
4.3 编译器生成的方法:数据类和类委托
4.3.1 通用对象方法
本节只是讨论了三个非常常用的对象方法:toString(),equals(),hashCode()
tips:
1.==相等性
Java中,用来比较基本数据类型和引用类型,但基本数据类型比较的是值,引用类型比较的是引用。
引用类型比较值用equals.
Kotlin中,值相等就全用来比较,===来比较对象引用。
2.is
Kotlin中is检查相当于Java中instanceOf,用来检查一个值是否是一个指定的类型。
!is就是is检查的非运算。
4.3.2 数据类:自动生成通用方法的实现
data关键字:修饰类的时候,就可以自动重写所有标准Java方法的类:
- equals:用来比较示例,会将所有在主构造方法中声明的属性纳入考虑。
- hashCode:考虑所有在主构造方法中声明的属性,用来作为例如HashMap这种基于哈希容器的键
- toString
虽然data修饰class时不要求主构造方法参数一定是val修饰,但还是要慎用var修饰,因为很可能var属性被修改后造成放进对象的容器的无效。 - copy:允许复制类的实例的方法,并在复制的同时修改某些属性的值。
模仿实现copy
class Client(val name:String,val postalCode:Int){
fun copy(name:String = this.name,postalCode:Int = this.postalCode) =
Client(name,postalCode)
}
4.3.3 类委托:使用by关键字
为了优雅实现类似装饰器模式的功能,Kotlin将委托作为一个语言级别的功能做了头等支持。
使用by关键字将接口的实现委托到另一个对象。
示例:
class CoutingSet<T>(val innerSet:MutableCollection<T> = HashSet<T>())
:MutableCollection<T> by innerSet{
//下面的代码是不使用委托时的实现,使用委托的话,就不需要写下面的代码了
var objectsAdded = 0
override fun add(element:T):Boolean{
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c:Collection<T>):Boolean{
objectsAdded += c.size
return innerSet.addAll(c)
}
}
//调用
val cset = CoutingSet<Int>()
cset.addAll(listOf(1,2,3,4))
println("${cset.size}")
4.4 object关键字:将声明一个类与创建一个实例结合起来
Kotlin中object关键字在多种情况下出现,但都是定义一个类并同时创建一个实例。
4.4.1 对象声明:创建单例易如反掌
1.概念说明
- object+类的声明,就创建了个单一实例,也就是对象声明。
- 对象声明时不允许有任何构造方法,因为不需要在其他地方调用,构造方法是没有意义的。
- 对象声明允许使用对象的名加.字符的方法来调用方法和访问属性。
- 对象声明可以继承类和接口,但一般并不包含任何状态,用来实现类似Comparator这样的接口比较合适。
- 可以类中声明对象,这样的对象只有一个单一实例,同Java中static修饰一样。
2.示例:
//声明
object Payroll{
val allEmployee = arrayListOf<Person>()
fun cal(){ ... }
}
//调用
Payroll.allEmployee.add(Person(....))
Payroll.cal()
//类内声明
data class Person(val name:String){
object NameComparator:Comparator<Person>{
override fun compare(p1:Person,p2:Person):Int = p1.name.compareTo(p2.name)
}
}
//调用
val persons = listOf( ...... )
println(persons.sortedWith(Person.NameComparator))
4.4.2 伴生对象:工厂方法和静态成员的地盘
- 伴生对象:用companion object修饰的在类中定义的对象,这样的话就可以直接通过容器类名称来访问这个对象的方法和属性,不再需要显示的指定对象的名称。
每个类中只允许最多一个伴生对象。伴生对象可以没有名称。
示例:
//声明
class A{
companion object{
fun bar(){ ...... }
}
}
//调用
A.bar()
- 一个典型应用场景:工厂方法
class User private constructor(val nickname:String){
companion object{
fun newSubUser(email:String) = User(email.substringBefor('@'))
fun newFaceUser(accountId:Int) = User(getFace(accountId))
}
}
//调用
val subUser = User.newSubUser("aa@qq.com")
tips:
Kotlin中没有static关键字。
4.4.3 作为普通对象使用的伴生对象
- 伴生对象可以有名字,如companion object AA
- 伴生对象可以继承接口和类
- 伴生对象可以有扩展函数
4.4.4 对象表达式:改变写法的匿名内部类
1.概念说明
匿名内部类:object:类名(),也可能有参数,看主构造方法。
- 可以实现多个接口
- 可以访问创建它的函数中的变量
- 可以存储到变量中
2.示例
button.addOnClickListener(object:OnClickListener(){ ..... })
val listener = object:OnClickListener(){ ..... }
button.addOnClickListener(listener)
4.5 小结
略
特别章节
读完本篇,有几个定义概念很可能在脑子里比较浆糊:
嵌套类和内部类、object修饰类,companion object修饰类,object加:修饰的类。
首先嵌套类等于内部类,两者只是叫法不同而已。后面用内部类表示。
本章节里说的内部类和object相关的几个概念里的区别是:
- 内部类是类的声明,object修饰相关概念是对象声明。因此内部类还可以被实例化,而object相关类则不能再被实例化了。
- 内部类被使用时是一定要实例化的,不论在外部类的内部还是外部。被实例化后就可以正常使用它。
- object+类名和companion object+类名,这两者几乎是一样的使用方法,只不过companion修饰后就可以直接用外部类名调用伴生对象内的方法属性。
- 伴生对象只能有一个,object+类名则可以有多个
- 伴生对象可以省略名称,object+类名则不能省略
- 两者的属性方法都可以在外部类内部被调用,但不能被外部类的实例对象所调用。
- 两者都不能直接调用外部类的方法属性。
- object加:修饰的类比较简单,就是直接实例化一个类,不带名字而已。
代码示例:
//内部类声明
class Outer{
class Inner{//没有inner的话不能引用
fun innerFun1() = println("this is inner fun1")
}
fun useInner(){
println("this is useInner")
val inner = Inner()
inner.innerFun1()
}
}
//内部类调用
val outer = Outer()
outer.useInner()
val inner = Outer.Inner()
inner.innerFun1()
// Outer.Inner.innerFun1()
>>>>>
this is useInner
this is inner fun1
this is inner fun1
//object+类声明
class ObjectOuter{
object ObjectInner{
fun innerFun1() = println("this is object inner fun1")
fun useOuterFun(){
//不能调用外部类的属性方法
}
}
object ObjectInner2{}
fun useObjectInner(){
println("this is useObjectInner")
ObjectInner.innerFun1()
//innerFun1()这样直接调用是不可以的
}
}
//object+类调用
ObjectOuter.ObjectInner.innerFun1()
val objectOuter = ObjectOuter()
objectOuter.useObjectInner()
//objectOuter.ObjectInner.innerFun1()
>>>>>>
this is object inner fun1
this is useObjectInner
this is object inner fun1
//companion object类声明
class CompanionObjectOuter{
companion object {
fun companionInnerFun1() = println("this is companion object inner fun1")
}
fun useCompanionObjectInner(){
println("this is useCompanionObjectInner")
companionInnerFun1()
}
}
//companion object类调用
CompanionObjectOuter.companionInnerFun1()
val companionObjectOuter = CompanionObjectOuter()
//companionObjectOuter.companionInnerFun1()
companionObjectOuter.useCompanionObjectInner()
>>>>>>
this is companion object inner fun1
this is useCompanionObjectInner
this is companion object inner fun1