面向对象编程通过对事物的抽象,大大的简化了程序的开发难度。我们常用的编程语言:Java、C++、Python都属于面向对象编程。Kotlin与java类似,也是一种面向对象编程语言。本文从面向对象三个基本特征:封装、继承、多态,来阐述一下Kotlin中的面向对象编程。
修饰符
阐述类之前,先来说一下Kotlin中的修饰符,kotlin中的修饰符可以分为这几类:访问修饰符、类修饰符、成员修饰符、泛型修饰符。
访问修饰符
Kotlin修饰符有四种:public、internal、protected、private。他们都可以用来修饰类、方法、属性,修饰后将限制对他们的访问权限,访问权限的关系可以表示为:public > internal > protected > private。
- public:默认修饰符,任何位置都可以被访问
- internal:同一个模块内可以访问
- protected:同一个文件及子类可以被访问(但是不能用来修饰文件中的顶层类)
- private:仅在同一个文件中可以被访问
修饰外部类及成员
这里说的外部类指的是文件中顶层定义的类。
修饰符 | 修饰类 | 修饰方法 | 修饰属性 | 外部模块 |
public | 任何位置都可以被访问 | 任何位置都可以被访问 | 任何位置都可以被访问 | 可访问 |
internal | 同一模块任何位置都可以被访问 | 同一模块任何位置都可以被访问 | 同一模块任何位置都可以被访问 | 不可访问 |
protected | 不可修饰 | 当前类及其子类可访问 | 当前类及其子类可访问 | 不可访问 |
private | 当前文件的类可访问 | 当前类可访问 | 当前类可访问 | 不可访问 |
修饰嵌套类及成员
这种情况访问时分两种情况:1、嵌套类所在的类的访问范围;2、其他范围
嵌套类所在的类的访问范围
这种情况访问与上面说的修饰外部类及成员访问的关系表类似,只是访参考不再是本文件而是直接外部类。如:
class A{
class SubA{
private var name:String = ""
}
class SubB{
private var name:String = ""
}
}
类A中有两个嵌套类: SubA、SubB。对于SubA类来说,其SubA类在A类的访问权限可以按照内部类所在的类的访问范围来判断。
其他范围
主要受外部类的访问权限影响,应与外部类的访问权限取最小(如果有多成外部类,多层取最小)。如:
internal class A{
class SubA{
private var name:String = ""
}
}
class B{
}
我们要在B类中访问A中的SubA类时,需要考虑到A类和SubA的访问修饰符, 两者取最小。
与java的区别
- 相同点:private、protected、public 访问权限修饰符kotlin 和 java 是一致的。
- 不同点:java 默认修饰符是default (包可见),kotlin 默认修饰符是public,kotlin 存在 internal 访问修饰符,protected 不能用来修饰kotlin 文件顶层声明的类、方法、变量
类修饰符
- final:不能被继承
- open:可以被继承
- abstract:抽象类
- enum:枚举类
- data:数据类
- sealed:密封类
- annotation:注解类
java 中 默认类都是public 的 ,kotlin 类默认都是final 修饰的不能用来继承,需要添加open 修饰符
成员修饰符
- override: 重写函数
- open:可以被重写
- final:不能被重写
- abstract:抽象函数
- lateinit:延迟初始化
泛型修饰符
- in:相当于Java中的super关键字的作用
- out:相当于Java中的extends关键字的作用
类与对象
类定义
Kotlin中使用关键字class声明类,后面紧跟类名,如:
class Book { // 类名为 Book
// 大括号内是类体构成
}
我们也可以定义一个空类:
class Empty
可以在类中定义方法:
class Book {
fun look() { print("look") }
}
Kotlin 类可以包含:构造函数和初始化代码块、方法、属性、内部类、对象声明。
类的属性
类的属性可以用关键字 var 声明为可变的或者用关键字 val 声明为不可变。
class Book{
var name: String = ……
var author: String = ……
var publisher: String = ……
}
属性声明的完整语法:
var|val <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>]
如果属性类型可以从初始化语句或者类的方法中推断出来,那就可以省去类型。
var allByDefault: Int? // 错误: 需要一个初始化语句 var initialized = 1 // 类型为 Int val simple: Int? // 类型为 Int ,但必须在构造函数中初始化 val inferredType = 1 // 类型为 Int 类型
getter 和 setter
getter 和 setter都是可选的,如果你没有在代码中创建它们,它是会默认自动生成。
class Book{
var name = "Kotlin"
}
相当于:
class Book{
var name = "Kotlin"
get() = field
set(value){
field = value
}
}
注意:val修饰的属性不允许设置sette方法,因为它是只读的。):
class Book{
var name = "Kotlin"
get() = "Java"
set(value){
field = "Java"
}
}
var book = Book()
book.name = "修改Kotlin"
Log.d(TAG,book.name)
运行结果如下:
当存在多个属性时,也可以显示的对多个属性定义getter 和 setter,getter 和 setter的作用原则是:作用上面最近一个属性。
class Book{
var name = "Kotlin"
get() = field //name的getter
set(value){ //name的setter
field = value
}
var title = "Java"
get() = " get Java" //title的getter
var title2 = "" //title2的getter
set(value){ //title2的setter
field = "set Java2"
}
}
var book = Book()
book.name = "修改Kotlin"
Log.d(TAG,book.name)
Log.d(TAG,book.title)
book.title2 = "修改Java2"
Log.d(TAG,book.title2)
运行结果如下:
但是如果我们重写了这个变量的 getter 方法和 setter 方法,并且在 getter 方法和 setter 方法中都没有出现过 filed 这个关键字,则编译器会报错,提示 Initializer is not allowed here because this property has no backing field,除非显式写出 filed 关键字(哪怕它什么都不干,只要放在那里就可以了)。
我们可以像使用普通函数那样使用构造函数创建类实例:
val book = Book() // Kotlin 中没有 new 关键字
要使用一个属性,只要用名称引用它即可:
site.name // 使用 . 号来引用
site.url
主构造器
主构造器中不能包含任何代码,可以包含参数,参数可以在初始化代码(使用 init 关键字作为前缀)中直接使用,如:
class Book constructor(name: String) {
init {
println("name is $name")
}
}
主构造器中的参数也可以在类主体定义的属性初始化代码中使用,如:
class Book constructor(n: String) {
var name = n
init {
println("name is $n")
}
}
也可以省略关键字constructor,如:
class Book(n: String) {
var name = n
init {
println("name is $n")
}
}
如果构造器有注解,或者有可见度修饰符,这时constructor关键字是必须的,注解和修饰符要放在它之前。
var/val修饰参数
主构造函数的参数可以使用var/val就行修饰,修饰后相当于在类中定义相同名的属性。
open class Base(var name: String ,var title: String){
}
var base = Base("java","title")
Log.d(TAG,base.name)
Log.d(TAG,base.title)
运行效果如下:
次构造函数
Kotlin类也可以有二级构造函数,需要加前缀constructor:
class Book {
constructor(name:String){
}
}
如果类有主构造函数,每个次构造函数都要直接或间接代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字:
class Book(n: String) {
var name = n
constructor(name: String,author:String) : this(name){
// 初始化...
}
}
对象表达式
Kotlin中可以用对象表达式来实现创建一个对某个类做了轻微改动的类的对象,且不需要去声明一个新的子类。
通过对象表达式实现一个匿名内部类的对象用于方法的参数中:
findViewById<TextView>(R.id.tv).setOnClickListener(object :View.OnClickListener {
override fun onClick(p0: View?) {
}
})
对象可以继承与某个基类,或者实现其他接口:
open class Book {
public open val name: String = "Kotlin"
}
interface Base {}
val book: Book = object : Book(), Base {
}
如果超类型有一个构造函数,则必须传递参数给它。多个超类型和接口可以用逗号分隔开。
通过对象表达式可以越过类的定义直接得到一个对象:
val book = object{
var name = "Kotlin"
}
Log.d(TAG,book.name)
注意,匿名对象可以用做只在本地和私用作用域中声明的类型。如果你使用匿名对象作为共有函数的返回值或者用作共有属性的值,那么该函数或属性的实际类型回事匿名对象声明的超类型,如果你没有声明任何超类型,就会是Any。在匿名对象中添加成员将无法访问。
class Book {
var b = object {
val x: String = "x"
}
private var c = object {
val x: String = "x"
}
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
val bx = b.x // 错误:未能解析的引用“x”
val cx = c.x // 没问题
}
}
对象声明
Kotlin中使用object关键字来声明一个对象。如:
object ObjManager{
fun add(){
}
fun remove(){
}
}
使用该对象,我们可以直接使用其名称即可:
ObjManager.add()
ObjManager.remove()
当然也可以定义一个变量来获取这个对象,但是当你定义两个不同的变量来获取这个对象时,你会发现并不能得到两个不同的变量。也就是说可以通过这种方式,我们获得一个单例。
object book{
var name:String = "Kotlin"
}
var book1 = book
var book2 = book
Log.d(TAG,book1.name)
Log.d(TAG,book2.name)
运行结果如下:
抽象类
Kotlin中抽象可分为抽象类、抽象方法、抽象属性,抽象类的成员包括抽象方法和抽象属性。抽象类的成员只有定义而没有实现。声明抽象使用abstract关键字进行修饰,抽象类的子类必须全部重写带有abstract修饰的抽象属性和函数,除非子类也是抽象类。
abstract class 抽象类名[(主构造函数)][:继承父类和实现接口]{
...
}
如:
abstract class Book {
abstract var name:String
var title:String = ""
abstract fun look()
fun show(){
}
}
抽象类与普通类除了都可以具有自己的属性、构造函数、方法等组成部分外,抽象类还可以包含抽象函数和抽象属性。抽象类本身具有普通类的特性以及组成部分,不过抽象类并不能直接被实例化。
嵌套类
把类定义嵌套定义在其他类的里。如:
class Outer { // 外部类
class Nested { // 嵌套类
fun foo() = 4
}
}
使用嵌套类格式:外部类.嵌套类.嵌套类方法/属性。如下:
val demo = Outer.Nested().foo() // 调用格式:外部类.嵌套类.嵌套类方法/属性
内部类
内部类使用 inner 关键字来表示。如:
class Outer {
private val a: Int = 1
var v = "成员属性"
/**嵌套内部类**/
inner class Inner {
fun foo() = a // 访问外部类成员
fun innerTest() {
var o = this@Outer //获取外部类的成员变量
Log.d(TAG,"内部类可以引用外部类的成员,例如:" + o.v)
}
}
}
内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。 消除歧义,要访问来自外部作用域的 this,我们使用this@XXX,其中 @XXX 是一个代指 this 的来源。
内部类的使用格式:外部类对象.嵌套类.嵌套类方法/属性。如:
val demo = Outer().Inner().foo()
val demo2 = Outer().Inner().innerTest()
匿名内部类
kotlin中使用对象表达式来创建匿名内部类
findViewById<TextView>(R.id.tv).setOnClickListener { object :View.OnClickListener{
override fun onClick(p0: View?) {
Toast.makeText(this@MainActivity,"点击了",Toast.LENGTH_SHORT).show()
}
}}
枚举类
Kotlin使用enum定义一个枚举类,枚举类最基本的用法是实现一个类型安全的枚举。枚举常量用逗号分隔,每个枚举常量都是一个对象。
enum class Color{
RED,BLACK,BLUE,GREEN,WHITE
}
枚举初始化
每个枚举都是枚举类的实例,默认名称为枚举字符名,值从0开始,若需要指定值,则可以使用期构造函数:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
枚举还支持以声明自己的匿名类及相应的方法、以及覆盖基类的方法。如:
enum class Color {
RED {
override fun signal() = RED
},
BLACK {
override fun signal() = BLACK
};
abstract fun signal(): Color
}
如果枚举类定义任何成员,要是用;将成员定义与枚举常量定义分割开来。
获取枚举常数
1、使用valueOf和enumValueOf<T>()通过名称获取枚举常量:
Color.valueOf("RED")
enumValueOf<Color>("RED")
2、使用 values()和enumValues<T>()获取枚举列表:
Color.values()
enumValues<Color>()
枚举的使用
Kotlin 中的枚举类具有合成方法,允许遍历定义的枚举常量,并通过其名称获取枚举常数。
enum class Color{
RED,BLACK,BLUE,GREEN,WHITE
}
var color:Color=Color.BLUE
Log.d(TAG,Color.values().toString())
Log.d(TAG,Color.valueOf("RED").toString())
Log.d(TAG,color.name)
Log.d(TAG,color.ordinal.toString())
运行结果如下:
继承
Kotlin 中所有类都继承该 Any 类,它是所有类的超类,对于没有超类型声明的类是默认超类:
class Book // 从 Any 隐式继承
Any 默认提供了三个函数:
equals()
hashCode()
toString()
在Kotlin开发中类和方法默认不允许被继承和重写,等同于Java中用 final 修饰类和方法。
如果在Kotlin 中类和方法想被继承和重写,需添加open关键字修饰。
open class Base(b : Int) {
}
class Book(b : Int):Base(b){
}
构造函数
子类有主构造函数
如果子类有主构造函数, 则基类必须在主构造函数中立即初始化。
open class Base(name : String,title : String) {
}
class Book(name : String,title : String):Base(name,title){
}
子类没有主构造函数
如果子类没有主构造函数,则必须在每一个次构造函数中用 super 关键字初始化基类,或者间接通过另一个次构造函数代理到基类的构造函数。初始化基类时,可以调用基类的不同构造方法。
open class Base{
constructor(name : String){
}
constructor(name : String, title : String){
}
}
class Book:Base{
constructor(name : String, title : String):super(name,title){
}
constructor(name : String):super(""){
}
}
重写
方法重写
在基类中,使用fun声明的方法时,次函数默认为final修饰,补鞥呢被子类重写。如果允许子类重写该方法,那么就要手动添加open修饰它,子类重写方法使用override 关键词:
open class Base{
open fun look(){
print("看XX书")
}
}
class Book:Base(){
override fun look(){
print("看Kotlin书")
}
}
如果有多个相同的方法(继承或者实现自其它类,如A、B类),则必须要重写改方法,使用super范型去选择地调用父类的实现。
open class A {
open fun f () { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } //接口的成员变量默认是 open 的
fun b() { print("b") }
}
class C() : A() , B{
override fun f() {
super<A>.f()//调用 A.f()
super<B>.f()//调用 B.f()
}
}
属性重写
属性重写使用override关键字,属性必须具有兼容类型,每一个声明的属性都可以通过初始化程序或者getter方法被重写:
open class Base{
open var x :Int = 0
}
class Book :Base(){
override var x :Int = 2
}
你可以使用一个var属性重写一个val属性,但是反过来不行。因为val属性本身定义了getter方法,重写为var属性会在衍生类中额外声明一个setter方法。
你也可以在主构造函数中使用override关键字直接对属性进行重载:
open class Base{
open var x :Int = 0
}
class Book(override var x:Int):Base(){
}