Kotlin_4_类的基础知识

246 阅读8分钟

面向对象的概念

  • 封装性

    封装性是面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体实现细节,这就是封装思想。例如,用户使用电脑,只需要使用手指敲键盘就可以了,无须知道电脑内部是如何工作的,即使用户可能碰巧知道电脑的工作原理,但在使用时,并不完全依赖电脑工作原理这些细节。

  • 继承性

    继承性主要描述的是类与类之间的关系,通过继承,可以在无须重新编写原有类的情况下,对原有类的功能进行扩展。例如,有一个汽车的类,该类中描述了汽车的普通特性和功能,而轿车的类中不仅应该包含汽车的特性和功能,还应该增加轿车特有的功能,这时,可以让轿车类继承汽车类,在轿车类中单独添加轿车特性的方法就可以了。

  • 多态性

    多态性指的是在程序中允许出现重名现象,它指在一个类中定义的属性和方法被其他类继承后,它们可以具有不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中具有不同的语义。

class类:对象的抽象

创建类

类是对象的抽象,它用于描述一组对象的共同特征和行为。类中可以定义成员变量和成员函数,其中成员变量用于描述对象的特征,也被称作属性,成员函数用于描述对象的行为,可简称为函数或方法。

class Person{
    var name = "X"
    var age = 18
    fun sayHello() {
        println("name:$name\nage:$age")
    }
}

创建对象

var p=Person()

创建了一个Person类的实例对象。

实际应用:

class Person {
	// 成员属性
	var name = "X"
	var age = 18
	// 成员方法
	fun sayHello() {
		println("Name:$name\nAge:$age")
	}
}

fun main() {
	val p = Person()
	p.sayHello()
    // Name:X
	// Age:18
	println("Name:${p.name}")
    // Name:X
	p.name="Z"
	println("Name:${p.name}")
    // Name:Z
}

类的封装

类的封装是指在定义一个类时,将类中的属性私有化,即使用private关键字来修饰,私有属性只能在它所在的类中被访问。为了让外界访问这些私有属性,需要提供一些使用public修饰的公有方法。(这句话目前还没明白)。

class Student {
	var name:String = ""
	private var age:Int = 0

	fun setAge(age:Int) {
		if (age>=0) {
			this.age = age
			println("Age:$age")
		}else {
			println("WRONG.")
		} 
	}

	fun sayHello() {
		println("Name:$name\nAge:$age")
	}
}

fun main() {
	val stu = Student()
	stu.name = "Z"
	stu.setAge(-4)
	stu.sayHello()
	stu.setAge(3)
}

其中,this关键字指的是该类的当前对象,意在与Student类中的私有属性age区分开。

构造函数

构造函数是类的一个特殊成员,它会在类实例化对象时被自动调用。Kotlin中的构造函数分为两种——主构函数和次构函数。

简单来说,构造函数的意图是在初步实例化类时,就可以简单地传入参数,进行使用(跟函数一样)。

主构函数:constructor

一个类可以有一个主构造函数和多个次构造函数。主构函数位于类头跟在类名之后,如果主构造函数没有任何注解或可见性修饰符(如public),constructor关键字可省略。

主构函数的语法如下:

class ClassName constructor([args1,args2,args3]) {}
  • 当在定义一个类时,如果没有显示指定主构函数,则Kotlin编译器会默认为其生成一个无参主构函数,这点和Java是一样的。

    class ClassName constructor() {}  //第一种写法
    class ClassName() {}  //第二种写法
    
  • 有参的主构函数:通过有参的构造函数为属性赋值。主构造器中不能包含任何代码,初始化代码可以放在初始化代码段中,初始化代码段使用init关键字作为前缀。

    class Clerk constructor(username:String) {
    	init{
    		println("I am $username")
    	}
    }
    
    fun main() {
    	var clerk=Clerk("Z")
    }// I am Z
    

    在《Kotlin从基础到实战》一书中,对主构函数的举例十分多此一举:

    class Clerk constructor(username:String) {
        var name:String
        init{
            name=username
            println("I am $name")
        }
    }
    

    多此一举之处在于,var name:String这一行以及下面的赋值是完全没有必要的。往下看之后,作者是为了刻意介绍this关键字。也就是说,他试图将构造函数中的username和类中的成员变量name都变成name,然后为了区分,便故意在init中加入this.name。因为之前已经研究过this,这里不再详说。

    class Clerk constructor(name:String) {
        var name:String
        init{
            //这里this.name指的是类中的成员变量name
            this.name = name
            println("I am $name")
        }
    }
    

次构函数:constructorthis

在Kotlin中,可以定义多个次构函数,次构函数同样使用constructor关键字定义,只不过次构函数位于类体中。

