美文网首页Android开发Android开发经验谈Android技术知识
Kotlin 类和对象(下)object对象的分析

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

作者: 小鱼人爱编程 | 来源:发表于2022-05-31 23:56 被阅读0次

    前言

    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 轻松入门系列

    相关文章

      网友评论

        本文标题:Kotlin 类和对象(下)object对象的分析

        本文链接:https://www.haomeiwen.com/subject/okgzprtx.html