从Kotlin的类开始提及

欢迎来到kotlin的世界,Kotlin 是一个用于现代多平台应用的静态编程语言,它能够编译成Java字节码,在JVM平台上运行,而且能够彻底兼容Java。它有不少优势,如:如空指针检查、高阶函数、函数扩展等等。2017Google IO大会上,指定Kotlin做为Android开发的官方语言。所以,若是你是一个Android开发者,该学习使用kotlin来进行开发了。html

如何开始学习Kotlin呢?在面向对象编程中,咱们说万物皆对象,任何事物均可以进行抽象和封装成一个对象来表达它所具备的属性和特征。Kotlin做为一种现代面向对象编程语言,所以咱们就从类和对象开始来认识它,本篇本章就来说讲Kotlin中的全部类。 java

image

1 . Kotlin中的类

1.一、Java中的类

在认识Kotlin的类以前,咱们来先看看咱们熟悉的Java类,一段Java代码以下:git

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类:api

class Person(var name:String,var age:Int){
    /** * 函数 */
    fun walk():Unit{
        println("Person walk...")
    }
}

复制代码

1.2 . Kotlin中的类

Kotlin的类的声明形式为:bash

class 类名 [可见性修饰符] [注解] [constructor] (Params){
     ...
  }  
复制代码

其中,{}中的 的内容成为类体,类名与类体之间的内容称为类头,在kotlin中,类头和类体是能够省略的,[]中的类容也是可选的。好比一个完整的类声明如:网络

class Person private @Inject constructor(name: String) { …… }
复制代码

若是没有可见性修饰符和注解,类头能够省去,那么能够简写成以下:编程语言

class Person(name: String) { …… }
复制代码

若是**构造函数(kotlin的构造函数将在下文讲解)**没有参数,能够写成以下:ide

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个类属性namegender,能够看出,经过主构造函数声明属性简洁了不少。

在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 中也有,再此不表。componentNcopy 函数使数据类有了2个特性:

  • 1 . 解构
  • 二、数据类复制
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

数据类小结 数据类需知足如下要求: 一、主构造函数须要至少有一个参数; 二、主构造函数的全部参数须要标记为 val 或 var; 三、数据类不能是抽象、开放、密封或者内部的;

5 . 枚举类

kotlin 中的枚举类与Java 中的枚举类差很少,简单的说一下:

一、枚举用关键字enum声明,与Java不一样,紧跟后面是class (Java声明枚举没有class关键字)枚举类的声明形式以下:

enum class 类名{
    常量1,
    常量2,
    ...
  }
复制代码

如:

enum class Direction{
    WEST,
    EAST,
    NORTH,
    SOUTH;
}
复制代码

二、枚举类默认有2个属性ordinalname:

  • ordinal 属性:枚举常量的顺序,从0开始
  • name属性: 枚举常量的名字 以上面的枚举类Direction为例:
Direction.WEST.ordinal // 0
 Direction.WEST.name // WEST
复制代码

三、枚举类默认又2个方法:values()valueOf()

  • values : 获取全部枚举常量
  • valueOf() : 获取对应枚举常量
// 遍历
    Direction.values().forEach {
        println("value:${it.ordinal}")
    }
    // 获取"EAST"对应枚举常量,若是枚举类中没这个常量会抛异常
    val direction = Direction.valueOf("EAST")
复制代码

四、枚举常量能够有构造函数和自有属性、方法,自定义方法需放在;后,每个枚举常量都是一个实例,调用构造函数初始化。

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()
}
复制代码

有几点须要注意:

  • 一、一个密闭类是自身抽象的,它不能直接实例化并能够有抽象(abstract)成员

  • 二、密闭类不容许有非-private 构造函数(其构造函数默认为 private)

  • 三、扩展密闭类子类的类(间接继承者)能够放在任何位置,而无需在同一个文件中。

密闭类算是枚举类的扩展,用法和枚举类类似,常常配合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 代码对比的方式讲解,但愿能好理解一点。若是有什么错误的地方,欢迎指出。

参考:

Kotlin 语言官方参考文档

Kotlin 数据类与密封类

更多Android干货文章,关注公众号 【Android技术杂货铺】

相关文章
相关标签/搜索