紧接上节的类与构造方法的介绍,下来接口与一些特殊类的用法(上节理解了,这节会显得轻松很多
1.接口(Interface)
Kotlin中的接口部分和Java几乎是完全一致的。
接口是用于实现多态编程的重要组成部分。我们都知道,Java是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin也是如此。
我们可以在接口中定义一系列的抽象行为,然后由具体的类去实现。下面还是通过具体的代码来学习一下,首先创建一个Study接口,并在其中定义几个学习行为。右击刚创建的包→New→Kotlin File/Class,在弹出的对话框中输入“Study”,创建类型选择“Interface”。然后在Study接口中添加几个学习相关的函数,注意接口中的函数不要求有函数体,代码如下所示:
interface Study {
fun readBooks()
fun doHomework()
}
接下来就可以让Student类去实现Study接口了,这里我将Student类原有的代码调整了一下,以突出继承父类和实现接口的区别:
class Student(name: String, age: Int) : Person(name, age), Study {
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
}
熟悉Java的人一定知道,Java中继承使用的关键字是extends,实现接口使用的关键字是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。 上述代码就表示Student类继承了Person类,同时还实现了Study接口。另外接口的后面不用加上括号,因为它没有构造函数可以去调用。
Study接口中定义了readBooks()和doHomework()这两个待实现函数,因此Student类必须实现这两个函数。Kotlin中使用override关键字来重写父类或者实现接口中的函数,这里我们只是简单地在实现的函数中打印了一行日志。
现在我们可以在main()函数中编写如下代码来调用这两个接口中的函数:
fun main() {
val student = Student("小明", 23)
doStudy(student)
}
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}
这里为了向你演示一下多态编程的特性,我故意将代码写得复杂了一点。首先创建了一个Student类的实例,本来是可以直接调用该实例的readBooks()和doHomework()函数的,但是我没有这么做,而是将它传入到了doStudy()函数中。doStudy()函数接收一个Study类型的参数,由于Student类实现了Study接口,因此Student类的实例是可以传递给doStudy()函数的,接下来我们调用了Study接口的readBooks()和doHomework()函数,这种就叫作面向接口编程(将功能分离,比如任意一个学生执行读书和写写作业操作,只需要调用dostudy函并将对象实例(学生)作为参数传入就可以了,如果临时需要修改了,也可以来分开操作,例如把学生实例换个人之类的),也可以称为多态。
运行结果如下图:
这样我们就将Kotlin中接口的用法基本学完了,是不是很简单?不过为了让接口的功能更加灵 活,Kotlin还增加了一个额外的功能:允许对接口中定义的函数进行默认实现(相当于自动写了一个适配器吧)。其实Java在JDK1.8之后也开始支持这个功能了,因此总体来说,Kotlin和Java在接口方面的功能仍然是一模一 样的。
下面我们学习一下如何对接口中的函数进行默认实现,修改Study接口中的代码,如下所示:
interface Study {
fun readBooks()
fun doHomework() {
println("do homework default implementation.")
}
}
可以看到,我们给doHomework()函数加上了函数体,并且在里面打印了一行日志。如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。现在当一个类去实现Study接口时,只会强制要求实现readBooks()函数,而doHomework()函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。
现在回到Student类当中,你会发现如果我们删除了doHomework()函数,代码是不会提示错误的,而删除readBooks()函数则不行。当删除了doHomework()函数之后,重新运行main()函数,结果如下图1所示。可以看到,程序正如我们所预期的那样运行了。
现在你已经掌握了Kotlin面向对象编程中最主要的一些内容,接下来我们再学习一个和Java相比变化比较大的部分——函数的可见性修饰符。
熟悉Java的人一定知道,Java中有public、private、protected和default(什么都不写)这4种函数可见性修饰符。Kotlin中也有4种,分别是public、private、protected和internal,需要使用哪种修饰符时,直接定义在fun关键字的前面即可。下面我详细介绍一下Java和Kotlin中这些函数可见性修饰符的异同。
首先private修饰符在两种语言中的作用是一模一样的,都表示只对当前类内部可见。
public修饰符的作用虽然也是一致的,表示对所有类都可见,但是在Kotlin中public修饰符是默认项,而在Java中default才是默认项。前面我们定义了那么多的函数,都没有加任何的修饰符,所以它们默认都是public的。
protected关键字在Java中表示对当前类、子类和同一包路径下的类可见,在Kotlin中则表示只对当前类和子类可见。
Kotlin抛弃了Java中的default可见性(同一包路径下的类可见),引入了一种新的可见性概念,只对同一模块中的类可见,使用的是internal修饰符。比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成internal。关于模块开发的内容,之后有机会再讲述,暂时也用不到。
下图更直观地对比了Java和Kotlin中函数可见性修饰符之间的区别。
2.kotlin的数据类与单例类(kotlin的特点与优势点)
在面向对象编程这一节,我们已经学习了很多的知识,那么在最后我们再来了解几个Kotlin中特有的知识点,从而圆满完成本次的学习任务。
在一个规范的系统架构中,数据类通常占据着非常重要的角色,它们用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。或许你听说过MVC、MVP、MVVM之类的架构模式,不管是哪一种架构模式,其中的M指的就是数据类。
数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中,equals()方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。
这里我们新构建一个手机数据类,字段就简单一点,只有品牌和价格这两个字段。如果使用Java来实现这样一个数据类,代码就需要这样写:
//java写法
public class Cellphone {
String brand;
double price;
public Cellphone(String brand, double price) {
this.brand = brand;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Cellphone) {
Cellphone other = (Cellphone) obj;
return other.brand.equals(brand) && other.price == price;
}
return false;
}
@Override
public int hashCode() {
return brand.hashCode() + (int) price;
}
@Override
public String toString() {
return "Cellphone(brand=" + brand + ", price=" + price + ")";
}
}
看上去挺复杂的吧?关键是这些代码还是一些没有实际逻辑意义的代码,只是为了让它拥有数据类的功能而已。而同样的功能使用Kotlin来实现就会变得极其简单,右击包名→New→Kotlin File/Class,在弹出的对话框中输入“Cellphone”,创建类型选择“Class”。然后在创建的类中编写如下代码:
//kotlin写法
data class Cellphone(val brand: String, val price: Double)
你没看错,只需要一行代码就可以实现了!神奇的地方就在于data这个关键字,当在一个类前面声明了data关键字时,就表明你希望这个类是一个数据类,Kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。另外,当一个类中没有任何代码时,还可以将尾部的大括号省略。
下面我们来测试一下这个数据类,在main()函数中编写如下代码:
data class Cellphone(val brand: String, val price: Double)
fun main() {
val cellphone1 = Cellphone("huawei", 1998.0)
val cellphone2 = Cellphone("honor", 998.0)
println(cellphone1)
println("cellphone1 equals cellphone2 " + (cellphone1 == cellphone2))
}
这里我们创建了三个Cellphone对象,首先直接将第一个对象打印出来,然后判断这两个对象 是否相等,运行结果见下图1:
很明显,Cellphone数据类已经正常工作了。而如果Cellphone类前面没有data这个关键字,得到的会是截然不同的结果。如果感兴趣的话,你可以自己动手尝试一下。
掌握了数据类的使用技巧之后,接下来我们再来看另外一个Kotlin中特有的功能——单例类。想必你一定听说过单例模式吧,这是最常用、最基础的设计模式之一,它可以用于避免创建重复的对象。比如我们希望某个类在全局最多只能拥有一个实例,这时就可以使用单例模式。当然单例模式也有很多种写法,这里就演示一种最常见的Java写法吧:
//java写法
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void singletonTest() {
System.out.println("singletonTest is called.");
}
}
这段代码其实很好理解,首先为了禁止外部创建Singleton的实例,我们需要用private关键字将Singleton的构造函数私有化,然后给外部提供了一个getInstance()静态方法用于获取Singleton的实例。在getInstance()方法中,我们判断如果当前缓存的Singleton实例为null,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。而如果我们想调用单例类中的方法,也很简单,比如想调用上述的singletonTest()方法,就可以这样写:
//java调用方法
Singleton singleton = Singleton.getInstance();
singleton.singletonTest();
虽然Java中的单例实现并不复杂,但是Kotlin明显做得更好,它同样是将一些固定的、重复的逻辑实现隐藏了起来,只暴露给我们最简单方便的用法。
在Kotlin中创建一个单例类的方式极其简单,只需要将class关键字改成object关键字即可。现在我们尝试创建一个Kotlin版的Singleton单例类,右击包名→New→Kotlin File/Class,在弹出的对话框中输入“Singleton”,创建类型选择“Object”,点击“OK”完成创建,创建方式和初始代码如下所示:
//kotlin创建方式
object Singleton {
}
现在Singleton就已经是一个单例类了,我们可以直接在这个类中编写需要的函数,比如加入一个singletonTest()函数:
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}
可以看到,在Kotlin中我们不需要私有化构造函数,也不需要提供getInstance()这样的静态方法,只需要把class关键字改成object关键字,一个单例类就创建完成了。而调用单例类中的函数也很简单,比较类似于Java中静态方法的调用方式:
//kotlin调用方式
Singleton.singletonTest()
这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。
顺便写在一个kt文件中运行一下看看,运行结果如下图1:
这样我们就将Kotlin面向对象编程最主要的知识掌握了,这也是非常充实的一节内容,希望你能好好掌握和消化。