Kotlin(五)类与对象
一.类的构造
1.类的简单定义
先对比一下kotlin和java中普通类的写法:
public class A extends B {
public A() {
}
}
class A : B() {
init {
}
}
构造实例:
var b = B()
相比较之下可以看出:
- kotlin省略了public关键字,它默认就是public权限
- kotlin用冒号代替了extends,而且在继承时父类后面多了一个()
- kotlin类的初始化方法叫init,不像java中用类名作为构造方法的名称,注意init不是构造方法他只是定义了初始化操作,init方法无法直接定义输入参数
- 创建实例时去除了new关键字,也不需要指明实例的类型
2.类的构造方法
kotlin中类的构造方法分为主构造方法和二级构造方法,它的主构造方法是写在类名之后,前面说的初始化方法init就是主构造方法的内部代码:
class A constructor(name:String, id:Int) {
init{
println("$id$name")
}
}
那如果要定义多个不同参数的构造方法,就需要二级构造方法来实现
class A constructor(id:Int) {
init{
println("$id")
}
constructor(id:Int, name:String):this(id) {
println("$name的id是$id")
}
}
在二级构造方法中调用主构造方法就要在后面加上:this(...),这样就会在用该二级构造方法创建实例时先去执行
主构造方法的代码即init方法中的代码,再去执行本二级构造方法的代码,所以执行var a = A(1, "jack")会打印出:
"1"
"jack的id是1"
可以看到二级构造方法和主构造方法一样没有方法名,只用constructor来表明这是一个构造方法。而且kotlin规定主构造方法是可以被省略的,即所有构造方法都以二级构造方法的形式来声明,之间没有从属关系。
3.带默认参数的构造方法
和普通方法一样,类的构造方法也是可以自带默认参数
class A constructor(id:Int, name:String = "jack") {
private var id:Int
private var name:String
init{
this.id = id
this.name = name
}
//constructor(id:Int, name:String):this(id) {
// this.name = name
//}
}
这样写那么这个二级构造方法就没有必要存在了,因为构建该类实例时其主构造器可以有一个参数也可以有两个参数,因为name参数已经有了默认值,不需要修改时就可以用只包含id的主构造器来创建实例,这样就没必要再去重载一个二级的构造方法了
二.类的成员
1.成员属性
在上面说构造方法的demo中定义了id和name两个成员属性,它们的初始化方式和java语言是一样的,kotlin觉得这样写也太麻烦了,于是它改成了这样:
class A constructor(var id:Int, var name:String = "jack") {
init{
}
}
就是在主构造方法的形参之前加上var或val关键字,然后像java中的那种成员属性初始化方式就全部不用了,只需要一个var/val关键字就OK了,是不是很简便。而且java中的那些get、set方法也不需要写,构建实例之后对成员属性的get、set操作直接以类名.成员属性名的形式来操作:
class A constructor(var id:Int, var name:String = "jack") {
init{
}
}
创建实例:
var a1 = A(0)
var a2 = A(1, "tom")
get操作:
println("编号${a1.id} 姓名${a1.name}")
println("编号${a2.id} 姓名${a2.name}")
set操作:
a1.id = 100
a1.name = "jack1"
a2.id = 101
a2.name = "tom1"
2.成员方法
成员方法和上一篇说的方法以及其特殊的使用没什么区别,这一部分就不多说了,它的调用方式是实例对象.成员方法名(输入参数)。
3.伴生对象
java中静态成员的定义用的是static关键字,kotlin中取消了static关键字,而是使用了伴生对象的概念,伴生你可以理解为“影子”,因为影子是与生俱来的有且仅有一个。所有声明为伴生的成员变量和成员方法都会被放在一个伴生代码块中,这个代码块每个类只可以有一个,且在类加载的时候就会执行该代码块,优先级高于普通成员变量和方法:
class A(var name:String, val sex:Int = 0) {
var sexName:String
init{
sexName = if(sex==0) "boy" else "girl"
}
companion object WithA{
var flag = false
fun judgeSex(sexStr:String):String{
var result:String = when(sexStr) {
"boy", "male", "男" -> "这是个爷们"
"girl", "female", "女" -> "这是个娘们"
else -> "???"
}
return result
}
}
}
companion表示伴随,object表示对象,WithA是伴生对象的名称,这里声明了两个伴生成员,一个是变量flag还有一个是方法judgeSex,
他们的调用方式:
A.WithA.flag
A.WithA.judgeSex("female")
因为每个类只能有一个伴生代码块,所以这个WithA可以省略:
A.flag
A.judgeSex("female")
这样调用方式跟java中对类静态成员的调用方式就是一样的
三.类的继承
1.继承
我们先弄一个动物的基类作为例子:
class Animal(var type:String, var name:String, var age:Int) {
var desc:String
init{
desc = "这是一只${age}岁的$name,它是${type}类动物"
}
fun eat():Unit {
println("吃饭")
}
fun sleep():Unit {
ptintln("睡觉")
}
fun getDesc():String {
return desc
}
}
然后我们再声明一个鸡类去继承它:
class Chicken(type:String = "禽", name:String = "鸡", age:Int):Animal(type, name, age) {...}
可能你觉得这样写就没得问题,但其实kotlin在继承性安全的问题上和java是相反的,java默认类是可以被继承的,除非类被final关键字修饰,而kotlin默认类是不可以被继承的,除非被open关键字修饰,所以上述的Animal基类必须用open关键字修饰才可以被继承:
open class Animal(var type:String, var name:String, var age:Int) {
var desc:String
init{
desc = "这是一只${age}岁的$name,它是${type}类动物"
}
fun eat():Unit {
println("吃饭")
}
fun sleep():Unit {
ptintln("睡觉")
}
fun getDesc():String {
return desc
}
}
有了继承就一定会说到覆写,即子类可以覆写父类的方法,前提是父类方法也要被open修饰。kotlin仿照java也给出了四个开放性修饰符:
- public 对所有人开放,一般不加权限修饰符时默认取public
- internal 对本模块内部开放,对于Android开发来说本模块就是本app
- protected 对本类及子类开放
- private 只对自身开放
可能你会问道既然权限修饰符不写时默认取public,那么基类不加权限修饰符时那它就是公有的,公有的为什么不能被继承呢?因为kotlin规定类能否被继承只和是否被open关键字修饰有关,和访问权限没有关系,那你可能又会问如果我用private来修饰类,那么加上open还能被继承吗?这里就要提到open关键字和private不可以共存,即“open private class Animal...”这样的写法编译是无法通过的。所以要使某类可被继承就必须用open修饰且不能被private修饰,如果要覆写基类的方法,也要用open来修饰方法:
open class Animal(var type:String, var name:String, var age:Int) {
private var desc:String
init{
desc = "这是一只${age}岁的$name,它是${type}类动物"
}
open fun eat():Unit {
println("吃饭")
}
open fun sleep():Unit {
println("睡觉")
}
open protected fun getDesc():String {
return desc
}
}
子类:
class Chicken(type:String = "禽", name:String = "鸡", age:Int):Animal(type, name, age) {
override fun eat() {
super.eat()
println("${name}在啄米")
}
override fun sleep() {
super.sleep()
println("${name}在窝里趴着睡觉")
}
override public fun getDesc(): String {//这里将父类该方法的权限升级为了public,子类中可以将父类方法的访问权限由小往大升级,但不可以往更小的权限降
return super.getDesc()
}
}
调用:
var c = Chicken("家禽", "鸡", 12)
println(c.getDesc())
//最后会打印出:这是一只12岁的鸡,它是家禽类动物
2.抽象类
kotlin和java一样也是用abstract关键字来修饰抽象类,所有被abstract修饰的类都是默认可被继承的,所有被abstract修饰的方法都是默认可以被重写的,他们的open关键字就可以被省略:
abstract class Chicken(type: String, name: String, age: Int, sex: String):Animal(type, name, age) {
override fun eat() {
super.eat()
println("${name}在啄米")
}
override fun sleep() {
super.sleep()
println("${name}在窝里趴着睡觉")
}
override public fun getDesc(): String {
return super.getDesc()
}
abstract fun voice(): Unit
}
我们把鸡类变成一个抽象类,并加上一个成员属性sex来指明是母鸡还是公鸡,再声明一个抽象方法voice,
实现该方法时就打印出鸡的叫声(公鸡和母鸡叫声不同)
鸡类被母鸡类实现:
class Hen(type: String = "家禽", name: String = "鸡", age: Int, val sex: String = "母"): Chicken(type, name, age, sex) {
override fun getDesc(): String {
return "这是一只${age}岁的$sex$name,它是${type}类动物"
}
override fun voice(){
println("咯咯咯...")
}
}
调用:
var h = Hen("家禽", "鸡", 12)
println(h.getDesc())
h.voice()
打印出:
这是一只12 岁的母鸡,它是家禽 类动物
咯咯咯...
3.接口
kotlin和java一样不允许多继承,于是只能通过定义接口来实现多继承的功能,首先要注意:
- 接口不允许有构造方法
- 接口中的方法默认为抽象类型,所以open abstract就可以被省略
- kotlin允许在接口内部实现某个方法,但java接口中的所有方法必须都是抽象方法
interface Behavior {
open abstract fun fly(): String
fun run(): String
fun swim(): String {
return "大多数禽类都会游泳"
}
//kotlin的接口允许声明抽象属性,实现该接口的类必须重载该属性
//这里的open abstract也可以省略
open abstract var sex: String
}
接口实现:
class Pig(var name: String = "猪"): Behavior {
override fun fly():String {
return "${name}不会飞"
}
override fun run():String {
return "${name}会跑但懒得跑"
}
override fun swim():String {
return super.swim()
}
override var sex:String = "母"
}
四.几种特殊的类
1.嵌套类
即在一个类中去定义另一个类,但和java不同的是java的嵌套类允许访问外部类的成员,kotlin的嵌套类则正好相反他不允许访问外部类的成员:
class Tree(var treeName: String) {
class Flower(var flowerName: String) {
fun getName(): String {
return "这是一朵$flowerName"
//return "这是${treeName}上的一朵$flowerName"//编译会报错
}
}
}
嵌套类的调用:
创建嵌套类实例时要在提嵌套类之前写外部类类名,相当于将嵌套类作为外部类的静态对象来使用
var flower = Tree.Flower("牵牛花")
println(flower.getName)
2.内部类
kotlin中内部类是可以访问外部类的成员的,只需要在定义嵌套类时用inner关键字修饰就变成了内部类,而且在实例化内部类时要先实例化外部类,就相当于把内部类作为外部类的一个成员对象来使用:
class Tree(var treeName: String) {
inner class Flower(var flowerName: String) {
fun getName(): String {
return "这是${treeName}上的一朵$flowerName"//编译不会报错
}
}
}
内部类的调用:
var flower = Tree("杨树").Flower("牵牛花")
println(flower.getName)
3.枚举类
java提供了一种枚举类型,用enum关键字来表达,其内部定义了一系列的名称旨在通过有意义的名称比0、1、2这些没有含义的数字能够更有效地表达语义,java中的枚举类型:
enum Season{ SPRING, SUMMER, AUTUMN, WINTER }
实际上枚举类型是一种类,java中我们创建一个枚举类型时,jvm会自动编译生成一个类,这个类继承自java.lang.Enum,所以在kotlin中没有枚举类型这一说,取而代之的是枚举类,即enum修饰的类:
enum class Season {
SPRING, SUMMER, AUTUMN, WINTER
}
而且枚举类自带属性信息,可以通过ordinal获取枚举值的序号,通过name获取枚举值名称。如果枚举类存在构造器,可以定义成员属性来对枚举值进行描述:
enum class Season(var seasonName: String) {
SPRING("春天"),
SUMMER("夏天"),
AUTUMN("秋天"),
WINTER("冬天")
}
调用:
Season.SPRING.ordinal = 0
Season.SUMMER.name = "SUMMER"
Season.WINTER.seasonName = "冬天"
4.数据类
在Android开发中经常要创建一个存放用户信息、访问地址等数据的实体类,每个数据对象都要有set/get方法,有的还需要toString方法,在前面类的定义我提到过在主构造方法的变量声明前加上var/val就可以免写set/get方法以及this.xxx = xxx等一系列操作,但是这个toString方法还是得自己敲,kotlin此时就为我们提供了data关键字修饰的数据类,它提供以下功能:
- 自动声明与构造方法同名的属性字段
- 自动实现每个属性字段的set/get方法
- 自动提供equals方法,用来比较两个本数据类对象是否相等
- 自动提供copy方法,允许完整复制一个本数据类对象
- 提供toString方法用来打印所有的字段值
例如一个用户信息类:
data class User(var name:String, var sex:String, var age:Int) {}
调用:
var user1 = User("sss", "男", 12)
var user2 = user1.copy()
var user3 = user1.copy(age = 15)
user1.equals(user2)//true
user1.equals(user3)//false
5.模板类(泛型类)
这个泛型类和泛型方法一样,跟java的使用没有什么区别:
class Processor<T> {
fun process(value: T) {
value?.hashCode()
}
}
fun main(args: Array<String>) {
// 可空类型String?被用来替换T
val nullableString = Processor<String?>()
// 可传递null
nullableString.process(null)
}