前言
Kotlin 类和对象 系列
上篇分析了Kotlin类的一些知识,本篇将继续分析Kotlin 对象相关内容。
通过本篇文章,你将了解到:
1、object 关键字的应用场景
2、对象表达式使用(与Java 对比)
3、对象声明原理&使用(与Java 对比)
4、伴生对象原理&使用(与Java 对比)
1、object 关键字的应用场景
Java 中Object(首字母大写) 是所有类的超类,而在Kotlin 中却不是如此,Kotlin 中object(首字母小写)应用在三个地方:
接下来将逐一分析。
2、对象表达式使用(与Java 对比)
Java 匿名内部类
先看一个场景:
public interface JavaInterface {
//学生姓名
String getStuName();
//学生年级
int getStuAge();
}
public void getStuInfo(JavaInterface javaInterface) {
String name = javaInterface.getStuName();
int age = javaInterface.getStuAge();
}
定义了接口,该接口有两个方法,分别是获取学生姓名和年龄。
在类里定义一个方法,该方法参数为接口类型,方法内部通过接口对象获取姓名和年龄。
这种场景很常见,其实就是我们常说的回调接口。
调用方式:
//继承接口
class MyInter implements JavaInterface {
@Override
public String getStuName() {
return null;
}
@Override
public int getStuAge() {
return 0;
}
}
//调用
TestJava testJava = new TestJava();
//实例化接口
MyInter myInter = new TestJava().new MyInter();
//传入参数
testJava.getStuInfo(myInter);
既然getStuInfo()需要对象,而接口不能实例化,需要定义类去实现它,然后再在此类的基础上new 出对象。
在大部分场景下,我们是不需要单独定义一个类去实现接口的,简化写法如下:
TestJava testJava = new TestJava();
testJava.getStuInfo(new JavaInterface() {
@Override
public String getStuName() {
return "fish";
}
@Override
public int getStuAge() {
return 18;
}
});
可以看出,此时无需定义类,也就是没有了类名(匿名),只需要实现对应的方法即可。
这就是匿名内部类。
Kotlin 匿名内部类
还是上面的接口和方法,我们用Kotlin 实现匿名内部类:
var testJava = TestJava()
testJava.getStuInfo(object : JavaInterface {
override fun getStuName(): String {
return "fish"
}
override fun getStuAge(): Int {
return 18
}
})
规则:
object + ":" + 接口名/类名()
上述匿名内部类实现了接口,试着书写继承类的匿名内部类:
//Java 抽象类
public abstract class JavaAbClass {
public String getStuName() {
return null;
}
public abstract int getStuAge();
}
//调用
var testJava = TestJava()
testJava.getStuInfo(object : JavaAbClass() {
override fun getStuAge(): Int {
return 18
}
override fun getStuName(): String {
return "fish"
}
})
由此可以看出:
Kotlin 实现匿名内部类时,若是实现了接口则不用"()"调用,因为接口没有构造方法,若是继承了类,则需要使用"()"调用。
函数式接口与Lambda
Java Lambda优化
将上述接口简化:
public interface EasyJavaInterface {
//学生姓名
String getStuName();
}
该接口里只有一个方法。
接着在Java 里调用:
testJava.getStuInfo(new EasyJavaInterface() {
@Override
public String getStuName() {
return "fish";
}
});
写法没问题,不过编译器会提示:可以用Lambda表达式替换匿名内部类。
改造后如下:
testJava.getStuInfo(() -> "fish");
这么看就简洁了许多,这也是Java8 引入Lambda后经常用Lambda 代替此种形式接口,常用的如Java 构造线程、Android View点击事件:
//构造线程
new Thread(() -> {
Thread.sleep(100);
});
//点击事件,仅做使用展示,正式不要传入null
new View(null).setOnClickListener((v) -> {
v.setBackgroundColor(11);
});
由此引入了函数式接口:
当接口只有唯一的一个方法时,称为函数式接口。
当使用匿名内部类实现该接口时,可以用Lambda简化。
注:只是针对接口,类不适用。
Kotlin Lambda优化
Java 能使用Lambda简化调用,Kotlin 当然不会示弱:
//普通使用
testJava.getStuInfo(object : EasyJavaInterface {
override fun getStuName(): String {
return "fish"
}
})
//Lambda 代替
testJava.getStuInfo { "fish" }
Kotlin Lambda 表示更简洁了,连"()"都不需要了。
你说还不够直观,没关系我们用常用的线程构造举例:
//匿名类
Thread(object : Runnable {
override fun run() {
Thread.sleep(100)
Thread.sleep(200)
}
})
//Lambda 代替
Thread {
Thread.sleep(100)
Thread.sleep(200)
}
使用 Lambda 后确实使代码简洁了许多,不过有时候简洁也意味着不好快速理解。
如果你一时半会不知道该怎么用Lambda,那么先按照匿名内部类的实现方式书写,而后根据编译器的优化提示一键转Lambda。
若你想知道Lambda的详细规则,可以移步:包你懂Lambda
对象表达式其它特点
修改外部变量值
先看Java 表现:
//学生身高
int height = 0;
JavaInterface javaInterface = new JavaInterface() {
@Override
public String getStuName() {
//编译错误
// height = 180;
return "fish";
}
@Override
public int getStuAge() {
return 18;
}
};
此时想要修改height 值是不被允许的,原因:
height 在栈上分配,随着方法的执行完毕会释放,而匿名内部类被调用时height可能已经不存在。
再看Kotlin 表现:
var height = 0
var javaInterface = object : JavaInterface{
override fun getStuName(): String {
//编译正确
height = 99
return "name"
}
override fun getStuAge(): Int {
return 18
}
}
由此可见,Kotlin 匿名内部类是可以修改外部值的,原理:
Kotlin 检测到匿名内部类访问局部变量时,会将局部变量包裹到Ref类(int 类型对应IntRef)里,并在堆上new 出对象,而原始的变量存储在Ref.element里,这样即使函数执行结束后,依然可以访问。
扩展匿名内部类属性/函数
先看Java:
JavaInterface javaInterface1 = new JavaInterface() {
//新增分数
private float score;
public float getScore() {
return score;
}
@Override
public String getStuName() {
return null;
}
@Override
public int getStuAge() {
return 0;
}
};
//无法访问
javaInterface1.getScore();
如上在匿名内部类里新增score 变量,匿名内部类返回的对象无法访问score。
再看Kotlin 表现:
//扩展属性
var javaInterface1 = object :JavaInterface {
var score = 0f
override fun getStuName(): String {
return "fish"
}
override fun getStuAge(): Int {
return 18
}
}
//可以访问
javaInterface1.score = 88f
如此一来,使用Kotlin 实现匿名内部类增加了灵活性。
实现多接口/类
Java 书写匿名内部类时,只能继承单个类/实现单个接口,而使用Kotlin object 表达式则没有这个限制:
var multiple = object : JavaInterface, JavaAbClass2() {
override fun getStuName(): String {
return "fish"
}
override fun getStuAge(): Int {
return 18
}
override fun getScore(): Float {
return 88f
}
}
如上,不仅继承了类,也实现了接口,接口、类之间使用","分割。
注:Kotlin 和 Java 都不支持多继承。
作为变量/函数返回值展示
object 对象表达式不是非得要继承某个类/实现某个接口,可以仅仅简单扩展一下数据结构:
//对象
var tempObject = object {
var name : String ? = null
var age = 0
}
//调用
tempObject.age = 18
tempObject.name = "fish"
临时构造一个对象,无需声明类名等信息。
当然也可以作为函数的返回值:
fun tempFun() = object {
var name : String ? = null
var age = 0
}
tempFun().age = 18
tempFun().name = "fish"
不管是属性还是函数,其本质还是new 出一个对象。
注意:此种访问方式限制在本地和私有作用域访问。
如:
class ObjectExpression {
private fun tempFun() = object {
var name : String ? = null
var age = 0
}
fun tempFun2() = object {
var name : String ? = null
var age = 0
}
fun test() {
//ok
tempFun().age = 5
//报错
tempFun2().age = 6
}
}
3、对象声明原理&使用(与Java 对比)
Java 单例实现&使用
public class JavaSingleton {
private static volatile JavaSingleton instance;
private JavaSingleton(){}
//双重检测锁
public JavaSingleton getInstance() {
if (instance == null) {
synchronized (JavaSingleton.class) {
if (instance == null) {
instance = new JavaSingleton();
}
}
}
return instance;
}
}
这是一个典型的Java 单例构造方式,此处的构造方法设置为private是为了保持单例的唯一性。外部无法通过构造方法直接构造对象,必须通过getInstance()获取。
Kotlin 单例实现&使用
object KtSingleton {
var name: String? = null
var age: Int = 0
fun getStuName(): String {
return "name:$name"
}
}
对比起来,比Java 简单多了,只需要:
object + 类名 即可实现单例。
先看看Kotlin 里如何调用:
fun test() {
KtSingleton.getStuName()
KtSingleton.age = 18
}
很简单,类名+"."访问属性/函数即可。
再看Java 调用:
public void testKtSingleton() {
String name = KtSingleton.INSTANCE.getStuName();
int age = KtSingleton.INSTANCE.getAge();
}
相比Kotlin里调用,多了INSTANCE。
若想要与Kotlin里调用写法类似,可对object 对象声明做些改造:
object KtSingleton {
var name: String? = null
@JvmField
var age: Int = 0
@JvmStatic
fun getStuName(): String {
return "name:$name"
}
}
对属性使用@JvmField 注解,对函数使用@JvmStatic 注解,此时再在Java 代码里调用:
public void testKtSingleton() {
String name = KtSingleton.getStuName();
int age = KtSingleton.age;
}
很明显,与在Kotlin里调用方式一致了。
object 对象声明可以赋值给变量,后续可通过变量访问:
//赋值变量
var mySingleton = KtSingleton
//访问
fun test1() {
mySingleton.getStuName()
}
Kotlin 单例原理
object 对象声明反编译结果:
public final class KtSingleton {
private static String name;
private static int age;
public final String getStuName() {
return "name:" + name;
}
//静态实例
public static final KtSingleton INSTANCE;
//防止外部调用
private KtSingleton() {
}
static {
//构造对象
KtSingleton var0 = new KtSingleton();
INSTANCE = var0;
}
}
这也是构造单例的另一种方式:恶汉模式。
因此,在Java 里调用Kotlin 单例是通过:类名. INSTANCE 索引的。
再来看看加了@JvmField和@JvmStatic 的反编译结果:
public final class KtSingleton {
private static String name;
//被@JvmField 修饰,访问控制由private变为public
public static int age;
@NotNull
public static final KtSingleton INSTANCE;
//被@JvmStatic 修饰,由实例方法变为静态方法
public static final String getStuName() {
return "name:" + name;
}
private KtSingleton() {
}
static {
KtSingleton var0 = new KtSingleton();
INSTANCE = var0;
}
}
本质上是将实例方法变为了静态方法,将变量访问权限开放为public。
注:上述的构造函数都为private,就是为了禁止外部调用,因此Kotlin 对象表达式不允许书写构造函数。
4、伴生对象原理&使用(与Java 对比)
Java 静态方法
以学生信息为例:
public class JavaStatic {
private String name;
private int age;
private float score;
//构造Bean对象
public static JavaStatic buildBean() {
JavaStatic bean = new JavaStatic();
return bean;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
如上,JavaStatic 作为学生信息的Bean。为了外部调用方便,该类里提供了一个静态的方法用来构造Bean。
外部使用时:
List<JavaStatic> beanList = new ArrayList<>();
for (int i = 0; i < 100; i ++) {
beanList.add(JavaStatic.buildBean());
}
当然此处示例里buildBean()就只负责new对象,若是还有其它设置,那么优势就体现出来了:统一归类构建对象。
Kotlin 伴生对象使用
当试图在Kotlin 里复制Java 方式再写一套代码时,发现尴尬了,Kotlin 里没有"static"。
我们之前有提到过,Kotlin里可以使用顶层函数/属性 来实现类似Java 里的static 效果,实际上还有另外一种实现方式:
伴生对象(companion object)
class KotlinStatic {
private val name: String? = null
private val age = 0
private val score = 0f
companion object StudentFactory{
//伴生对象函数
fun buildBean(): KotlinStatic {
return KotlinStatic()
}
}
}
companion object + 类名 即可声明伴生对象。
外界如何调用呢?
先看Kotlin 里如何调用:
fun test() {
for (i in 1..100) {
//外层类名调用
KotlinStatic.buildBean()
}
}
再看Java 调用:
public void testKt() {
for (int i = 0; i < 100; i++) {
KotlinStatic.StudentFactory.buildBean();
}
}
Java 调用需要指明伴生对象名。
Kotlin 伴生对象原理
还是从反编译结果看:
public final class KotlinStatic {
private final String name;
private final int age;
private final float score;
//构造静态内部类实例
public static final KotlinStatic.StudentFactory StudentFactory = new KotlinStatic.StudentFactory((DefaultConstructorMarker)null);
//静态内部类
public static final class StudentFactory {
@NotNull
public final KotlinStatic buildBean() {
return new KotlinStatic();
}
private StudentFactory() {
}
}
}
很明显,所谓的伴生对象:
1、声明静态内部类。
2、构造静态内部类实例,该实例被外部类作为static 方式引用。
这就不难理解为啥Java 调用时需要指定伴生对象名,若是对Java 静态内部类有疑惑可查看上篇文章。
Kotlin 伴生对象一些特点
既然是静态内部类,那么当然无法访问外部类的成员属性/函数了,如下代码将会报错:
companion object StudentFactory{
//伴生对象函数
fun buildBean(): KotlinStatic {
//不允许访问外部变量
// score = 13.f
return KotlinStatic()
}
}
伴生对象还可以继承类/实现接口:
companion object StudentFactory : EasyJavaInterface {
//伴生对象函数
fun buildBean(): KotlinStatic {
//不允许访问外部变量
// score = 13.f
return KotlinStatic()
}
override fun getStuName(): String {
TODO("Not yet implemented")
}
}
一个类里只能声明一次伴生对象,鉴于此,我们可以省略对象名:
class KotlinStatic1 {
private val name: String? = null
private val age = 0
private val score = 0f
companion object {
//伴生对象函数
fun buildBean(): KotlinStatic {
return KotlinStatic()
}
}
}
Kotlin 访问时没变化,因为访问时和对象名无关。
而对于Java 访问,则变为如下:
KotlinStatic1.Companion.buildBean();
若是没有指定伴生对象名,则会生成默认的:Companion。
以上阐述了Kotlin 里object 的三种用法,分别从Java 角度、Kotlin 角度出发,说明其来源、能解决的问题、应用场景以及原理,希望对大家有所帮助。
前面几篇基础知识具备了,下篇开始正式进入协程的世界,相信一定会让大家轻松、自然、深刻理解协程。
本文基于Kotlin 1.5.3,文中Demo请点击
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android/Kotlin
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易懂易学系列
19、Kotlin 轻松入门系列
网友评论