次构函数应用于以下的场景中,即假如主构函数只有一个name属性,而尚存在同时给name属性和age属性赋值的情况,这就需要再添加一个函数,也就是次构函数。次构造函数必须调用主构造函数或其他次构造函数,其调用方式为次构函数:this(参数列表)

class Workers constructor(name:String) {
	init {
		println("I am $name.")
	}
	constructor (name:String,age:Int):this(name) {
		println("I am $name,\nI am $age years old.")
	}
	constructor (name:String,age:Int,sex:String):this(name,age) {
		println("I am $name,\nI am $age years old,\nI am a $sex.")
	}
}

fun main() {
	var person = Workers("Z",18,"man")
}

从代码中可看出,第1个次构函数继承自主构函数,第2个次构函数继承自第1个次构函数。不仅第2个次构函数被调用,第1个次构函数和主构函数都会被调用。


类的继承

继承

在Kotlin中,类的继承是指在一个现有类的基础上去构建一个新类,构建出来的新类被称作子类,现有类被称作父类,子类会自动拥有父类所有可继承的属性和方法。

这个概念与python一致。在具体的语法上,父类最前端应当加上open关键字(原因是Kotlin中的所有类都默认使用final关键字修饰,不能被继承),子类需要在声明时加上:FatherClass(),举例如下:

open class Father() {
    fun sayHello() {
        println("Hello")
    }
}

class Son:Father() {}

fun main() {
    var son = Son()
    son.sayHello()  // "Hello"
}
  • 一个子类只能继承一个父类。
  • 多个子类可以继承一个父类。
  • 多层继承是可以的,如A继承B,B继承C,这时,A也可算是C的子类。

继承的重写

指的是有些时候,子类并不会完全继承父类,对于所继承的某些方法或属性需要进行修改,这种修改叫做方法或属性的重写。

  • 父类中,需要被重写的方法或属性需要在前面加上open关键字。
  • 子类中,重写的方法或属性前需要使用override关键字。
open class Father() {
	open var name = "Z"
	open var age = 25
	fun sayHello() {
		println("Hello,I am $name,I am $age years old.")
	}
}

class Son():Father() {
	override var name = "X"
	override var age = 15
}

fun main() {
	val father = Father()
	val son = Son()
	father.sayHello()
	son.sayHello()
    //Hello,I am Z,I am 25 years old.
    //Hello,I am X,I am 15 years old.
}

super:重写后再次调用父类

如前所述,重写后因为有了新的方法或属性,但这时,假如还想访问父类的属性或方法,则需要super.func

A BORING example:

open class Father() {
	open var name = "Z"
	open var age = 25
	open fun sayHello() {
		println("Hello,I am $name,I am $age years old.")
	}
}

class Son():Father() {
	override var name = "X"
	override var age = 15
	override fun sayHello() {
		super.sayHello()
		println("Hello,I am ${super.name}'s son,I am $name,I am $age years old.")
	}
}

fun main() {
	val son = Son()
	son.sayHello()
}

一个没看懂的小知识,Any

Kotlin中所有类都继承Any类,它是所有类的父类,如果一个类在声明时没有指定父类,则默认父类为Any类,在程序运行时,Any类会自动映射为Java中的java.lang.Object类。

在Kotlin中,所有类型都是引用类型,这些引用类型统一继承父类AnyAny类中默认提供了3个方法,分别是equals()hashCode()toString()

抽象类与接口

抽象类 abstract

当定义一个类时,通常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现方式是无法确定的。因此,可以将其定义为抽象方法,抽象方法使用abstract关键字修饰,该方法没有方法体,在使用时需要实现其方法体,当一个类中包含了抽象方法,该类必须使用abstract关键字定义为抽象类。

也就是说,实际需要是需要在类中定义一个方法,但是这个方法具体内容不知道,于是可以将方法先定义为抽象方法,有了抽象方法,方法所在的类就必须定义为抽象类

但是,有抽象方法的类必须声明抽象类,但是抽象类中不必须有抽象方法。

抽象类不能被实例化,原因在于抽象方法不能被调用。如果想调用,则需要创建一个子类。

如下:

abstract class Animal() {
	abstract fun eat()
}

class Monkey(var food:String):Animal() {
	// var food = food
	override fun eat() {
		println("Monkeys are eating $food.")
	}
}

fun main() {
	var monkey = Monkey("banana")
	monkey.eat()
    // Monkeys are eating banana.
}

接口 interface

如果一个抽象类中的所有方法都是抽象的,则可以将这个类用另外一种方式来定义,即接口。接口可以说是一个特殊的抽象类,在定义接口时,需要使用interface关键字来声明:

interface Animal {
    fun eat()
}
  • eat()并没有abstract关键字修饰,因为接口默认带有了
  • Animal没有括号
  • 与抽象类一样,接口需要子类以实例化来调用方法

