Kotlin 类和对象

从这篇文章开始我们一起正式进入 Kotlin 面向对象的世界,Kotlin 实际上也是一门面向对象的语言但同时又兼顾了函数式编程语言。只不过函数在 Kotlin 中的地位被提升至一等公民。但是在 Kotlin 中也是有类、对象、属性、方法等。

1. Kotlin 中的类

在 Kotlin 中类和 Java 中概念基本是一致的,都是使用 class 关键字来声明一个类,一个类中可以用属性表示一个类的状态,可以用方法来表示一个类的行为。但是与 Java 不同的是 Kotlin 中的类声明默认就是 finalpublic , 所以在 Kotlin 中不能直接继承一个类,因为默认类是 final 的,此外也不需要像 Java 中一样显式使用 public 修饰符。

//Student.java
public class Student {//public修饰符
    private String name;
    private String nickName;
    private int age;

    public Student(String name, String nickName, int age) {
        this.name = name;
        this.nickName = nickName;
        this.age = age;
    }
}

//SeniorStudent.java
public class SeniorStudent extends Student {//直接继承Student类
    public SeniorStudent(String name, String nickName, int age) {
        super(name, nickName, age);
    }
}

而在 Kotlin 中不能直接继承一个类,如果需要继承一个类则需要在基类上加 open 关键字修饰。

 open class Student(
    private val name: String,
    private val nickName: String,
    private val age: Int
)//Student类被继承需要加open关键字,此外Kotlin中构造器初始化也省去了很多模版代码

class SeniorStudent(
    private val name: String,
    private val nickName: String,
    private val age: Int
) : Student(name, nickName, age)//在Kotlin中继承不再使用extends关键字而是使用:来替代

2. 类的定义

在 Kotlin 中和 Java 一样都是使用 class 关键字修饰对应类的名称即可。在类中会有属性描述类的对象状态,方法描述类的对象方法。

class Bird {
    val color: String = "green"//类的属性描述类的对象的状态
    val age: Int = 3
    
    fun fly() {//类的方法描述类的对象的行为
      println("I can fly!")
    }
}

我们可以上述 Kotlin 代码反编译成 Java 代码,会发现虽然 Kotlin 和 Java 声明方法基本类似,但是还是存在一些不同的

public final class Bird {//可以看到java中自动加上public,进一步证明了在Kotlin默认是public访问,而java默认是包可见。
    //此外还可看到Bird使用了final修饰,所以也就进一步证明Kotlin中默认所有都是final修饰,也就意味这个类默认是不能被继承的。
   @NotNull
   private final String color = "green";//final修饰,是因为在Kotlin中使用的是val修饰成员变量,所以可以看到kotlin val就是使用Java中的final实现的。那么如果使用var修饰就不需要final了。
   private final int age = 3;

   @NotNull
   public final String getColor() {//由于是val修饰,所以color属性只会有对应getter方法,没有setter方法
      return this.color;
   }

   public final int getAge() {
      return this.age;
   }

   public final void fly() {//可以看到fly函数是final修饰,也就进一步证明Kotlin中默认所有都是final修饰,那么这个fly是不能被子类重写的
      String var1 = "I can fly!";
      boolean var2 = false;
      System.out.println(var1);
   }
}

3. 更简单构造类的对象

在 Kotlin 中构造对象不再需要 new 关键字了,而是直接调用类的构造器方法就可以创建一个对象了。例如以下代码:

val bird = Bird() // 省略了new关键字,直接创建Bird对象

当然也可以创建带参数的对象,Kotlin 只需要将上述 Bird 类修改为带默认参数的构造器即可,而在 Java 中则需要增加一个重载构造器函数,但是相比你会发现 Kotlin 更为方便和简洁。
Java 实现:

class Bird {
    private String color;
    private int age;
    public Bird(String color, int age) {
        this.color = color;
        this.age = age;
    }
    
    public void fly() {
        println("I can fly!");
    } 
}

Bird brid = new Bird("blue", 7);//java创建一个带参数Bird对象

Kotlin 实现:

class Bird(val color: String = "green", val age: Int = 3) {
    fun fly() {
        println("I can fly!")
    }
}

val brid = Bird(color = "blue", age = 7)//创建一个带参数Bird对象

4. 类的构造器函数

在 Kotlin 中构造器函数是存在 “主从” 关系,这点是 Java 中不存在的,也就是常说的主构造器函数和从构造器函数。比如在上述 Bird 类中需要新增一个带类型 (type) 属性的构造器,就可以定义从构造器,从构造器是利用 constructor 关键字声明。

class Bird(val color: String = "green", val age: Int = 3) { //主构造器
    constructor(
        color: String = "green",
        age: Int = 3,
        type: String
    ) : this(color, age) {//使用constructor声明从构造器,:this(color, age)从构造器直接委托调用主构造器函数
        //do logical
    }

    fun fly() {
        println("I can fly!")
    }
}

fun main() {
    val smallBird = Bird(color = "blue", age = 8, type = "small")
}

需要注意的是,在 Kotlin 中默认类都会存在一个无参主构造器函数,除非我们手动指定。此外如果一个存在主构造器,那么从构造器函数就会直接或间接委托调用主构造器,直接委托给主构造器就类似上述例子中的 : this(color, age) ,当然可以通过从构造器 A 委托从构造器 B,然后从构造器 B 委托给主构造器,从而达到间接委托作用。

class CustomView : View {
    constructor(context: Context) : this(context, null)//从构造器A委托调用从构造器B
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)//从构造器B委托调用从构造器C

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {//从构造器C委托调用主构造器

    }
}

