跳到主要内容

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 比较
publicKotlin 默认修饰符,全局可见与 Java 中显式指定的 public 效果一致
protected受保护修饰符,类和子类可见与 Java 一致,除了类和子类可见,其包内也可见
private私有修饰符,只有本类可见,类外文件内可见只能类内可见
internal模块内可见无该修饰符

8. 总结

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