Kotlin基础-有趣的面向对象(中)

235 阅读3分钟

7、伴生对象

在Kotlin中,伴生对象是使用两个关键字companion object来声明,它的出现主要是为了解决Java中静态变量、静态方法和成员变量、成员方法混写在一个类中杂乱的问题,因此Kotlin中取消了static关键字

companion object中的属性和方法只能通过类调用,而不能通过实例对象

如下

19.png

 class Companion(private val name: String) {
 ​
     fun printObjectMessage() {
         println("${this.hashCode()} name=${this.name}")
     }
 ​
     companion object {
         val CLASS_NAME: String
             get() = "Companion"
         fun printClassMessage() {
             println("The Companion class has a name attribute")
         }
     }
 }
 ​
 fun main() {
     print("The name of the Companion class is ${Companion.CLASS_NAME} and it's description is ")
     Companion.printClassMessage()
 ​
     val companion = Companion("companion")
     companion.printObjectMessage()
 }

如上代码如果要翻译成Java代码类似下面这样(没有接触过Java的小伙伴可以跳过)

 class Companion {
 ​
     public static String CLASS_NAME = "Companion";
 ​
     public static void printClassMessage() {
         System.out.println("The Companion class has a name attribute");
     }
 ​
     public String getName() {
         return name;
     }
 ​
     public void setName(String name) {
         this.name = name;
     }
 ​
     private String name;
 ​
     public Companion(String name) {
         this.name = name;
     }
 ​
     public Companion() {
     }
 ​
     public void printObjectMessage() {
         System.out.println(this.hashCode() + " name=" + this.name);
     }
 ​
     public static void main(String[] args) {
         System.out.print("The name of the Companion class is " + Companion.CLASS_NAME + " and it's description is ");
         Companion.printClassMessage();
 ​
         Companion companion = new Companion("companion");
         companion.printObjectMessage();
     }
 }

我们可以看到区别就是Java中的一个类即有成员变量方法又有静态变量方法,而Kotlin中就将所有的类属性方法都放在了companion object代码块中,语义更加的清晰了

object关键字天生支持单例模式(对象声明)

下面的内容参考自水滴技术团队的《Kotlin核心编程》一书

比如说在Java中,我们需要自己封装一个数据库配置的单例对象(还没有接触过Java和单例模式的小伙伴也不用担心,可以先跳过),我们可以这样

 public class DatabaseConfig {
 ​
     private static DatabaseConfig databaseConfig = null;
 ​
     private static final String DEFAULT_HOST = "127.0.0.1";
     private static final String DEFAULT_PORT = "3306";
     private static final String DEFAULT_USERNAME = "root";
     private static final String DEFAULT_PASSWORD = "";
 ​
     public static DatabaseConfig getDatabaseConfig() {
         if (databaseConfig == null) databaseConfig = new DatabaseConfig(DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USERNAME, DEFAULT_PASSWORD);
         return databaseConfig;
     }
 ​
     private String host;
     private String port;
     private String username;
     private String password;
 ​
     public String getHost() {
         return host;
     }
 ​
     public void setHost(String host) {
         this.host = host;
     }
 ​
     public String getPort() {
         return port;
     }
 ​
     public void setPort(String port) {
         this.port = port;
     }
 ​
     public String getUsername() {
         return username;
     }
 ​
     public void setUsername(String username) {
         this.username = username;
     }
 ​
     public String getPassword() {
         return password;
     }
 ​
     public void setPassword(String password) {
         this.password = password;
     }
 ​
     public DatabaseConfig(String host, String port, String username, String password) {
         this.host = host;
         this.port = port;
         this.username = username;
         this.password = password;
     }
 ​
     public static void main(String[] args) {
         DatabaseConfig config = DatabaseConfig.getDatabaseConfig();
         System.out.println("host=" + config.getHost() + " port=" + config.port + " username=" + config.username + " password=" + config.password);
 ​
         config.password = "1234";
         System.out.println("host=" + config.getHost() + " port=" + config.port + " username=" + config.username + " password=" + config.password);
 ​
         // 验证是否为单例对象
         DatabaseConfig config2 = DatabaseConfig.getDatabaseConfig();
         System.out.println("host=" + config2.getHost() + " port=" + config2.port + " username=" + config2.username + " password=" + config2.password);
     }
 }

我们可以看到上述Java实现单例模式比较繁琐,而在Kotlin中只需要使用object关键字就可以用简短的代码来实现单例模式。如下

 object DatabaseConfig {
     var host: String = "127.0.0.1"
     var port: String = "3306"
     var username: String = "root"
     var password: String = ""
 }
 ​
 fun main() {
     println("host=${DatabaseConfig.host} port=${DatabaseConfig.port} username=${DatabaseConfig.username} password=${DatabaseConfig.password}")
 ​
     DatabaseConfig.password = "1234"
     println("host=${DatabaseConfig.host} port=${DatabaseConfig.port} username=${DatabaseConfig.username} password=${DatabaseConfig.password}")
 }

