Kotlin面向对象
类的定义与初始化
示例代码如下所示:
//需要被继承的话,需要加上open标识
//类的成员需要加上var标识
open class Human(var name: String, var age: Int) {
//构造方法的方法体
init {
println("我是一个${this.javaClass.simpleName},$name,今年岁$age")
}
}
//通过冒号:来继承,成员的初始化类似于C++
class Man(name: String, age: Int) : Human(name, age) {
//成员函数
fun say() {
println("你好,我是$name,年龄是$age")
}
}
//类只有属性没有方法的话,就不需要大括号了
class Woman(name: String, age: Int) : Human(name, age)
fun main(args: Array) {
//构造类的对象
val m = Man("楠宝宝", 20)
m.say()
val wm = Woman("璐宝宝", 18)
//类似于Java,Kotlin中任意类都是Any的子类
println(m is Any)
}
其中,类的最完整的写法是要加上修饰符和constructor(构造函数):
open class Human public constructor(var name: String, var age: Int) {
init {
println("我是一个${this.javaClass.simpleName},$name,今年岁$age")
}
}
类的属性
如下面的代码所示,类的属性可以在构造方法的参数列表中用var或者val定义,也可以在类内部定义。(注意:没有var或者val声明的都为构造方法的一般形参)
class Man(var name: Int, args: Int) {
var age: Int = 0
}
Kotlin中默认为属性实现了get/set方法,但是我们可以重写,可以修改访问权限,比如我们在get/set中打印一句话:
class Man(var name: Int, args: Int) {
var age: Int = 0
protected set(value) {
println("我是set方法")
field = value
}
get() {
println("我是get方法")
return field
}
}
属性的初始化:
class X
class Man(var name: Int, args: Int) {
//基本的几种类型可以直接用默认值初始化
var age: Int = 0
//var用lateinit关键字延迟初始化
lateinit var s: String
//val用lazy延迟初始化
val x: X by lazy {
//传入的是一个Lambda表达式,返回值是X类型
X()
}
}
- 属性的初始化尽量在构造方法中完成,如果无法完成初始化,那么尝试降级为局部变量
- 基本的几种类型可以直接用默认值初始化,var用lateinit关键字延迟初始化,val用lazy延迟初始化
- 可空类型慎用null初始化,因为可空类型使用起来比较麻烦
接口与抽象类
接口可以理解为是一种协议,不能够有状态;而抽象类可以理解为半成品,可以有状态。
例子代码如下:
interface A {
//接口中的变量不可以初始化,也就是不能有状态,必须由类实现后使用
var i: Int
//与Java不一样,Kotlin中的接口可以有没有状态的默认实现
fun a() {
println(i)
}
}
abstract class B {
//抽象类中的变量必须初始化
var j = 10
//抽象方法必须复写
abstract fun b()
//非抽象方法不用复写,需要添加open关键字才能覆写
fun c(){
}
}
class C(override var i: Int) : B(), A {
override fun a() {
super.a()
}
override fun b() {
}
}
接口代理
通过by关键字可以进行接口代理:
interface IA {
fun a()
}
class A() : IA {
override fun a() {
println("test")
}
}
//接口代理,IA的实现实际上是交给a来进行代理
class B(val a: A) : IA by a {
}
fun main(args: Array) {
val a = A()
val b = B(a)
b.a()
}
接口方法冲突
接口方法可以有默认实现。
接口冲突:签名一致,而且返回值类型相同的冲突。这时候子类(实现类)必须覆写冲突的方法,并且通过泛型类解决冲突,例如:
interface A {
fun a(): Int {
return 1
}
}
interface B {
fun a(): Int {
return 2
}
}
abstract class C() {
open fun a(): Int {
return 2
}
}
class D() : C(), A, B {
override fun a(): Int {
return super.a()
}
}
继承
- 父类需要open才能被继承,抽象类除外
- 父类的方法、属性需要open才可以被覆写,默认都final
- 接口、接口方法、抽象类默认为open
- 覆写父类、接口的成员需要override关键字
- 继承类的时候实际上调用的父类的构造方法
类成员的可见性
Kotlin中比较特殊的是internal关键字,是模块(Module)可见。
单例
通过object关键字可以实现最简单的单例:
object MusicPlayer {
fun play() {
println("啦啦啦")
}
}
fun main(args: Array) {
MusicPlayer.play()
}
注意:
- 这种单例只有一个实现的对象
- 不能自定义构造方法
- 可以实现接口、继续父类
本质上,通过反编译成Java代码之后可以看到其实是最简单的单例的实现:
public final class MusicPlayer {
public static final MusicPlayer INSTANCE;
public final void play() {
String var1 = "啦啦啦";
System.out.println(var1);
}
private MusicPlayer() {
INSTANCE = (MusicPlayer)this;
}
static {
new MusicPlayer();
}
}
在Java中是这样使用这个单例的:
MusicPlayer.INSTANCE.play();
伴生对象与静态成员
注意点:
- 每个类都可以对应一个伴生对象
- 伴生对象的成员全局只有一个,是单例
- 相当于Java中的静态成员
例子:
class MusicPlayer {
companion object {
var SONG = "《萌萌哒》"
@JvmField
var TAG = MusicPlayer.javaClass.simpleName
fun play(song: String) {
println("啦啦啦:$song")
}
@JvmStatic
fun stop() {
}
}
}
fun main(args: Array) {
val song = MusicPlayer.SONG
MusicPlayer.play(song)
}
在Java中调用的时候,加上JvmField、JvmStatic注解以后可以直接调用,不加的话必须间接调用,例如:
String song = MusicPlayer.Companion.getSONG();
MusicPlayer.Companion.play(song);
String tag = MusicPlayer.TAG;
MusicPlayer.stop();
注意,当不是特别必要的时候,应该考虑使用包级函数,这样就可以解决使用一个类的方法导入了整个类的麻烦了,例如:
public inline fun minOf(a: Int, b: Int): Int {
return Math.min(a, b)
}
方法重载与默认参数
JVM函数签名:函数名、参数列表,注意不包括返回值类型。
方法重载:方法重载主要是名称相同、参数不同的方法。
默认参数:可以为任意一个参数设置默认参数,函数调用产生混淆的时候使用具名参数即可。
下面是一个最简单的方法重载与默认参数的例子:
class A() {
@JvmOverloads
fun a(a: Int = 0) {
println(a)
}
fun a(s: String) {
println(s)
}
}
因为Java中没有默认参数,因此需要传参。需要添加@JvmOverloads才可以省略参数,直接使用默认参数。
Tips:尽量避免使用重载。
扩展成员
为了弥补一些类的不足,我们可以通过扩展成员来进行二次加工,例如我们可以扩展方法与属性:
//扩展方法
operator fun String.times(time: Int): String {
val sb = StringBuilder()
for (i in 0 until time) {
sb.append(this)
}
return sb.toString()
}
//扩展属性
val String.a: String
get() = "abc"
var String.b: String
get() = "abc"
set(value) {
}
fun main(args: Array) {
val s = "1"
//println(s.times(5))
println(s * 5)
println(s.a)
println(s.b)
}
注意:
- 扩展属性不能够直接初始化,需要通过实现get方法。
- Java调用扩展方法、属性的相关信息,暂时放一放……
属性代理
通过by关键字可以代理属性,类似于接口的代理:
class TestClass() {
val s1 by lazy {
"1"
}
val s2 by Delegates()
var s3 by Delegates()
override fun toString(): String {
return "被代理的TestClass"
}
}
class Delegates() {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("getValue 代理了 $thisRef 类的 ${property.name} 属性")
return "2"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("setValue 代理了 $thisRef 类的 ${property.name} 属性")
}
}
fun main(args: Array) {
val t = TestClass()
println(t.s1)
println(t.s2)
println(t.s3)
}
- 对于val,代理类需要有getValue方法
- 对于val,代理类需要有getValue方法以及setValue方法
Tips:代理是为了屏蔽内部的细节实现,例如我们有一个File属性,通过代理的方式,就可以把文件读取的代理写进代理里面了。
lazy也是一个代理,实现如下:
//Lazy的接口定义
public interface Lazy {
public val value: T
public fun isInitialized(): Boolean
}
//Lazy的扩展方法
@kotlin.internal.InlineOnly
public inline operator fun Lazy.getValue(thisRef: Any?, property: KProperty<*>): T = value
//而Lazy的实现类是SynchronizedLazyImpl
@kotlin.jvm.JvmVersion
public fun lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)
private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
private val lock = lock ?: this
//override了Lazy的value,并且重写了get方法
override val value: T
get() {
val _v1 = _value
//如果初始化了,直接返回
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
//如果没有初始化,那么需要进行初始化
//lazy有三种模式,默认是线程安全的
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
}
else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
lazy有三种模式,默认是线程安全的模式:
@kotlin.jvm.JvmVersion
public fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
例如我们需要使用非线程安全的lazy:
val s1 by lazy(mode = LazyThreadSafetyMode.NONE) {
"1"
}
数据类
类似于Java中的Bean,Kotlin中有数据类data class:
data class Person(val name: String, val age: Int)
fun main(args: Array) {
val p = Person("璐宝宝", 18)
println(p)
}
通过观看Kotlin的字节码,然后反编译成Java代码:
public final class Person {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final Person copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new Person(name, age);
}
// $FF: synthetic method
// $FF: bridge method
@NotNull
public static Person copy$default(Person var0, String var1, int var2, int var3, Object var4) {
if((var3 & 1) != 0) {
var1 = var0.name;
}
if((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
public String toString() {
return "Person(name=" + this.name + ", age=" + this.age + ")";
}
public int hashCode() {
return (this.name != null?this.name.hashCode():0) * 31 + this.age;
}
public boolean equals(Object var1) {
if(this != var1) {
if(var1 instanceof Person) {
Person var2 = (Person)var1;
if(Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
可以看到:
- Kotlin的数据类默认实现了toString、copy等方法
- Kotlin的数据类是一个final类,不可以被继承,也没有默认的无参构造方法
- Kotlin中有componentN方法,用于返回第几个属性
componentN方法
我们在Kotlin中可以这样赋值,本质上是调用了componentN方法:
val p = Person("璐宝宝", 18)
val (name, age) = p
通过看字节码,可以看到的确是调用了componentN方法:
LINENUMBER 8 L1
NEW com/nan/kotlin/Person
DUP
LDC "\u7490\u5b9d\u5b9d"
BIPUSH 18
INVOKESPECIAL com/nan/kotlin/Person. (Ljava/lang/String;I)V
ASTORE 1
L2
LINENUMBER 9 L2
ALOAD 1
ASTORE 4
ALOAD 4
INVOKEVIRTUAL com/nan/kotlin/Person.component1 ()Ljava/lang/String;
ASTORE 2
ALOAD 4
INVOKEVIRTUAL com/nan/kotlin/Person.component2 ()I
ISTORE 3
ACONST_NULL
ASTORE 4
L3
又例如我们在遍历数组的时候也可以这样写:
for ((index, value) in args.withIndex()) {
println("$index : $value")
}
withIndex()方法的实现如下:
public fun Array.withIndex(): Iterable> {
return IndexingIterable { iterator() }
}
其中IndexedValue就是一个数据类:
public data class IndexedValue(public val index: Int, public val value: T)
当然,一般的类也可以有componentN方法,注意是需要加上operator,因为这是操作符重载的方法:
class Test() {
operator fun component1(): String {
return "哈哈"
}
operator fun component2(): Int {
return 1
}
}
fun main(args: Array) {
val (str, i) = Test()
}
构造方法与final的解决
因为Kotlin中的数据类都是final类不能被继承,又没有提供默认的构造方法,因此不能被一些含有反射构造类的框架使用。为了解决这个问题,需要引入两个插件,gradle脚本如下:
buildscript {
//省略一些代码
dependencies {
//添加两个插件
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
}
}
//省略一些代码
apply plugin: 'kotlin-noarg'
apply plugin: 'kotlin-allopen'
//添加配置
noArg{
annotation("com.nan.annotations.PoKo")
}
allOpen{
annotation("com.nan.annotations.PoKo")
}
//省略一些代码
其中的PoKo注解是一个自定义的注解:
annotation class PoKo
最后通过看字节码可以发现我们的数据类已经不是final类型,并且有默认的构造方法了:
public class Person {
//省略一些代码
public Person() {
}
}
但是这是在编译出来字节码之后修改字节码实现的,我们自己调用的时候还是不能访问。但是我们可以通过类似于反射的手段来进行访问。
内部类
内部类的访问控制
先来看一段Java代码:
public class OutterJava {
//注意这里的访问权限
public class InnerJava {
}
public static void main(String[] args) {
}
}
注意,内部的访问控制分为以下几种:
- private:只有在外部类OutterJava的范围内可以访问
- protect:在外部类OutterJava及其子类的范围内可以访问
- 不写权限(默认):同一个包都可以访问
- public:所有类都可以访问
然后来看Kotlin代码:
class Outter {
private class Inner {
}
}
如此类推。
静态内部类与非静态内部类
先来说说非静态内部类,看一段Java代码:
public class OutterJava {
public int i;
public class InnerJava {
public void test() {
//非静态内部类持有外部类的this引用
//比如下面两句代码是等价的
System.out.println(i);
System.out.println(OutterJava.this.i);
}
}
public static void main(String[] args) {
//不能直接实例化内部类的对象
//InnerJava inner = new InnerJava();
OutterJava outter = new OutterJava();
//非静态内部类需要用外部类的对象进行实例化
InnerJava inner = outter.new InnerJava();
}
}
可以看出:
- 非静态内部类持有外部类的状态,即this引用
- 非静态内部类需要用外部类的对象进行实例化
下面来看静态内部类的Java代码:
public class OutterJava {
public int i;
public static class InnerJava {
public void test() {
//静态内部类持有外部类的this引用
//比如下面两句代码是等价的
//System.out.println(i);
//System.out.println(OutterJava.this.i);
}
}
public static void main(String[] args) {
InnerJava inner = new InnerJava();
}
}
可以看出:
- 静态内部类没有持有外部类的状态、this引用
- 静态内部类不需要用外部类的对象进行实例化
在Kotlin中,内部类默认都是静态内部类,如果需要定义非静态内部类,需要加上inner关键字:
class Outter {
val i: Int = 0
inner class Inner {
val i: Int = 1
fun test() {
//访问自己的i
println(this.i)
println([email protected])
//访问外部类的i
println([email protected])
}
}
}
fun main(args: Array) {
val outter = Outter()
val inner = outter.Inner()
}
下面来看看默认下的静态内部类:
class Outter {
val i: Int = 0
class Inner {
val i: Int = 1
fun test() {
//访问自己的i
println(this.i)
println([email protected])
//静态内部类不能访问外部类的i
//println([email protected])
}
}
}
fun main(args: Array) {
//静态内部类可以直接初始化
val inner = Outter.Inner()
}
使用场景:
- 内部实例必须依赖外部类的实例才能运行,使用非静态内部类
- 只是逻辑上的关系,可以不依赖实例的时候,使用静态内部类
匿名内部类
关于匿名内部类,看下面的例子:
open class Test {
}
interface OnClickListener {
fun onClick(view: View)
}
class View {
private var mOnClickListener: OnClickListener? = null
fun setOnClickListener(listener: OnClickListener) {
this.mOnClickListener = listener
}
}
fun main(args: Array) {
val view = View()
view.setOnClickListener(object : Test(), OnClickListener {
override fun onClick(view: View) {
}
})
}
注意:
- 通过object类写一个匿名内部类。
- 与Java不同的是,写匿名内部类的时候,可以直接继承的关系,如上面的代码所示。但是这种用法用得不多。
- 通过查看字节码,匿名内部类在运行的时候还是会分配一个类名的,甚至我们可以通过反射去获取
枚举类的定义、静态方法和属性
枚举类也是一个类,只不过这种类比较特殊。下面是一个Kotlin的枚举类的基本使用例子:
enum class Lang(val mHello: String) {
ENGLISH("hello"),
CHINESE("你好");
fun sayHello() {
println(mHello)
}
}
fun main(args: Array) {
Lang.CHINESE.sayHello()
//打印顺序
println(Lang.CHINESE.ordinal)
println(Lang.CHINESE.name)
//通过名字求枚举
Lang.valueOf("CHINESE").sayHello()
//打印所有枚举
Lang.values().map(::println)
}
下面几个注意的地方的:
-
Kotlin中枚举类同过enum class类定义,在Java中只是把class省略了而已。编译出来的字节码内容如下:
//实质上Kotlin把枚举编译成字节码以后,构造方法protected,并且是final类型,不可继承 public final enum com/nan/kotlin/Lang extends java/lang/Enum { public final static enum Lcom/nan/kotlin/Lang; ENGLISH public final static enum Lcom/nan/kotlin/Lang; CHINESE //在这里的静态块(类加载的时候)里面初始化所有枚举实现 static
()V //构造方法是protect的,但是 protected (Ljava/lang/String;ILjava/lang/String;)V }
密封类
密封类是子类可数,枚举类是实例可数。
在Kotlin1.1之前密封类只能在内部类定义,1.1之后可以在同一个文件中定义。
//sealed class,子类可数
sealed class PlayerCmd {
//有很多个实例有参数,枚举不能做到,因为实例有限
class Play(val url: String, val position: Long = 0) : PlayerCmd()
class Seek(val position: Long = 0) : PlayerCmd()
//只有一个实例
object Pause : PlayerCmd()
object Resume : PlayerCmd()
object Stop : PlayerCmd()
}
对比枚举类:
//枚举,实例可数
enum class PlayerState{
IDEL,PAUSE,PLAYING
}
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。