Kotlin与Java的不同之处

伴生对象

在 Kotlin 中并不没有 static 这个关键字,该如何处理呢?这里需要用到 Kotlin 的伴生对象来处理。

类内部的对象声明可以用 companion 关键字标记:

该伴生对象的成员可通过只使用类名作为限定符来调用:

可以省略伴生对象的名称,在这种情况下将使用名称 Companion:

伴生对象的作用

类似于 Java 中使用类访问静态成员的语法。因为 Kotlin 取消了 static 关键字,所以 Kotlin 引入伴生对象来弥补没有静态成员的不足。可见,伴生对象的主要作用就是为其所在的外部类模拟静态成员。

在 Java 代码中调用伴生对象

如何在 Java 代码中调用 Kotlin 的伴生对象呢?

  • 如果声明伴生对象有名称,则使用:
  • 如果声明伴生对象无名称,则采用 Companion 关键字调用:

@JvmField 和 @JvmStatic 的使用

在上面的例子中,我们知道了可以在 Java 代码中调用 Kotlin 中伴生对象的成员,类似于 Java 类中的静态成员。但是看上去和 Java 中的还是略有区别,因为类名和方法名/属性setter,getter方法名之间多了个伴生对象的名称或者 Companion 关键字。如何使其在调用的时候与 Java 中的调用看上去一样呢?

Kotlin 为我们提供了 @JvmField 和 @JvmStatic 两个注解。@JvmField 使用在属性上,@JvmStatic 使用在方法上。如:

这样我们在 Java 代码中调用的时候就和 Java 类调用静态成员的形式一致了,Kotlin 代码调用方式不变:

const 关键字

在伴生对象中,我们可能需要声明一个常量,目的是等同于 Java 中的静态常量。有两种方式,一种是上面所提到的使用 @JvmField 注解,另一种则是使用 const 关键字修饰。这两种声明方式都等同于 Java 中 static final 所修饰的变量。如下代码:

扩展属性和扩展方法

扩展函数

Kotlin的扩展函数可以让你作为一个类成员进行调用的函数,但是是定义在这个类的外部。这样可以很方便的扩展一个已经存在的类,为它添加额外的方法

下面我们为String添加一个toInt的方法

在这个扩展函数中,你可以直接访问你扩展的类的函数和属性,就像定义在这个类中的方法一样,但是扩展函数并不允许你打破封装。跟定义在类中方法不同,它不能访问那些私有的、受保护的方法和属性。

扩展函数的导入

我们直接在包里定义扩展函数。这样我们就可以在整个包里面使用这些扩展,如果我们要使用其他包的扩展,我们就需要导入它。导入扩展函数跟导入类是一样的方式。

有时候,可能你引入的第三方包都对同一个类型进行了相同函数名扩展,为了解决冲突问题,你可以使用下面的方式对扩展函数进行改名

扩展函数不可覆盖

扩展方法的原理

Kotlin 中类的扩展方法并不是在原类的内部进行拓展,通过反编译为Java代码,可以发现,其原理是使用装饰模式,对源类实例的操作和包装,其实际相当于我们在 Java中定义的工具类方法,并且该工具类方法是使用调用者为第一个参数的,然后在工具方法中操作该调用者

如:

反编译为对应的Java代码:

扩展属性

类的扩展属性原理其实与扩展方法是一样的,只是定义的形式不同,扩展属性必须定义get和set方法

为MutableList扩展一个firstElement属性:

反编译后的java代码如下:

内部类

kotlin的内部类与java的内部类有点不同java的内部类可以直接访问外部类的成员,kotlin的内部类不能直接访问外部类的成员,必须用inner标记之后才能访问外部类的成员

  • 没有使用inner标记的内部类

反编译后的java代码

  • 用inner标记的内部类

反编译后的java代码

从上面可以看出,没有使用inner标记的内部类最后生成的是静态内部类,而使用inner标记的生成的是非静态内部类

匿名内部类

匿名内部类主要是针对那些获取抽象类或者接口对象而来的。最常见的匿名内部类View点击事件:

上面这个是java匿名内部类的写法,kotlin没有new关键字,那么kotlin的匿名内部类该怎么写呢?

方法的参数是一个匿名内部类,先写object:,然后写你的参数类型View.OnClickListener{}

kotlin还有一个写法lambda 表达式,非常之方便:

数据类

在Java中没有专门的数据类,常常是通过JavaBean来作为数据类,但在Kotlin中提供了专门的数据类。

  • Java

从上面的例子中可以看到,如果要使用数据类,需要手动写相应的setter/getter方法(尽管IDE也可以批量生成),但是从代码阅读的角度来说,在属性较多的情况下,诸多的seeter/getter方法还是不利于代码的阅读和维护。

  • Kotlin
    在Kotlin中,可以通过关键字data来生成数据类:

