回顾一下上章的内容
- Kotlin没有定义自己的集合类,而是在Java集合类的基础上提供了更丰富的api
- Kotlin可以给函数参数定义默认值,这样降低了重载函数的必要性,而且命名参数让多参数函数的调用更加易读
- Kotlin允许更灵活的代码结构:函数和属性都可以直接在文件中声明,而不仅仅是在类中作为成员
- Kotlin可以用扩展函数和属性来扩展任何类的api,包括在外部库中定义的类,而不需要修改其源代码,也没有运行时开销
- 中缀调用提供了处理单个参数的,类似调用运算符方法的简明语法
- Kotlin为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
- 三重引号的字符串提供了一种简洁的方式,解决了原本在Java中需要进行大量啰嗦的转义和字符串连接的问题
- 局部函数帮助保持代码整洁的同时,避免重复
第4章前言 类、对象和接口
本章会加深对如何使用kotlin的类的理解,Kotlin类和接口和Java的类和接口还是有一点区别的。例如,接口可以包含属性声明。与Java不同,Kotlin的声明默认是final和public的。此外,嵌套的类默认并不是内部类:它们并没有包含对其外部类的隐式引用。
Kotlin编译器能够生成有用的方法来避免冗余。将一个类声明为data类可以让编译器为这个类生成若干标准方法。同样可以避免动手书写委托方法,因为委托模式是Kotlin原生支持的。
章还介绍一个新的可以声明类并创建这个类的一个实例的object关键字。这个关键字用来表示单例对象、伴生对象和对象表达式(类似于Java的匿名类)。
4.1 定义类继承结构
本节内容会将Kotlin的类继承结构定义与Java做比较。我们会关注到Kotlin的可见性和访问修饰符,它们与Java中的类似但还是有一些不同。你还可以学到新的用于限制一个类可能存在的子类的sealed修饰符。
4.1.1 Kotlin中的接口
声明一个简单的接口
interface Clickable {
fun click()
}
这声明了一个拥有名为click的单抽象方法的接口。所有实现这个接口的非抽象类都需要这个方法的一个实现。接下来来实现这个接口
class Button : Clickable{
override fun click() {
println("button was clicked")
}
}
>>> Button().click()
button was clicked
Kotlin在类名后面使用冒号来替代了Java中的extends和implements关键字。和Java一样,一个类可以实现任意多个接口,但是只能继承一个类。
与Java中的@Override注解类似,override修饰符用来标注被重写的父类或者接口的方法和属性。与Java不同的是,在Kotlin中使用override修饰符是强制要求的。这会避免先写出实现方法再添加抽象方法造成的意外重写:你的代码将不能编译,除非你显式地将这个方法标注为override或者重名它。
接口的方法可以有一个默认实现。与Java8不同的是,Java8中需要你再这样的实现上标注default关键字,对于这样的方法,Kotlin没有特殊的注解:只需要提供一个方法体。让我们来给Clickable接口添加一个带默认实现的方法。
在接口中定义一个带方法体的方法
interface Clickable {
fun click()
fun showOff() = println("I'm Clickable")
}
如果你实现了这个接口,你需要为click提供一个实现。可以重新定义showOff方法的行为,或者如果你对默认行为感到满意也可以直接省略它。
现在有另一个接口Focuable ,它也定义了一个showOff方法并且有如下实现
interface Focusable {
fun setFocus(b : Boolean) = println("I ${if(b) "got" else "lost"} focus")
fun showOff() = println("I'm focusable")
}
如果需要在你的类中实现这两个接口会发生什么?它们每一个都包含了带默认实现的showOff:子类将会使用哪一个实现? 答案是,任何一个都不会使用。如果你不自己去实现showOff,编译器将会报错,Kotlin编译器强制要求你提供自己的实现。
class Button : Clickable,Focusable {
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
//使用尖括号加上父类名字的“super”表明了你想要调用哪一个父类的方法
}
override fun click() {
println("button was clicked")
}
}
在Java中实现包含方法体的接口
Kotlin1.0是以Java6为目标设计的,并不支持接口中的默认方法。因此它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。接口中只包含声明,类中包含了以静态方法存在的所有实现。因此,如果需要在Java中实现这样的一个接口,必须实现所有的方法,包括在Kotlin中有方法体的方法定义你自己的实现
4.1.2 open 、final 和 abstract修饰符:默认为final
Java 允许你创建任意类的子类重写任意方法,除非显式地使用final关键字进行标注。这通常很方便,但是也造成了一些问题。
对基类修改会导致子类不正确的行为,这就是所谓的脆弱的基类问题,因为基类代码的修改不再符合在其子类中的假设。如果类没有提供子类应该怎么实现明确规则(哪些方法需要被重写及如何重写),当事人可能会有按基类作者之外的方式来重写方法的风险。因为不可能分析所有的子类,这种情况下基类是如此“脆弱”,任何修改都有可能导致子类出现预期之外的行为改变。
为了防止这种问题,作为优秀Java编程风格最为知名的图书之一,《Effective Java》建议你“要么为继承做好设计并记录文档,要么禁止这么做”。这意味着所有没有特殊需要在子类中被重写的类和方法应该背显式地标注为final。
Kotlin采用了同样的哲学思想。Java的类和方法默认是open的,而Kotlin中默认都是final的。
如果你想允许创建一个类的子类,需要使用open修饰符来标注这个类,此外,需要给每一个可以被重写的属性和方法添加open修饰符。
声明一个带一个open方法的open类
禁止重写
open class RichButton : Clickable {
override fun click() { //这个函数重写了一个open函数,并且它本身也是open的,
}
fun disable() {} // 这个函数是final的:不能再子类中重写它
open fun animate() {} // 这个函数是open的,可以在子类中重写它
}
open class RichButton : Clickable {
final override fun click() { //禁止重写,加上final的“override”意味着是final的
}
}
/**
* 在Kotlin中,同Java一样 ,可以将一个类声明为abstract,这种类不能实例化,
* 一个抽象类通常包括一些没有实现并且必须在子类重写的抽象成员
* 抽象成员始终是open的,所以不需要显式地使用open修饰
*/
abstract class Animated{ //这个类是抽象的不能创建它的实例
abstract fun animate() //这个函数是抽象的:它没有实现必须被子类重写
open fun stopAnimating(){} //抽象类中的非抽象函数默认final的,但可以标注open
fun animateTwice(){ //抽象类中的final函数
}
}
修饰符 | 相关成员 | 评注 |
---|---|---|
final | 不能被重写 | 类中成员默认使用 |
open | 可以被重写 | 需要明确的声明 |
abstract | 必须被重写 | 只能在抽象类中使用,抽象成员不能有实现 |
override | 重写父类或接口中的成员 | 果没有使用final标明,重写的成员默认是open的 |
4.1.3 可见性修饰符 :默认为public
可见性修饰符帮助控制对代码库中声明的访问,通过限制类中实现细节的可见性,Kotlin中的可见性修饰符与java中类似,同样可以使用
public 、 protected和private修饰符。但是默认可见性是不一样的:如何省略了修饰符,默认是 public的
Java中默认可见性—包私有,在Kotlin中并么有使用。Kotlin只把包作为命名空间里组织代码的一种方式而已,并没有将其用作可见性控制
回顾一下Java访问修饰符
public | 可以被所有其他类所访问 |
private | 只能被自己访问和修改 |
protected | 自身、子类及同一个包中类可以访问 |
default | 同一包中的类可以访问,声明时没有加修饰符,认为是friendly。 |
Kotlin 提供了一个新的修饰符,internal,表示“只在模块内部可见”。一个模块就是一组一起编译的Kotlin文件。这有可能是一个IntellijIDEA模块
一个Eclipse项目、一个Maven或Gradle项目或者一组使用调用Ant任务进行编译的文件。
internal 可见性的优势在于它提供了对模块实现细节的真正封装。使用Java时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你
的包私有声明的权限。
另一个区别就是Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就会只在声明它们的文件中可见
Kotlin可见性修饰符总结
修饰符 | 类成员 | 顶层声明 |
---|---|---|
public(默认) | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | —— |
private | 类中可见 | 文件中可见 |
看一下实例
internal open class TalkactiveButton :Focusable{
private fun yell() = println("Hey!")
protected fun whisper() = println("let's talk!")
}
public fun TalkactiveButton.giveSpeech(){ //错误,“public”成员暴露了其“internal”接收者类型TalkactiveButton
yell() // 错误:不能访问“yell”:它在TalkactiveButton中是private
whisper() //错误:不能访问,它在TalkactiveButton中是protected的
}
Kotlin禁止从public函数giveSpeech去引用低可见性的烈性TalkativeButton(例子中是internal)。
一个通用的规则:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性,解决上面的问题可以把函数改为internal的,也可以把类改为public的。
注意,protected修饰符在Java和Kotlin中不同的行为。在Java中,可以从同一个包中访问一个protected的成员,在K呕吐临终不允许,protected成员只在类和它的子类中可见。同样还要注意的是类的扩展函数不能访问private和protected成员。
4.1.4 内部类和嵌套类:默认是嵌套类
像Java一样,在Kotlin中可以在另一个类中声明一个类。这样在封装一个辅助类或者把一些代码放到靠近它被使用的地方时非常有用。区别是Kotlin的嵌套类不能访问外部类实例,除非做了特殊要求。
声明一个序列化的视图
public interface State extends Serializable {
}
public interface View {
State getCurrentState();
void restoreState(State state);
}
用带内部类的Java代码来实现View
public class TextView implements View {
@NotNull
@Override
public State getCurrentState() {
return new TextViewState();
}
@Override
public void restoreState(@NotNull State state) {
}
public class TextViewState implements State{
}
定义了实现State接口的TextViewState 类 ,并且持有TextView的特定信息。
我们来运行这段代码
public static void main(String[] args) {
State textViewState = new TextView().getCurrentState();
// ObjectOutputStream 对象输出流,完成对TextView对象的序列化操作
try {
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
new File("E:/textView.txt")));
oo.writeObject(textViewState);
} catch (IOException e) {
e.printStackTrace();
}
}
报错了
java.io.NotSerializableException: fourth.one.TextView
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at fourth.one.TestJava.main(TestJava.java:19)
TextView隐式的存储了它的外部类View类的引用,当序列化TextView时,是需要所有变量的序列化,View不是可序列化的,要修复这个问题要把TextViewState 变为静态
在Kotlin中,内部类的默认行为与我们刚刚描述相反
用Kotlin来实现
class TextView : View {
override fun getCurrentState(): State {
return TextViewState()
}
override fun restoreState(state: State) {
}
class TextViewState : State
}
val textViewState = TextView().currentState
try {
val oo = ObjectOutputStream(FileOutputStream(
File("E:/textView.txt")))
oo.writeObject(textViewState)
} catch (e: IOException) {
e.printStackTrace()
}
Kotlin中没有显式修饰符的嵌套类与Java中的静态内部类一样。要把它变为一个内部类来持有外部类引用的话需要使用inner修饰符。
在Kotlin中引用外部内实例的语法与Java不同,需要使用this@Outer
class Outer{
inner class Inner{
fun getOuterReference() : Outer = this@Outer
}
}
嵌套类和内部类在Java和Kotlin中的对应关系
类A在另一个类B中声明 | Java | Kotlin |
嵌套类(不存储外部类引用) | static class A | class A |
内部类(储存外部类的引用) | class A | inner class A |
4.1.5 密封类 定义受限的类继承结构
sealed 类。为父类添加一个sealed修饰符,对可能创建的子类做出严格限制。所有的子类必须嵌套在父类中。
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 -> TODO()
is Expr.Sum -> TODO()
}
-——《Kotlin实战》