object还有一个作用就是实现Java中匿名内部类的功能(对象表达式)

Java中的匿名内部类其实本质上就是实现某个接口的一个类的实例对象,只不过不需要再去声明这个类,它是匿名的。这样做的目的无非就是简化不必要的代码。而在Kotlin中object也可以实现Java中匿名内部类的功能,本质一样,用关键字object修饰的本身也是一个对象,通过object表达式就可以实现匿名内部类的的功能。

其实有些开发者认为匿名内部类是方法参数中又掺杂类声明会不易阅读,看起来复杂,而Kotlin的object就可以更好一些。这个观点我不这么认为,其实这两者的本质是一样的,方法中要传入的那个参数就是一个特定类型的对象,抓到这个点就不难理解,也不难阅读。因为我认为Java中的匿名内部类并没有什么歧义之处。

我们平常在使用集合的时候可能会进行一个自定义排序,在Java中很多时候可能就需要用到匿名内部类,如下代码片段(不熟悉Java的小伙伴也没有关系,可以跳过这一部分,不影响Kotlin基础)

 ArrayList<Integer> items = new ArrayList<>();
 ​
 items.add(4);
 items.add(5);
 items.add(2);
 items.add(1);
 items.add(3);
 ​
 items.sort(new Comparator<Integer>() {
   @Override
   public int compare(Integer o1, Integer o2) {
     return o2.compareTo(o1);
   }
 });
 ​
 items.forEach(System.out::println);

在Kotlin中,可以使用object关键字实现,如下

 val items = ArrayList<Int>()
 items.add(4)
 items.add(5)
 items.add(2)
 items.add(1)
 items.add(3)
 ​
 items.sortWith(object : Comparator<Int> {
   override fun compare(o1: Int, o2: Int): Int {
     return o2.compareTo(o1)
   }
 })
 ​
 items.forEach { println(it) }

所以本质上是一样的,参数都是传入一个对象,object声明的就是一个对象,您只需要了解Kotlin中object表达式的功用即可。

对了,如上代码在Intellij IDEA中可能会建议您使用labmda表达式去实现,类似这样,会更加简洁

 items.sortWith { o1, o2 -> o2.compareTo(o1) }

这些我们后面会提及,并分析object和labmda表达式的一些场景

简单总结:

伴生对象其实就是替换Java中用static修饰的静态成员和方法,让代码的语义更清晰一点,层次分明

并且object天生支持单例模式,这个是一个非常好的特性,可以极大的减少代码量

以及object表达式,与Java中匿名内部类相似,了解就好

8、密封类

Kotlin中密封类用关键字sealed声明,Kotlin中引入密封类是限制类的继承,从而解决继承的安全性和可控性的问题

继承密封类的直接子类必须在同一文件中定义,其实也就是说要在同一个包下,通过这种限制就可以避免不必要的继承和多态引起的意外问题。

此外,密封类还可以实现类似枚举的功能,并且更加灵活和可扩展。它经常会配合when表达式一起使用。很多情况下我们已知可以匹配有限种可能的情况,我们就可以利用它继承的特性来搭配when表达式实现枚举的功能,比如

 sealed class Animal {
     class Dog(val name: String) : Animal()
     class Cat(val name: String) : Animal()
     object Bird : Animal() 
 }
 ​
 fun distinguish(animal: Animal) {
     when (animal) {
         is Animal.Dog -> println(animal.name)
         is Animal.Cat -> println(animal.name)
         is Animal.Bird -> println("bird")
     }
 }
 ​
 fun main() {
     val dog = Animal.Dog("dog")
     val cat = Animal.Cat("cat")
     val bird = Animal.Bird
 ​
     distinguish(dog) // dog
     distinguish(cat) // cat
     distinguish(bird) // bird
 }

如上代码就轻松地实现了枚举类的功能,创建了一个sealed class Animal,包含DogCatBird三个子类(Bird是通过object表达式直接实例化的对象),我们用when表达式分别处理了不同类型的错误,而且因为Animal是一个密封类,所以确保了distinguish函数中when表达式涵盖了所有的可能情况,无需使用else就避免了意外的错误。

简单总结:

密封类通过限制类的继承解决了安全性和可控性的问题

密封类还可以搭配when表达式实现类似枚举的功能,并且可以拥有状态,更加灵活,并且可以提高代码的可读性

9、数据类