即在class关键字之前添加data关键字即可。编译器会根据主构造函数中的参数生成相应的数据类。自动生成setter/getter、toString、hashCode等方法

要声明一个数据类,需要满足:

  • 主构造函数中至少有一个参数
  • 主构造函数中所有参数需要标记为val或var
  • 数据类不能是抽象、开发、密封和内部的

枚举类

枚举类是一种特殊的类,kotlin可以通过enum class关键字定义枚举类。

枚举类可以实现0~N个接口;

  • 枚举类默认继承于kotlin.Enum类(其他类最终父类都是Any),因此kotlin枚举类不能继承类;
  • 非抽象枚举类不能用open修饰符修饰,因此非抽象枚举类不能派生子类;
  • 抽象枚举类不能使用abstract关键字修饰enum class,抽象方法和抽象属性需要使用;
  • 枚举类构造器只能使用private修饰符修饰,若不指定,则默认为private;
  • 枚举类所有实例在第一行显式列出,每个实例之间用逗号隔开,整个声明以分号结尾;
  • 枚举类是特殊的类,也可以定义属性、方法、构造器;
  • 枚举类应该设置成不可变类,即属性值不允许改变,这样更安全;
  • 枚举属性设置成只读属性后,最好在构造器中为枚举类指定初始值,如果在声明时为枚举指定初始值,会导致所有枚举值(或者说枚举对象)的该属性都一样。

定义枚举类

枚举类实现接口

  1. 枚举值分别实现接口的抽象成员
  1. 枚举类统一实现接口的抽象成员
  1. 分别实现抽象枚举类抽象成员

委托

委托模式 是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承。

Java中委托:

Kotlin:

by表示 p 将会在 PrintImpl 中内部存储, 并且编译器将自动生成转发给 p 的所有 Printer 的方法。

委托属性

有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性 。

委托属性的语法是:

在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。

标准委托:

Kotlin 标准库为几种有用的委托提供了工厂方法。

  1. 延迟属性 Lazy
    lazy() 接受一个 lambda 并返回一个 Lazy实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。例如:
  1. 可观察属性 Observable
    Delegates.observable() 接受两个参数:初始值和修改时处理程序(handler)。每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值和新值:

如果想拦截赋的新值,并根据你是不是想要这个值来决定是否给属性赋新值,可以使用 vetoable() 取代 observable(),接收的参数和 observable 一样,不过处理程序 返回值是 Boolean 来决定是否采用新值,即在属性被赋新值生效之前 会调用传递给 vetoable 的处理程序。例如:

  1. 把属性存在map 中
    一个常见的用例是在一个映射(map)里存储属性的值。这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

例如:

在上例中,委托属性会从构造函数传入的map中取值(通过字符串键——属性的名称),如果遇到声明的属性名在map 中找不到对应的key 名,或者key 对应的value 值的类型与声明的属性的类型不一致,会抛出异常。

内联函数

当一个函数被声明为inline时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方

inline函数(内联函数)从概念上讲是编译器使用函数实现的真实代码来替换每一次的函数调用,带来的最直接的好处就是节省了函数调用的开销,而缺点就是增加了所生成字节码的尺寸。基于此,在代码量不是很大的情况下,我们是否有必要将所有的函数定义为内联?让我们分两种情况进行说明:

  1. 将普通函数定义为内联:众所周知,JVM内部已经实现了内联优化,它会在任何可以通过内联来提升性能的地方将函数调用内联化,并且相对于手动将普通函数定义为内联,通过JVM内联优化所生成的字节码,每个函数的实现只会出现一次,这样在保证减少运行时开销的同时,也没有增加字节码的尺寸;所以我们可以得出结论,对于普通函数,我们没有必要将其声明为内联函数,而是交给JVM自行优化。
  2. 将带有lambda参数的函数定义为内联:是的,这种情况下确实可以提高性能;但在使用的过程中,我们会发现它是有诸多限制的,让我们从下面的例子开始展开说明:

假如我们这样调用doSomething:

上面的调用会被编译成:

从上面编译的结果可以看出,无论doSomething函数还是action参数都被内联了,很棒,那让我们换一种调用方式:

上面的调用会被编译成:

doSomething函数被内联,而action参数没有被内联,这是因为以函数型变量的形式传递给doSomething的lambda在函数的调用点是不可用的,只有等到doSomething被内联后,该lambda才可以正常使用。

通过上面的例子,我们对lambda表达式何时被内联做一下简单的总结:

  1. 当lambda表达式以参数的形式直接传递给内联函数,那么lambda表达式的代码会被直接替换到最终生成的代码中。
  2. 当lambda表达式在某个地方被保存起来,然后以变量形式传递给内联函数,那么此时的lambda表达式的代码将不会被内联。

