从Kotlin的类开始说起
欢迎来到kotlin的世界,Kotlin 是一个用于现代多平台应用的静态编程语言,它可以编译成Java字节码,在JVM平台上运行,并且可以完全兼容Java。它有很多优点,如:如空指针检查、高阶函数、函数扩展等等。2017Google IO大会上,指定Kotlin作为Android开发的官方语言。因此,如果你是一个Android开发者,该学习使用kotlin来进行开发了。
如何开始学习Kotlin呢?在面向对象编程中,我们说万物皆对象,任何事物都可以进行抽象和封装成一个对象来表达它所具有的属性和特征。Kotlin作为一种现代面向对象编程语言,因此我们就从类和对象开始来认识它,本篇本章就来讲讲Kotlin中的所有类。
image
1 . Kotlin中的类
1.1、Java中的类
在认识Kotlin的类之前,我们来先看看我们熟悉的Java类,一段Java代码如下:
class Person {
private String name;
private int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
/**
* 方法
*/
public void walk(){
System.out.print("person walk...");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Java类特征如下:
-
class
关键字声明一个类,形式如:class 类名 {} - 类可以有一个或者多个构造函数,如果没有显示的构造函数,会默认有一个无参构造函数
- 类中可以声明属性和方法,私有属性用相应的
getXXX()
和setXXX()
方法
一起来看一下,和上面功能功能一样的kotlin类:
class Person(var name:String,var age:Int){
/**
* 函数
*/
fun walk():Unit{
println("Person walk...")
}
}
1.2 . Kotlin中的类
Kotlin的类的声明形式为:
class 类名 [可见性修饰符] [注解] [constructor] (Params){
...
}
其中,{}
中的 的内容成为类体,类名与类体之间的内容称为类头,在kotlin中,类头和类体是可以省略的,[]
中的类容也是可选的。比如一个完整的类声明如:
class Person private @Inject constructor(name: String) { …… }
如果没有可见性修饰符和注解,类头可以省去,那么可以简写成如下:
class Person(name: String) { …… }
如果构造函数(kotlin的构造函数将在下文讲解)没有参数,可以写成如下:
class Person { …… }
如果类体里面也没有类容的话,类体也可以省略,如下:
class Person
2 . 构造函数
上面提到了构造函数,熟悉Java的同学都知道,Java也有构造函数,Java中的构造函数有如下特点:
- 一个Java类可以有多个构造函数,构造函数之间是重载的
- 可以不给Java类显示声明构造函数,但是会默认生成一个无参的构造函数
- 如果显示的生成了构造函数,则在
new
对象的时候,不能使用默认的无参构造函数,除非显示的生成一个无参构造函数。
如:多个重载的构造函数:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this.name = name;
}
}
// 生成对象时
Person person1 = new Person("Paul",30);
Person person2 = new Person("jake");
Person person3 = new Person();//编译错误,没有无参构造函数
也可以生明构造函数,但是它会有一个默认的无参构造函数:
public class Person {
private String name;
private int age;
}
// 生成对象时
Person person = new Person();//使用默认无参构造函数
回到Kotlin ,在Kotlin中,一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。与Java稍有不同,Kotlin有主构造函数和次构造函数之分。
2.1 主构造函数
一个带有主构造函数的类声明如下:
class Cat constructor(name: String){
...
}
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字。
class Cat(name: String){
...
}
注意,Kotlin主构造函数是不能包含任何代码的,但是有时候我们又需要在构造函数中做一些初始化的操作,这咋办呢?Kotlin 引入了初始化代码块,用 关键字init声明,需要在主构造函数中初始化的操作可以放到初始化代码块中,如:
class Cat(name: String){
//初始化代码块
init {
// 在这里面做一些需要在主构造函数中做的初始化操作
println("第一个初始化代码块,name:$name")
}
}
// 使用如下:
fun main(args: Array<String>) {
var cat: Cat = Cat("喵喵")
}
执行结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java
第一个初始化代码块,name:喵喵
Process finished with exit code 0
可以看到,生成一个Cat对象的时候,执行了初始化代码块。有2点值得注意的是:
- 1,初始化代码块和类体都可以访问主构造函数的参数,如上面的例子,可以访问
name
参数 - 2,类体中可以有多个初始化代码块,它们的执行顺序与在类中声明的顺序一样
多个初始化代码块例子:
class Cat(name: String){
//类体中也可以访问构造函数的参数
val catName:String = "catName:$name"
//初始化代码块
init {
// 在这里面做一些需要在主构造函数中做的初始化操作
println("第一个初始化代码块,name:$name")
}
init {
println("第二个初始化代码块,name:$name")
}
init {
println("第三个初始化代码块,name:$name")
}
}
// 使用如下:
fun main(args: Array<String>) {
var cat: Cat = Cat("喵喵")
println(cat.catName)//打印属性
}
执行结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java
第一个初始化代码块,name:喵喵
第二个初始化代码块,name:喵喵
第三个初始化代码块,name:喵喵
catName:喵喵
Process finished with exit code 0
上面的代码中,我们在类体中声明了一个属性catName
,在Kotlin中声明属性其实有更简单的方法,那就是在主构造函数中声明类属性:
方式一:
class Person(var name:String ,var gender: Int)
方式二:
class Person{
var name:String = ""
var gender:Int = 0
//这是次构造函数
constructor(name: String, gender: Int) {
this.name = name
this.gender = gender
}
}
上面2种方式声明属性是等价的,声明了2个类属性name
和gender
,可以看出,通过主构造函数声明属性简洁了很多。
在Kotlin中,函数的参数是可以设置默认值的,如果调用的时候不传对应参数,就使用默认值,主构造函数声明类属性也一样,也可以设置默认值。 代码如下:
class Person(var name: String= "" ,var gender: Int= 0)
2.2 次构造函数
Kotlin 中,类也可以有次构造函数,次构造函数在类体中用关键字constructor声明,代码如下:
class Person {
var name: String = ""
var gender: Int = 0
//次构造函数
constructor(name: String, gender: Int){
this.name = name
this.gender = gender
}
}
如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:
class Person(name: String){
var name: String = name // 主构造函数参数赋值
var gender: Int = 0
constructor(name: String, gender: Int) : this(name) {
this.name = name
this.gender = gender
}
}
请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块:
class Person(name: String){
var name: String = name
var gender: Int = 0
init {
println("这是初始化代码块...")
}
constructor(name: String, gender: Int) : this(name) {
println("这是次构造函数...")
this.name = name
this.gender = gender
}
}
// 运行程序
fun main(args: Array<String>) {
var person = Person("Paul",30)
}
打印结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java
这是初始化代码块,name:Paul
这是次构造函数,name:Paul,gender:30
Process finished with exit code 0
从结果看,先执行初始化块,再执行次构造函数。
在Java中,如果没有声明任何构造函数,会有一个默认的无参构造函数,这有利于通过这个无参构造函数创建类的对象,但是有些情况,比如单例模式,不希望外部构造对象,我们只需私有化一个无参构造函数就行,Java代码如下:
class Singleton{
// 私有化构造函数,外部不能直接创建对象
private Singleton(){}
public static Singleton getInstance(){
return new Singleton();
}
}
在Kotlin中也类似,如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数:
class DontCreateMe private constructor () { ... }
kotlin构造函数小结:
1, 可以有一个主构造函数和多个次构造函数
2,可以只有主构造函数或者只有次构造函数
3,主、次构造函数同时存在的时候,次构造函数必须直接或者间接地委托到主构造函数
4,没有声明主构造函数或者次构造函数时,会有一个默认的无参数主构造函数,方便创建对象,这与Java一样
5,如果不希望类有公有构造函数,那么请私有化一个无参数主构造函数
3 . 抽象类
和Java一样,在kotlin中,抽象类用关键字abstract修饰,抽象类的成员可以在本类中提供实现,也可以不实现而交给子类去实现,不实现的成员必须用关键字abstract声明:
abstract class AbsBase{
abstract fun method()
}
在kotlin中,被继承的类需要用关键字open声明,表明该类可以被继承,但是抽象类或者抽象函数是不用 open 标注的,因为这不言而喻。但是如果子类要实现抽象类的非抽象函数,需要在抽象类中将其声明为open
abstract class AbsBase{
abstract fun method()
// 如果子类要实现需声明为抽象
open fun method1(){
println("非抽象方法如果要类子类实现,需要声明为open")
}
}
class Child : AbsBase() {
override fun method() {
}
override fun method1() {
super.method1()
println("子类实现")
}
}
另外,抽象成员可以覆盖一个非抽象成员:
abstract class AbsBase{
open fun method1(){
println("非抽象方法如果要类子类实现,需要声明为open")
}
}
abstract class AbsChild :AbsBase(){
// 将父类的非抽象方法覆盖为一个抽象方法
abstract override fun method1()
}
抽象类小结:
跟Java 的抽象类几乎一样,熟悉Java的同学很容易理解。
4 . 数据类
在Java中,我们会经常创建一些保存数据的类,xxxModule
或者xxxEntry
,主要用在网络请求中,保存api接口返回的数据,如一个 Java 的User
类:
public class User {
private String name;
private int gender;
private String avatar;
private int age;
public User(String name, int gender, String avatar, int age) {
this.name = name;
this.gender = gender;
this.avatar = avatar;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getGender() {
return gender;
}
public void setGender(int gender) {
this.gender = gender;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
一个复杂点的实体类,动不动就几十几百行代码。臃肿而且麻烦。
在Kotlin 中,引入了一种特殊的来来解决这个问题,叫做数据类,用关键字data标记,kotlin数据类代码如下:
data class User(var name:String,var age:Int,var gender: Int,var avatar: String)
Java 中几十行的数据类,在kotlin中,一行就搞定。为什么kotlin 能这么简单,那是因为编译器为我们做了许多事,编译器会自动的从主构造函数中根据所有声明的属性提取以下函数:
- equals()/hashCode() ;
- toString() 格式是 "User(name=John, age=42)";
- componentN() 函数 按声明顺序对应于所有属性;
- copy() 函数
也就是说编译器自动为我们的数据类生成了以上个函数,前面几个熟悉Java的同学知道,Java 中也有,再此不表。componentN
和copy
函数使数据类有了2个特性:
- 1 . 解构
- 2、数据类复制
4.1 . 解构
解构是什么意思呢?就是把一个对象拆解成对应的多个变量,比如上面的User
类 ,我们可以把它的4 个属性拆解出来:
fun main(args: Array<String>) {
// 创建一个User对象
var user = User("Paul",30,1,"https:qiniu.com/w200/h200.png")
// 解构
val(name,age,gender,avatar) = user
// 打印
println("name:$name,age:$age,gender:$gender,avatar:$avatar")
}
打印结果:
name:Paul,age:30,gender:1,avatar:https:qiniu.com/w200/h200.png
Process finished with exit code 0
为什么数据类可以解构呢?是因为编译器自动帮数据类,生成了componentN
函数,
其中N代表有N个 component
函数,这取决于数据类主构造函数声明属性的个数,如上面的例子,有4个属性,那么就有4个component
函数:
component1()
component2()
component3()
component4()
4个函数对应4个component函数,按在主构造函数声明属性的顺序对应。
上面的解构会被编译成:
val name = user.component1()
val age = user.component2()
val gender = user.component3()
val avatar = user.component4()
Q: 普通的类可以解构吗?
A: 可以,为其声明component函数
4.2 复制copy
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:
fun copy(name: String = this.name, age: Int = this.age,gender: Int = this.gender,avatar: Int = this.avatar) = User(name, age,gender,avatar)
比如我们复制了一个User类:
var user = User("Paul",30,1,"https:qiniu.com/w200/h200.png")
// 想再创建一个对象只改变年龄
var user2 = user.copy(age = 31)
// 改变名字和年龄,其他不变
var user3 = user.copy(name = "Dw",age = 33)
println(user.toString())
println(user2.toString())
println(user3.toString())
运行结果如下:
1535613834079
注意:数据类也可以在类体中声明属性,但是在类体中声明的属性不会出现在那些自动生成的函数中,如:
data class User(val name: String) {
var age: Int = 0
}
因为主构造函数只有name,因此在 toString()、 equals()、 hashCode() 以及 copy() 的实现中只会用到 name 属性,只有一个component1函数,对应name。如果你创建2个对象,名字相同,年龄不同,但是会被视为相等。user1 == user2
数据类小结
数据类需满足以下要求:
1、主构造函数需要至少有一个参数;
2、主构造函数的所有参数需要标记为 val 或 var;
3、数据类不能是抽象、开放、密封或者内部的;
5 . 枚举类
kotlin 中的枚举类与Java 中的枚举类差不多,简单的说一下:
1、枚举用关键字enum
声明,与Java不同,紧跟后面是class (Java声明枚举没有class关键字)枚举类的声明形式如下:
enum class 类名{
常量1,
常量2,
...
}
如:
enum class Direction{
WEST,
EAST,
NORTH,
SOUTH;
}
2、枚举类默认有2个属性ordinal
和 name
:
- ordinal 属性:枚举常量的顺序,从0开始
- name属性: 枚举常量的名字
以上面的枚举类Direction
为例:
Direction.WEST.ordinal // 0
Direction.WEST.name // WEST
3、枚举类默认又2个方法:values()
和valueOf()
- values : 获取所有枚举常量
- valueOf() : 获取对应枚举常量
// 遍历
Direction.values().forEach {
println("value:${it.ordinal}")
}
// 获取"EAST"对应枚举常量,如果枚举类中没这个常量会抛异常
val direction = Direction.valueOf("EAST")
4、枚举常量可以有构造函数和自有属性、方法,自定义方法需放在;
后,每一个枚举常量都是一个实例,调用构造函数初始化。
enum class Season(var enumName: String,var range: String){
Spring("春季","1-3"),
Summer("夏季","4-6"),
Fall("秋季","7-9"),
Winter("冬季","10-12");
fun printSeason(){
print("name:$enumName,range:$range")
}
}
6. 密闭类
密闭类定义如下:密闭类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密闭类的一个子类可以有可包含状态的多个实例。
这么长一串定义,看得一脸懵逼,没关系,稍候解释。先来看一下如何声明一个密闭类
密闭类用 sealed 修饰符 ,密闭类的字类必须与密闭类在同一文件中(子类也可以嵌套在密闭类的内部)
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
子类在内部:
sealed class Expr{
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
}
有几点需要注意:
-
1、一个密闭类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员
-
2、密闭类不允许有非-private 构造函数(其构造函数默认为 private)
-
3、扩展密闭类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。
密闭类算是枚举类的扩展,用法和枚举类相似,经常配合when
表达式使用,使用例子如下:
fun eval(expr: Expr): Double = when(expr) {
is Expr.Const -> expr.number
is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
Expr.NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
fun main(args: Array<String>) {
val const = eval(Expr.Const(12.0))
val sum = eval(Expr.Sum(Expr.Const(10.0),Expr.Const(12.0)))
println("const:$const")
println("sum:$sum")
}
// 执行结果:
const:12.0
sum:22.0
作为初学者,密闭类是Kotlin 中比较难以理解的一个类,其实看完上面的例子还是很难理解它到底能干嘛,感觉它做的事儿,枚举类也能做到,前面说它算是对枚举类的扩展,那么他就应该能做到枚举类做不到的事。网上看到一篇博客用View 显示和隐藏来举例,顿时茅舍顿开。如下:
场景:假如在 Android 中我们有一个 view,我们现在想通过 when 语句设置针对 view 进行两种操作:显示和隐藏,那么就可以这样做:
sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
}
//定义了一个操作View的方法
fun viewOperator(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
}
以上功能其实完全可以用枚举实现,但是如果我们现在想加两个操作:水平平移和纵向平移,并且还要携带一些数据,比如平移了多少距离,平移过程的动画类型等数据,用枚举显然就不太好办了,这时密封类的优势就可以发挥了,现在密闭类中添加2个操作:
sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
class TranslateX(val px: Float): UiOp() // 水平移动
class TranslateY(val px: Float): UiOp()//垂直移动
}
接着在when
表达式添加两个移动的case
fun execute(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px // 这个 when 语句分支不仅告诉 view 要水平移动,还告诉 view 需要移动多少距离,这是枚举等 Java 传统思想不容易实现的
is UiOp.TranslateY -> view.translationY = op.px
}
以上代码中,TranslateX 是一个类,它可以携带多于一个的信息,比如除了告诉 view 需要水平平移之外,还可以告诉 view 平移多少像素,甚至还可以告诉 view 平移的动画类型等信息,这大概就是密封类出现的意义吧。
看到这个场景演示后,是不是就觉得拨开云雾见月明了呢?好理解多了吧!
7. 嵌套类
一个类可以嵌套在另一个类里面
class Outer{
val attr = 0
// 嵌套类
class Nested{
fun inMethod(){
// 不能访问外部类的属性
println("内部类")
}
}
}
fun main(args: Array<String>) {
// 调用嵌套类方法
Outer.Nested().inMethod()
}
嵌套类不能访问外部类的属性,它其实就相当于Java 中的静态内部类,我们把它翻译成Java 代码,Nested
其实就是一个静态内部类,如下:
public final class Outer {
private final int attr;
public static final class Nested {
public final void inMethod() {
String var1 = "内部类";
System.out.println(var1);
}
}
}
7.2 内部类
Kotlin中,内部类用inner关键字,声明,跟Java 一样,内部类持有一个外部类的对象引用,可以访问外部类的属性和方法。
class Outer{
private val attr = 10
inner class Inner{
fun method(){
println("内部类可以访问外部类属性:$attr")
}
}
}
fun main(args: Array<String>) {
// 调用内部类方法,看出区别了吗
Outer().Inner().method()
}
注意调用方法,嵌套类通过类直接调用(Java静态方法方式),内部类通过对象调用。
7.3 匿名内部类
来看一下Java的匿名内部类:
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// onClick
}
});
在kotlin中,匿名内部类用对象表达式创建,给View设置点击事件的kotlin代码如下:
mButton?.setOnClickListener(object: View.OnClickListener{
override fun onClick(v: View?) {
//onClick
}
})
8 . 总结
八月份初的时候就在写着篇文章,前前后后差不多1个月左右,这篇文章终于写完了,本篇文章看完算是对Kotlin 中的类能有一个完整了解,由于涉及的内容比较多,篇幅太长,关于类的继承、对象和对象表达式、属性和方法 这些另开篇幅吧。Kotlin 的的一些中文官方文档翻译得比较生硬,有的不好理解,本文有些知识点我尝试通过Java 代码对比的方式讲解,希望能好理解一点。如果有什么错误的地方,欢迎指出。