接《Android开发者快速上手Kotlin(一) 之 简介和基本语法》文章继续。
5 面向对象编程
在Kotlin中,所有的类都继承于Any,这就像Java中的Object一样。类的实例化不需要使用new关键字。
5.1 包
Kotlin中包跟Java中包是一样,但要注意一点的是,包名可以不跟文件夹名。
5.2 类
Kotlin的类要注意几点:使用关键字constructor来声明构造方法、成员变量默认必须在声明时就初始化或者在init块进行初始化,且可见性默认是publish。
class A1 {
var mA: Int = 0 // 成员变量必须初始化
var mB: Int = 0
constructor(a: Int) { // 构造方法1(副构造方法)
mA = a
}
constructor(a: Int, b:Int):this(a) { // 构造方法2(副构造方法)
mB = b
}
fun f() {
println(mA)
}
}
class A2(a: Int, b:Int) { // 如果只有一个构造函数时可这样写,这叫做主构造方法,推荐类要有一个主构造方法
var mA: Int = a
var mB: Int = b
fun f() {
println(mA)
println(mB)
}
}
class A3(val a: Int, b:Int) { // 构造函数的参数也是类全局变量
var mB: Int
init { // init可做一些初始化工作
mB = b
}
fun f() {
println(a)
}
}
5.2.1 属性
其实Java中并没有属性的概念,它并不是语法的规则,只是通过约定的形式来实现属性的逻辑。而在Kotlin中就存在一个真正意义上的属性,它的get和set是完整的一体的,它通过直接使用类的成员变量来实现,因为类的成员变量,默认就可以就是属性,所以说类的成员的可见性默认是publish,属性可使用var或val来修饰,可以不用另外去getter和setter,但也可以重载其属性,如:
class A {
var a = 0
get() {
println("get a")
return field
}
set(value) {
println("set b")
field = value
}
}
5.2.2 延迟初始化属性
如果实在需要延迟初始化成员变量的话,当然也是有办法的。变量初始化,使用关键字 lateinit表示延时初始化,如果是常量的话,要使用 by lazy表达式来处理,表示使用时才调用到:
class A {
var a = 0 // 变量立即初始化
var b:String? = null
lateinit var c:String // 变量延迟初始化
val d:Int by lazy { // 只读变量的延迟初始化,只要第一次访问时就会执行大括号里的初始化代码
println("init d")
100
}
// var e:Int by lazy { // 换成var变量的话,这句是报错的
// println("init e")
// 100
// }
}
5.2.3 属性代理
上面介绍延心初始化只读变量时,使用的就是属性代码lazy,它只能用于只读变量的情况。如果是变量的话,对应也有一个叫observable的属性代理。当然你也可以通过自定义类的方式来实现,代理类需要实现相应的setValue和getValue方法。如:
class A {
val a by lazy { 100 } // 语法已提供,也就是上述的延初始化只读变量
val b by Delegates.observable(0) {
property, oldValue, newValue -> println("$property,$oldValue,$newValue")
}
val c by B() // 自定义代理
var d by B() // 有了setValue方法后,var声明的变量也可用于代码
}
class B {
var value: String? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value ?: "abc"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value
}
}
5.2.4 类及其成员的可见性
在Java中可见性分为:publish、protected、default(包内可见)以及private。
而在Kotlin中,可见性分为:publish(默认)、internal(模块内可见)、protected、以及private。
5.3 接口
Kotlin接口定义跟Java中差不多。如:
interface IA {
fun a()
}
5.3.1 使用匿名方法代码接口作回调
在Kotlin里,如果遇到使用线程进行耗时操作后进行回调,可以不再定义接口,而是使用方法类型的参数,即匿名方法。假如有这样的方法,需要进行操作后回调:
fun connect(context: Context, connectListener: (result: Boolean) -> Unit) {
}
fun connect2(connectListener: (result: Boolean) -> Unit) {
}
调用如:
// 正式的写法
connect(applicationContext, fun (result: Boolean) {
})
// Lambda表达式写法
connect(applicationContext) { result ->
}
// 如果参数方法仅有一个参数,可省略,调用时使用it代替
connect(applicationContext) {
val result:Boolean = it
}
// 如果方法仅有一个回调的参数方法,小括号也可省略
connect2() {
}
connect2 {
}
5.4 继承
Kotlin中的继承关系跟Java中一样,也是单继承多实现原则,它的语法跟C++很象。如:
open class A { // 可被继承的类,要使用open声明
fun a() {
println("this is A.a")
}
}
interface B { // 接口的声明
fun b1()
fun b2() { // 接口方法可以默认实现
println("this is B.b2")
}
}
abstract class C { // 抽象类的声明
abstract fun c1() // 抽象方法,必须要子类实现
open fun c2() { // open方法,可由子类重写或不重写
println("this is C.c2")
}
fun c3() { // 子类不能实现
println("this is C.c3")
}
}
class Test1 : A(), B { // 子类继承使用豆号+父类的构造方法
override fun b1() {
}
// override fun b2() { // b2方法选择性实现
// }
}
class Test2 : C() {
override fun c1() { // 实现父类或接口的方法,要使用overried
}
override fun c2() {
}
// override fun c3() { // 这句会报错,因为c3不能被重写
// }
}
说明:
- 继承父类时,是会同时调用了父类的构造方法
- 抽象类依然是使用abstract关键字来进行声明。
- 类和方法默认是不能被继承的,想要对类或方式进行继承,需要加上open关键字。
- 子类重写父类的方法时,需要在方法前加上overried关键字,Java中只是通过注解来约定,而Kotlin是必须要加关键字。
- 接口的方法也可以在接口声明时就默认实现。
5.4.1 判断继承关系
Test1 is A
Test1 is B
5.4.2 继承中的构造函数参数
open class A1(a:Int) { // 构造函数参数不加var,表示a只是构造函数中的一个参数
}
class B1(a:Int) : A1(a) {
}
open class A2(var a:Int) { // 构造函数参数加var,表示类中的方法可以访问这个属性
}
class B2(a:Int) : A2(a) {
}
open class A3(open var a:Int) { // 构造函数参数加open var,表示子类可对该属性重写
}
class B3(override var a:Int) : A3(a) {
}
5.4.3 接口代理
使用by关键字进行接口代理。如:
interface A {
fun a()
}
interface B {
fun b()
}
class Test1 : A, B {
override fun a() {
}
override fun b() {
}
}
class Test2(a: A, b: B) : A by a, B by b { // 使用by关键字代理实现接口
}
5.4.4 使用super指定父类
方法名冲突,使用super<A>来指定父类
interface A {
fun a(){
println("A.a")
}
}
interface B {
fun a(){
println("B.a")
}
}
class Test1 : A, B {
override fun a() { // 接口方法冲突,可以使有super<父类>来指定使用哪个父类的方法
super<A>.a()
}
}
5.5 object实现单例
在Kotlin中单例模式最基本的实现就是使用object。如:
object A {
fun a() {
println("A.a")
}
}
// 调用
A.a()
其反编后的Java代码是这样,这是一个饿汉式的单例:
public final class A {
public static final A INSTANCE;
public final void a() {
String var1 = "A.a";
boolean var2 = false;
System.out.println(var1);
}
private A() {
}
static {
A var0 = new A();
INSTANCE = var0;
}
}
5.6 data数据类
数据类,使用data关键字声明类。如:
data class A(val id:Int, val name:String)
// 调用
var a = A(1, "子云心")
val id1 = a.id
val id2 = a.component1()
val name1 = a.name
val name2 = a.component2()
val(id, name) = a
注意:
- data的数据类不能被继承。
- data的数据类它在生成的Java代码中可见存在着N个component方法,其中N是字段数。
- 除N个component方法外,编译器还帮我们生成了equals、hashCode、toString、copy等方法。
- data的数据类是final声明的,而且没有无参数重载版本的构造函数。
- 使用插件:NoArg可以生成无参函造方法;AllOpen可以去掉final的声明,它们都是在编绎期处理的。所以要想使用它们处理后的代码就要使用反射 。
- 关于如何引用插件,读者们可以自行搜索,因为它并不是语法范畴,所以这里并不打算在这作过多介绍。
5.6.1 componentN方法的使用
class A() {
operator fun component1():Int {
return 1
}
operator fun component2():String {
return "子云心"
}
}
// 调用
var a = A()
val(id, name) = a
5.7 伴生对象与静态成员
Kotlin使用静态方法时,建议使用包级函数,就是直接在包下写函数,不需要在类里写方法。但是如果一定需要像Java一样在类里的静态方法,可以使用伴生对象。如:
class A private constructor() {
companion object {
fun a(p:Int) {
println(p)
}
var TAG = "A"
}
}
class B private constructor() {
companion object {
@JvmStatic
fun b(p:Int) {
println(p)
}
@JvmField
var TAG = "B"
}
}
fun main(args:Array<String>) {
A.a(2)
println(A.TAG)
B.b(3)
println(B.TAG)
}
注意:
以上代码虽然使用起来跟Java的静态方法很像,但是实际上反编出对应的Java是这样的:
public final class A {
@NotNull
private static String TAG = "A";
public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null);
private A() {
}
public static final class Companion {
public final void a(int p) {
boolean var2 = false;
System.out.println(p);
}
@NotNull
public final String getTAG() {
return A.TAG;
}
public final void setTAG(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
A.TAG = var1;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
public final class B {
@JvmField
@NotNull
public static String TAG = "B";
public static final B.Companion Companion = new B.Companion((DefaultConstructorMarker)null);
private B() {
}
@JvmStatic
public static final void b(int p) {
Companion.b(p);
}
public static final class Companion {
@JvmStatic
public final void b(int p) {
boolean var2 = false;
System.out.println(p);
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
在Java的调用是这样,A和B的区别在于B的b方法上加了@JvmStatic,这样如果你的代码也需要运行在Java中便会比较方便地调用就可以这样做
A.Companion.a(2);
A.Companion.getTAG();
B.b(3);
String tag = B.TAG;
5.8 内部类
Kotlin中的内部类就是等于Java中的静态内部类。我们在Android开发中往往在使用非静态内部类中产生内存泄露情况,所以一般情况下都是使用静态内部类来实现相关的逻辑。而Kotlin考虑到此情况,所以默认就是静态内部类了。如果你一定需要使用非静态内部的话,需要使用inner关键字来声明内部类。如:
fun main(args: Array<String>) {
val inA = OutA.InA()
val inB = OutB().InB()
}
class OutA {
class InA { // 静态内部类
}
}
class OutB {
val b: Int = 0
inner class InB { // 非静态内部类
val b: Int = 1
fun fun1() {
println(b) // 这里引用的是内部类的b
println(this@OutB.b) // 这里引用的是外部类的b
}
}
}
注意:
在内部类中如果需要使用外部类的方法以,可以使用this@Outter。同理也存在this@Inner的用法。
5.8.1 匿名内部类
在Kotlin中使用匿名内部类有一处跟Java中不一样,但是匿名内部类也可以同时实现继承。如:
open class Test
interface OnClickListener {
fun onClick()
}
class View {
var onClickListener: OnClickListener? = null
}
// 调用
val view = View()
view.onClickListener = object : OnClickListener { // 正常创建
override fun onClick() {
}
}
view.onClickListener = object : Test(), OnClickListener { // 创建时同时继承于Test
override fun onClick() {
}
}
5.8.2本地类
Java中也有本地类的语法,不过我们用得比较少。本地类就是在方法中定义类。如:
fun main(args: Array<String>) {
class LocalClass{
}
}
5.8.3本地函数
Java中没有本地函数的概念,它对应本地类的概念,本地函数就是函数中定义函数。如:
fun main(args: Array<String>) {
fun LocalFunc{
}
}
5.9 enum枚举类
Android官方中已明确强烈不推荐使用枚举,使用注解来代替枚举。因为枚举占用的内存空间要比整型大。通过反编你就知明白,枚举最终会转化成一个继承于Enum的类,并且在类中存在着N个静态该类的字段以及一个该类的数组。所以在手机开发中,它跟简单的几个Int类型对比起来这份性能开销是完全没有优势的。
虽然我们Android开发中并不推荐使用枚举,但是Kotlin中枚举的语法我们还是需要懂的。使用如:
enum class LogLevel(val abbreviation:String) {
VERBOSE("V"),
DEBUG("D"),
INFO("I"),
WARN("W"),
ERROR("E"),
ASSERT("A"); // 注意这个分号,这可能是Kotlin目前唯一需要用到的分号
fun getTag(): String {
// ordinal是下标、abbreviation是构造方法传入的值、name是名称
return "$ordinal, $abbreviation , $name"
}
}
// 调用
println(LogLevel.ERROR.getTag())
println(LogLevel.valueOf("DEBUG")) // 获得DEBUG的对象
LogLevel.values().map(::println) // 获得所有对象
注意:
- 语法中出现了分号“;”,这可能是Kotlin目前唯一需要用到分号的地方。
- 枚举其实也是一个类。
- 枚举的也有构造方法,对应每一项也需要传入相应的值。
5.10 sealed密封类
密封类是Java中没有的概念,密封类就是一种特殊的抽象类,其实它就是子类可数的类。在Kotlin1.1之前,密封类的子类必须定义为其内部类,而在1.1之后子类只需要与密封类在同一个文件中即可。使用如:
sealed class A{
class A1(val a:Int):A()
class A2:A()
object A3:A()
}
5.11 inline内联类
内联类是Kotlin在1.3中才出现。它是对另外一个类型的包装,它类似于Java装箱类型的一种类型,编译器会尽可能使用被包装的类型进行优化。使用如:
inline class A(val value: Int) {
operator fun inc(): A { // ++运算符重载
return A(value + 1)
}
}
// 调用
var a = A(5)
a++
println(a)
注意:
- 内联类主构造器必须有且仅有一个只读属性。
- 内联类只能定义方法。
- 内联类可以实现接口,但不能继承父类也不能被继承。
- 内联类被包装的类型必须不能是泛型类型。
- 内联类不能定义为其他类的内部类。
使用场景:
- 官方所有无符号的类型都是使用内联类来实现的。
- 因为枚举的性能问题,一般可以使用内联类+伴生对象来代替。如:
inline class LogLevel(val ordinal: Int) {
companion object {
val VERBOSE = LogLevel(0)
val DEBUG = LogLevel(1)
val INFO = LogLevel(2)
val WARN = LogLevel(3)
val ERROR = LogLevel(4)
val ASSERT = LogLevel(5)
}
}
5.12 typealias类型别名
类型别名为现有类型提供了替代名称。如果类型名称太长,可以引入一个不同的较短名称并使用新的名称。
使用示例1:
fun getList1(list: ArrayList<(String, Int?) -> String>) {
// TODO
}
typealias MyList = ArrayList<(String, Int?) -> String>
fun getList2(list: MyList) {
// TODO
}
使用示例2:
typealias AbcMyData = com.zyx.abc.MyData
typealias DefMyData = com.zyx.def.MyData
fun getAbcMyData():AbcMyData {
// TODO
}
fun getDefMyData():DefMyData {
// TODO
}
未完,请关注后面文章更新…