其实通过上面一些Java代码的例子我们可以看出来Java中一般会掺杂有构造方法、getter和setter方法、重写hashcode()、equals()方法、toString()方法等,很多时候需要封装一些数据类。比如在做Java服务端开发时很多时候需要封装一些实体类,这些实体类本身没有什么业务逻辑,但是仍然要写上面那些琐碎的东西,当然,Intellij IDEA工具中都可以自动生成这些,或者lombok可以解决这个问题。

而在Kotlin中,引入了数据类的概念,data class,Java的写法这里就不展示了,直接看Kotlin中数据类的使用

 data class User(val name: String, val age: Int)

上面这行简短的代码其实做了很多事情,编译器根据主构造函数中声明的所有属性自动生成以下函数

  • equals() and hashcode()
  • toString() 格式是"User(name=abprogramming, age=20)"
  • componentN()函数,按照主构造函数中的声明顺序排序
  • copy()函数

数据类的定义需要满足以下的条件

  • 主构造函数不能为空,至少要包含一个参数(否则数据类根本没有意义)
  • 主构造函数中的参数必须要表示val或者var(即声明变量为数据类中的属性)
  • 数据类不可以声明是abstractopensealedinner(这一些我们上面已经说过)
  • 数据类不能继承其他类,不过可以实现接口

下面简单扩宽一下copy()函数和componentN()函数

首先说一下copy()函数,这个函数是数据类中特有的,其实就是拷贝一个对象,但要注意这个函数是浅拷贝,那经常写Java的小伙伴肯定对浅拷贝和深拷贝不陌生,这里不说Java了,就用Kotlin来简单解释一下深拷贝和浅拷贝

首先,我这边有一个对象a还有一个对象b他们都是同一个类型,a是空,b已经初始化,那我们如果像将b拷贝到a应该怎么做?val a = b是这样吗?答案肯定不是的,这样的话a和b其实是同一个对象,因为他们是同一个引用,指向同一个地址。你如果修改了b的某一个地方你会发现a会跟着变。

那接下来测试一下copy()函数能否做到对象拷贝(这里为了方便测试先将属性都改为var修饰)

 fun main() {
     val s1 = Student("Jack", 18)
     val s2 = s1.copy()
 ​
     s2.name = "AB"
     s2.age = 20
 ​
     println(s1)
     println(s2)
 }
 ​
 data class Student(var name: String, var age: Int)

输出如下

 Student(name=Jack, age=18)
 Student(name=AB, age=20)

我们可以看到我们通过copy()函数得到s2对象,修改了s2的属性并不影响s1,也就是表明它们并不是同一处引用,它们是两个不同的对象。当然,严谨的话我们也可以输出它们的哈希码来看,当然您的对象中通过hashcode()函数计算出的哈希码也许与我不同

 println(s1.hashCode())
 println(s2.hashCode())
 71328755
 64529

但是如果不使用copy()函数而是通过val s2 = s1这种方式得到的哈希码肯定是相同的

由上我们证明出copy()函数确实可以拷贝一个新的对象,但是为什么我们说它是浅拷贝,如下

 fun main() {
     val s1 = Student("Jack", 18, School("一中"))
     val s2 = s1.copy()
 ​
     s2.name = "AB"
     s2.age = 20
     s2.school.name = "二中" // 不要写成 s2.school = School("二中") 这种形式,因为这样是您自己手动去创建了一个school对象
 ​
     println(s1)
     println(s2)
 }
 ​
 data class Student(var name: String, var age: Int, val school: School)
 ​
 data class School(var name: String)

输出如下

 Student(name=Jack, age=18, school=School(name=二中))
 Student(name=AB, age=20, school=School(name=二中))

我们会发现,我们在Student数据类中新加了一个School类型的属性,这是一个引用数据类型,也就是说第三个参数接收一个School类型的对象,而我们将拷贝后得到的s2的school属性的name属性改为“二中”,s1的school属性的name属性也跟着变成了“二中”,这也就说明了我们的s1和s2的school属性它们是同一处引用!它们是同一个对象!copy()函数并没有帮我们拷贝它!

可以通过下图理解

20.png

那其实我们也就看懂了,其实深拷贝和浅拷贝的区别就在于引用数据类型的属性是否拷贝

稍微补充一点,其实在Kotlin中,并没有Java中所谓的基本数据类型与引用数据类型之分,Kotlin中一切皆对象,包括Int、Long、Double等,但是我们习惯称Int、Double这种为基本数据类型。那么在Kotlin中Byte、Short、Int、Long、Float、Double、Boolean、Char、String这一些copy()函数都会帮我们拷贝,我们修改其中一个对象中这种类型的值不会影响另一个对象,而像Array、List以及您自己定义的class这些引用的类型就不会帮我们深度拷贝

