Kotlin 类和对象(下)object对象的分析

2,419 阅读10分钟

前言

Kotlin 类和对象 系列

Kotlin 类和对象(上)类的分析
Kotlin 类和对象(下)object对象的分析

上篇分析了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 轻松入门系列