一、概览
1.1 object 关键字的几种用法
修饰类(对象声明) |
是一个饿汉单例,定义一个类并创建它的实例。 |
修饰伴生对象(静态调用) |
伴随着类而存在的对象,在类加载的时候被实例化,多个实例会共享。 |
对象表达式(匿名内部类) |
可以多继承多实现,访问外部函数中的局部变量不用加 final。 |
1.2 几种“Java静态方法”的方式
Kotlin 极度弱化了静态方法这个概念,提供了比静态方法更好用的语法特性,同样是通过【类名.函数名】来调用。
object | 适合写“全是静态方法的工具类”。(Kotlin v1.9 版本新增的 data object 是针对密封类/密封接口使用场景的优化,toString() 不会打印 HashCode 等无用信息,让输出更有意义) |
companion object | 适合给类定义“专门的静态方法”。 |
顶层函数 | 假设定义顶层函数 method() 的文件名为 ,编译器会创建一个 ,在Java中就可以 () 调用了。 |
@JvmStatic 注解 | 给上面的单例类或伴生对象添加注解后,在Java中就可以当做静态方法调用了。 |
二、单例(对象声明)
定义一个类并创建它的实例。可以继承类实现接口、包含属性和方法,但是不能手动声明构造。由于不能使用构造函数,可以使用 init()。
Kotlin声明:可以声明在顶层、也可以声明在类中(见下方和伴生对象对比)
object Demo{
val name = "单例"
fun show() = println(name)
}
()
println()
反编译成Java代码:(饿汉)
public final class Demo {
//成员变量
private static String name;
//通过静态字段提供实例
public static final Demo INSTANCE;
//私有化构造
private Demo() { }
//静态代码块中初始化
static {
Demo var0 = new Demo();
INSTANCE = var0;
name = "单例";
}
//成员方法
public final void show() {
String var1 = name;
(var1);
}
//生成的getter、setter
public final String getName() { return name; }
public final void setName(@NotNull String var1) {
(var1, "<set-?>");
name = var1;
}
}
模仿Java方式写Kotlin带参数单例:
class Demo private constructor(
private var id: Long,
private var name: String
) {
companion object {
private var instance: Demo? = null
fun getInstance(id: Long, name: String) {
instance ?: synchronized(this) {
instance ?: Demo(id, name).also { instance = it }
}
}
}
}
三、伴生对象(静态)
3.1 伴生对象 & 类中单例
- 伴随着类而存在的对象,在类加载的时候被实例化,多个实例会共享。Kotlin 中没有 static 静态的概念,可以使用顶层函数和常量。顶层函数不能访问类中私有的成员,在伴生对象中需要用外部实例来访问(静态内部类无法直接访问外部类中的非静态成员),例如Outter().num。
- 伴生对象的名称可以省略(默认为Companion),不管伴生对象的名称是否手动声明,都可以直接用外部类名调用伴生对象中的属性和方法。
- 当类中成员和伴生对象中成员重名的时候,类名调用的是伴生对象中的,实例调用的是类中的。
class Outer {
companion object Inner { //Inner名称可以省略
var str: String = ""
fun show(){}
}
}
fun main() {
= ""
()
= "" //编译器会提示 Inner 是多余的
() //编译器会提示 Inner 是多余的
val obj: =
= ""
()
}
伴生对象 |
类中单例对象声明 |
|
相同 | 都是一个静态内部类,通过(外部类名.内部类名.成员名)的方式调用。 构造都是 private 都可以继承类、实现接口、拥有属性和函数 |
|
不同 | ①外部类中创建并持有伴生对象的实例 ②定义的属性会成为外部类的私有静态字段,声明private便不会。函数还留在内部。 ③一个类中只能拥有一个伴生对象 |
①自己就是实例 ②持有自己的属性和函数 ③一个类中可以拥有多个单例对象 |
Kotlin代码:
class Demo {
//伴生对象
companion object One { //不取名的话,默认名称是 Companion
var a = 1
val b = 2
fun one() = println("伴生对象")
}
//单例
object Two {
var c = 3 //可以自定义getter、setter
val d = 4
fun two() = println("单例")
}
}
反编译成Java代码:
public final class Demo {
//伴生对象中的成员变量(外部类持有)
private static int a = 1;
private static final int b = 2;
//持有伴生对象实例(外部类持有)
public static final One = new ((DefaultConstructorMarker)null);
//伴生对象
public static final class One {
//私有化构造
private One() { }
// $FF: synthetic method 合成的方法
public AA(DefaultConstructorMarker $constructor_marker) {
this();
}
//成员方法
public final void one() {
String var1 = "伴生对象";
(var1);
}
//生成的getter、setter
public final int getA() { return ; }
public final void setA(int var1) { = var1; }
public final int getB() { return ; }
}
//单例
public static final class Two {
//通过静态字段提供实例
public static final INSTANCE;
//私有化构造
private Two() { }
//静态代码块中初始化
static {
var0 = new ();
INSTANCE = var0;
c = 3;
d = 4;
}
//成员变量
private static int c;
private static final int d;
//成员方法
public final void two() {
String var1 = "单例";
(var1);
}
//生成的getter、setter
public final int getC() { return c; }
public final void setC(int var1) { c = var1; }
public final int getD() { return d; }
}
}
在伴生对象中的定义的 val/var 反编译后的权限修饰符是private,因此会生成一个静态对象和 getter/setter,成本远超一个静态参数的价值:
- 静态常量:由于 val 反编译是 private static final,因此会生成 getter。使用 consta val 反编译是 public static final,就不会生成 getter 了。
- 静态变量:由于 var 反编译是 private static,因此会生成 getter&setter。使用 @JvmFeild 反编译是 public static,就不会生成 getter&setter。
3.2 静态工厂
interface Car {
val brand: String
companion object {
operator fun invoke(type: CarType): Car {
return when (type) {
-> Audi()
-> BMW()
}
}
}
}
Car()
3.3 扩展方法
虽然是在伴生对象上扩展,实际相当于给外部类增加了静态方法。
fun (){}
()
3.4 Java互调
反编译成 Java 代码发现伴生对象是外部类持有的一个静态实例,属性和方法的权限都是private,全都需要通过Outter().来调用的,想要通过 类名.XXX 调用,可以使用注解 @JvmStatic 修饰函数、@JvmField 修饰属性,就在外部类中增加了对应的 public static 字段和方法。
//Kotlin
class Demo {
companion object Three {
@JvmField
var a = 1
@JvmField
val b = 2
@JvmStatic
fun three() = println("单例")
}
//Java
class Demo {
public static int a = 1;
public static final int b = 2;
public static final void three() { (); }
}
四、对象表达式(匿名内部类)
解决一 | Java 在运行时将匿名内部类当作是它所继承/实现的父类/接口来使用,因此在匿名内部类中增加了父类/父接口之外的额外方法是无法正常使用的。Kotlin 用对象表达式取代了 Java 的匿名内部类解决了这一点。 |
解决二 | 访问外部函数中的局部变量也不用加 final(详见:Lambda闭包)。对SAM(单抽接口)一般使用 Lambda 更便捷,当需要重写多个抽象方法时,只能选择匿名对象了。 |
4.1 赋值给变量
4.1.1 无继承无实现
val obj = object {
init { println() }
val name = ""
fun show() { println() }
}
4.1.2 单个父类型
abstract class A{}
interface B{}
val obj1: A = object : A() {}
val obj2: B = object : B {}
4.1.3 多个父类型
顶层变量 成员变量 |
声明为 private 的变量其类型才能被正常识别(IDE显示类型为 <anonymous object : AA, BB>)。 |
声明为 public 的变量或当作函数返回值时,会被 Kotlin 识别为父类/父接口类型,由于有多个父类型需要显示声明为其中的一个,如果声明没有就会被识别为 Any 类型从而不能调用成员变量/方法。 | |
局部变量 | 不存在可见性修饰符,能被正常识别(IDE显示类型为 <anonymous object : AA, BB>)。 |
abstract class AA{ abstract fun aa() }
interface BB{ fun bb() }
//用作顶层变量
val obj1: AA = object: AA(), BB {
override fun aa() {}
override fun bb() {}
}
private val obj2 = object: AA(), BB {
override fun aa() {}
override fun bb() {}
}
//用作成员变量
class Demo {
val obj3: AA = object: AA(), BB {
override fun aa() {}
override fun bb() {}
}
private val obj4 = object: AA(), BB {
override fun aa() {}
override fun bb() {}
}
}
//用作局部变量
fun show(){
val obj = object : AA(), BB {
override fun aa() {}
override fun bb() {}
}
}
4.2 匿名对象传参、闭包
fun outer() {
var num = 0 //被访问的局部可以声明为 var,而不是有 final 的 val
val obj = object {
fun inner() { num += 1 }
}
}
//Android中设置点击监听时,访问外部局部变量
fun show() {
var num = 0
//用对象表达式写
(object : {
override fun onClick(v: View){ num += 1 }
})
//用 Lambda 简写(仅针对只有一个抽象方法的接口,多方法重写只能用对象表达式)
{ view -> num += 1 }
}