那我们其实也可以简单实现一下深度拷贝

 fun main() {
     val s1 = Student("Jack", 18, School("一中"))
     val s2 = s1.copy()
 ​
     s2.name = "AB"
     s2.age = 18
     s2.school.name = "二中" // 不要写成 s2.school = School("二中") 这种形式,因为这样是您自己手动去创建了一个school对象
 ​
     println(s1)
     println(s2)
 }
 ​
 data class Student(var name: String, var age: Int, val school: School) {
     fun copy() = Student(this.name, this.age, School(this.school.name))
 }
 ​
 data class School(var name: String)

输出如下

 Student(name=Jack, age=18, school=School(name=一中))
 Student(name=AB, age=18, school=School(name=二中))

这样做我们就实现了一个简单的深度拷贝,即一个对象的引用属性改变不会影响另一个对象。做法也很简单,重新写一下copy()函数,这样就会调用我们自己写的copy()函数

最后关于copy()函数补充一点,如果您数据类中的属性是用val来修饰的,您可以在使用copy()函数拷贝对象时传入参数指定您想要改变其中一个属性值,例如

 val s2 = s1.copy(age = 20)

接下来关于componentN() 简单了解一下,其实我们前面已经接触过了一些结构赋值,比如我们标准输入操作的时候

 val (a, b, c) = readln().replit(" ")

就像上面这样,我们从标准输入中读取三个用空格分割的值,而对于data class我们也可以这样解构赋值,比如

 fun main() {
     val s1 = Student("Jack", 18, School("一中"))
     val (name, age, school) = s1
     println("$name $age $school")
 }
 ​
 data class Student(var name: String, var age: Int, val school: School) {
     fun copy() = Student(this.name, this.age, School(this.school.name))
 }

其实它背后就用到了componentN()函数,我们还可以通过下面这种方式去取值(按照主构造函数中的形参顺序取值)

 fun main() {
     val s1 = Student("Jack", 18, School("一中"))
     val name = s1.component1()
     val age = s1.component2()
     val school = s1.component3()
     println("$name $age $school")
 }
 ​
 data class Student(var name: String, var age: Int, val school: School) {
     fun copy() = Student(this.name, this.age, School(this.school.name))
 }

简单总结:

data class可以有效的简化开发,减少代码量。对于一些不具有业务逻辑的实体类无需重写toString()、hashCode()、equals()等方法,并且数据类的定义还要满足特定条件

copy()函数可以实现对象的浅拷贝,componentN()函数就可以实现按照顺序获取属性以及解构赋值等

10、封装和多态

封装

Kotlin支持很多面向对象的概念,而封装是面向对象思想中非常重要的一个,封装是指将数据和行为封装在一个类中,并限制对这些数据和行为的直接访问。这样做的好处是可以隐藏类的内部细节,只向外部暴露信息,防止外部对象不经意的修改类的状态。

下面根据前面的内容基础简单介绍一下Kotlin中通常封装的实现方式

  • 访问控制修饰符:public、protected、private和internal可以帮助我们来限制类、接口、属性、方法和构造函数
  • getter and setter方法:很简单,它可以控制对属性的读取和写入
  • 数据类(data class):Kotlin中的数据类本来就是被设计为数据的存储,它通常有很多的属性和方法来简便我们的访问和操作
  • 接口与抽象类:接口和抽象类可以用来定义公共的接口,通过这些接口使得外部对象可以与类进行交互,但是无法访问类的内部状态细节,这就很好的帮助我们封装了类的内部细节

多态

多态也是面向对象中一个非常重要的概念,它使得代码更加的灵活和易于扩展

在Kotlin中实现多态有很多种方式:

  • 继承和重写:这是一种很好理解的实现多态的方式,子类重写父类的方法。比如两个类同时实现了一个父类,重写了父类的共同方法以及属性,我们对这两个类进行实例化的时候接收的变量声明为它们的父类类型,调用时会调用不同的子类方法。
  • 接口和抽象类:接口和抽象类定义类的公共接口,具体实现由子类负责,很多时候就建议使用接口或者抽象类来进行一个多态的实现,就比如说下面将要学习的委托。
  • 泛型:泛型的内容后面会提及,我们现阶段只需了解,泛型也是实现多态的一种手段

简单总结:

封装可以隐藏类的内部细节,只向外暴露信息

多态可以使得对象之间的结构层次更加灵活,易于扩展

文章若有错误或不足之处,欢迎大家评论区指正,谢谢大家!

另外,欢迎大家来了解一下济南KUG(Jinan Kotlin User Group),如果您对Kotlin技术分享抱有热情,不妨加入济南KUG,济南KUG官网:济南KUG