5. init 初始化块

与 Java 不同的是在 Kotlin 中还存在 init 初始化块的概念,它属于构造器函数一部分,只是在代码形式看似两者是分离的。如果我们需要在初始化时进行其他的额外操作时,这时候就需要 init 语句块来执行,有个有趣的点需要注意的是,在 init 初始化块中,是可以直接访问构造器函数中参数的。

class Bird(val color: String = "green", val age: Int = 3) {
  //...
}
//上述代码实际上等同于下面代码
class Bird(color: String = "green", age: Int = 3) {
    val color: String = color
    val age: String = age
}

//所以针对没有val修饰构造器函数参数,只能在init初始化块中访问,而一般成员函数是无法访问的
class Bird(color: String = "green", age: Int = 3) {//当color没有val修饰
    init {
        println("color: $color")//可以看到在init块中使用构造器函数中的color参数
    }
    fun printInfo() {
        println(color)//非法访问
    }
}

对于 init 初始化块,是可以存在多个的,它们执行顺序是从上到下依次执行。

class Bird(color: String = "green", age: Int = 3) {
    init {
        println("color: $color")//init块1
    }

    init {
        println("age: $age")//init块2
    }
}    

//执行的顺序是,先输出init块1中日志再输出init块2中的日志

对于 init 初始化块和从构造器同时存在,它们的执行顺序是怎么样的呢?是先执行完所有的 init 初始化块,再执行从构造器函数中代码。
可以上述例子修改一下即可:

class Bird(color: String = "green", age: Int = 3) {
    init {
        println("color: $color")//init块1
    }

    init {
        println("age: $age")//init块2
    }

    constructor(color: String, age: Int, type: String) : this(color, age) {
        println("constructor executed")
    }
}

fun main() {
    val smallBird = Bird(color = "blue", age = 8, type = "small")
}

//输出结果
color: blue
age: 8
constructor executed
Process finished with exit code 0

6. 类的 setter,getter 访问器

与 Java 不同的是,需要手动创建 setter,getter 方法;即使现在很多 IDEA 插件工具可以自动生成,但是从语言层面来说还是比较啰嗦的。所以 Kotlin 直接在语言的层面省去了。先来对比一下:

public class Bird {
    private String color;
    private int age;
    private String type;

    public Bird(String color, int age, String type) {
        this.color = color;
        this.age = age;
        this.type = type;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

而对于 Kotlin 只需要简单一行即可达到以上实现:

class Bird(var color: String, var age: Int, var type: String)//var修饰则表示color属性会自动生成setter,getter方法,如果是val修饰表示只读,那么只会生成getter方法

为了进一步验证,看看这一行简单声明是否反编译成 java 代码是怎么样的

public final class Bird {
   @NotNull
   private String color;
   private int age;
   @NotNull
   private String type;

   @NotNull
   public final String getColor() {
      return this.color;
   }

   public final void setColor(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.color = var1;
   }

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   @NotNull
   public final String getType() {
      return this.type;
   }

   public final void setType(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.type = var1;
   }

   public Bird(@NotNull String color, int age, @NotNull String type) {
      Intrinsics.checkParameterIsNotNull(color, "color");
      Intrinsics.checkParameterIsNotNull(type, "type");
      super();
      this.color = color;
      this.age = age;
      this.type = type;
   }
}

7. 不同访问控制规则

7.1 自带默认的 final 修饰

在 Java 中我们经常会控制一个类不被修改或继承,则需要 final 修饰符修饰;而在 Kotlin 中不要手动添加 final 而是默认就是 final ,如果需要让这个类或方法被继承和修改,就需要手动添加 open 关键解除这个禁忌。

open class Animal(color: String, age: Int) {//open关键字打开final禁忌,使得Animal可以被继承
    open fun printInfo() {//open关键字打开final禁忌,使得printInfo可以被子类重写
        println("this is animal!")
    }
}

class Dog(color: String, age: Int) : Animal(color, age) {
    override fun printInfo() {
        println("this is dog!")
    }
}

我们也可以通过编译上述代码,看 Animal 类是否还存在 final 修饰符,来进一步证明我们结论。

public class Animal {//没有final可以被继承
   public void printInfo() {//没有final可以被子类重写
      String var1 = "this is animal!";
      boolean var2 = false;
      System.out.println(var1);
   }

   public Animal(@NotNull String color, int age) {
      Intrinsics.checkParameterIsNotNull(color, "color");
      super();
   }
}

7.2 可见性修饰符

在 Kotlin 中默认修饰符与 Java 则不一样,在 Kotlin 默认是 public 而 Java 则默认是 default (包级可见性)。此外 Kotlin 中还存在独有的 internal 访问可见修饰符。下面列出一张对应表格

修饰符 表示含义 与 Java 比较
public Kotlin 默认修饰符,全局可见 与 Java 中显式指定的 public 效果一致
protected 受保护修饰符,类和子类可见 与 Java 一致,除了类和子类可见,其包内也可见
private 私有修饰符,只有本类可见,类外文件内可见 只能类内可见
internal 模块内可见 无该修饰符

8. 总结

到这里有关 Kotlin 中面向对象的第一站就结束,回顾一下本篇文章主要介绍了 Kotlin 中类和对象定义和创建,以及类的构造函数、init 初始化块、可见性修饰符,并把这些特性语言一一和 Java 进行对比,帮助快速掌握和理解。下篇文章将继续 Kotlin 面向对象第二站抽象和接口。