上面对lambda的内联时机进行了讨论,消化片刻后让我们再看最后一个例子:

上面的例子是否有问题?是的,编译器会抛出“Illegal usage of inline-parameter”的错误,这是因为Kotlin规定内联函数中的lambda参数只能被直接调用或者传递给另外一个内联函数,除此之外不能作为他用;那我们如果确实想要将某一个lambda传递给一个非内联函数怎么办?我们只需将上述代码这样改造即可:

很简单,在不需要内联的lambda参数前加上noinline修饰符就可以了。

以上便是我对内联函数的全部理解,通过掌握该特性的运行机制,相信大家可以做到在正确的时机使用该特性,而非滥用或因恐惧弃而不用。

Kotlin下单例模式

饿汉式实现

懒汉式

上述代码中,我们可以发现在Kotlin实现中,我们让其主构造函数私有化并自定义了其属性访问器,其余内容大同小异。

  • 如果有小伙伴不清楚Kotlin构造函数的使用方式。请点击 – – – 构造函数
  • 不清楚Kotlin的属性与访问器,请点击 – – -属性和字段

线程安全的懒汉式

大家都知道在使用懒汉式会出现线程安全的问题,需要使用使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加@Synchronized注解。

双重校验锁式

哇!小伙伴们惊喜不,感不感动啊。我们居然几行代码就实现了多行的Java代码。其中我们运用到了Kotlin的延迟属性 Lazy。

Lazy内部实现

观察上述代码,因为我们传入的mode = LazyThreadSafetyMode.SYNCHRONIZED,
那么会直接走 SynchronizedLazyImpl,我们继续观察SynchronizedLazyImpl。

Lazy接口

SynchronizedLazyImpl实现了Lazy接口,Lazy具体接口如下:

继续查看SynchronizedLazyImpl,具体实现如下:

SynchronizedLazyImpl内部实现

通过上述代码,我们发现 SynchronizedLazyImpl 覆盖了Lazy接口的value属性,并且重新了其属性访问器。其具体逻辑与Java的双重检验是类似的。

到里这里其实大家还是肯定有疑问,我这里只是实例化了SynchronizedLazyImpl对象,并没有进行值的获取,它是怎么拿到高阶函数的返回值呢?。这里又涉及到了委托属性

委托属性语法是:val/var <属性名>: <类型> by <表达式>。在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(和 setValue()——对于 var 属性)。

而Lazy.kt文件中,声明了Lazy接口的getValue扩展函数。故在最终赋值的时候会调用该方法。

静态内部类式

静态内部类的实现方式,也没有什么好说的。Kotlin与Java实现基本雷同。

补充

在该篇文章结束后,有很多小伙伴咨询,如何在Kotlin版的Double Check,给单例添加一个属性,这里我给大家提供了一个实现的方式。(不好意思,最近才抽出时间来解决这个问题)

其中关于?:操作符,如果 ?: 左侧表达式非空,就返回其左侧表达式,否则返回右侧表达式。请注意,当且仅当左侧为空时,才会对右侧表达式求值。

Kotlin 智能类型转换

对于子父类之间的类型转换

  • 先看这样一段 Java 代码
  • 尽管在 main 函数中,对 person 这个对象进行了类型判断,但是在使用的时候还是需要强制转换成 Student 类型,这样是不是很不智能?
  • 同样的情况在 Kotlin 中就变得简单多了
  • 在 Kotlin 中,只要对类型进行了判断,就可以直接通过父类的对象去调用子类的函数了

安全的类型转换

  • 还是上面的那个例子,如果我们没有进行类型判断,并且直接进行强转,会怎么样呢?
  • 结果就只能是 Exception in thread “main” java.lang.ClassCastException
  • 那么在 Kotlin 中是不是会有更好的解决方法呢?
  • 在转换操作符后面添加一个 ?,就不会把程序 crash 掉了,当转化失败的时候,就会返回一个 null

在空类型中的智能转换

  • 需要提前了解 Kotlin 类型安全的相关知识(Kotlin 中的类型安全(对空指针的优化处理))
  • aString 在定义的时候定义成了有可能为 null,按照之前的写法,我们需要这样写
  • 但是已经进行了是否为 String 类型的判断,所以就一定 不是 空类型了,也就可以直接输出它的长度了

T.()->Unit 、 ()->Unit

在做kotlin开发中,经常看到一些系统函数里,用函数作为参数

.()-Unit与()->Unit的区别是我们调用时,在代码块里面写this,的时候,两个this代表的含义不一样,T.()->Unit里的this代表的是自身实例,而()->Unit里,this代表的是外部类的实例

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注