类继承接口

interface Animal {
	fun eat()
}

class Monkey(var food:String):Animal {
	override fun eat() {
		println("Monkeys are eating ${food}.")
	}
}

fun main() {
	val monkey = Monkey("banana")
	monkey.eat()
}

接口继承接口(其实是一样的)

interface Animal {
	fun eat()
}

interface Monkey:Animal {
	fun sleep()
}

class GoldenMonkey(var food:String):Monkey{
	override fun eat() {
		println("I am GoldenMonkey,I love $food.")
	}
	override fun sleep() {
		println("I am GoldenMonkey,I love sleep.")
	}
}

fun main() {
	var g = GoldenMonkey("banana")
	g.eat()
	g.sleep()
}

当然,一个类可以接收多个接口或抽象类,用,隔开。

常见的类

嵌套类与内部类inner

嵌套类和内部类都是类中套一个类,二者的区别也仅仅在于里面的类前面是否有一个inner标记,有inner的是内部类。

二者实质上的区别在于,嵌套类没有inner,所以不能访问它本身以外的变量或方法,举例如下:

class A{
    var name="A"
    class B{
        fun sayHello() {
            println("Hello!$name!")
        }
    }
}

这段代码是错误的,因为class B本身作为嵌套类,是不可以访问class A中的变量的。

但是换成内部类就成立了:

class A{
    var name="A"
    inner class B {
        fun sayHello(){
            println("Hello $name.")
        }
    }
}

fun main() {
    A().B().sayHello()
}

这就成立了。

枚举类enum

每个枚举常量都是一个对象,枚举常量用逗号分隔,枚举类的前面用enum关键字来修饰。

enum class Week{
    Monday,Tuesday,Wednesday,Tuesday,Friday,Saturday,Sunday
}

枚举类支持构造函数*(简单来说,构造函数的意图是在初步实例化类时,就可以简单地传入参数,进行使用(用法跟函数一样)。)*

enum class Week(val what:String, val doSomething:String) {
	Monday("Mon","Work"),
    Tuesday("Tue","Party"),
    Wednesday("Wed","Work"),
    Thursday("Thu","Work"),
    Friday("Fri","Work"),
    Saturday("Sat","Overtime"),
    Sunday("Sun","Rest")
}

密封类 sealed

每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

  • 密封类必须用sealed关键字来修饰。
  • 由于密封类的构造函数是私有的,因此密封类的子类只能定义在密封类的内部或者同一个文件中
sealed class A {
	class A1:A() {}
	class A2:A() {}
	class B() {}
}
class A3:A() {}

其中,密封类A的子类有A1 A2 A3,类B属于A的嵌套类。

==注意==:密封类适用于子类可数的情况,而枚举类适用于实例可数的情况。

数据类 data

数据类不可以用abstractopensealedinner关键字来修饰。

在实际开发中,经常会用到数据类来存储一些数据信息,这些信息一般是通过该类的主构造函数来传递的。

data class Man(var name:String,var age:Int) {
	init {
		println("Name=$name\nAge:$age")
	}
}

fun main() {
	var man:Man = Man("Z",20)
}

单例模式 object

单例模式就是在程序运行期间针对该类只存在一个实例。就好比这个世界只有一个太阳一样,假设现在要设计一个太阳类,则该类就只能有一个实例对象,否则就违背了事实。

object SingleMode {
	var s = "SingleMode"
	fun sayHello() {
		println("Hello,I am $s.")
	}
}

fun main() {
	SingleMode.s = "X"
	SingleMode.sayHello()
	SingleMode.s = "Z"
	SingleMode.sayHello()
    //Hello,I am X.
    //Hello,I am Z.
}

可以看出两个特点:

  • 单例类的最大特征是可以直接通过类名.成员名的形式调用类中的属性与函数,不需要创建该类的实例对象,这是因为通过object关键字创建单例类时,默认创建了该类的单例对象,因此在调用该类中的属性和函数时,不需要重新创建该类的实例对象。
  • 单例类的实例对象只有一个,但是可以重复调用、修改其属性,调用函数。

伴生对象 companion object

伴生对象是在类加载时初始化,生命周期与该类的生命周期一致。

定义伴生对象是通过companion关键字标识的,由于每个类中有且仅有一个伴生对象,因此也可以不指定伴生对象的名称,并且其他对象可以共享伴生对象

class Company {
	// Factory伴生对象名称可以写也可以省略,因为一个类只能由一个伴生对象
	companion object Factory {
		fun sayHello() {
			println("I am a Companion Object.")
		}
	}
}

fun main() {
	//两种调用方法
	Company.Factory.sayHello()
	Company.sayHello()
}