Kotlin系列(二十一):Kotlin中的object?对象声明和对象属性

  发布日期:   2017-11-01
  最新修改:   2020-10-20
  阅读次数:   58 次

一、前言:

  • 我们知道,在Java中我们时长会需要定义更多单例对象或者创建一些匿名内部类来实现一些接口。

  • 在Java中,我们封装一些管理对象或者工具类时更多的会使用单例模式。

  • 本篇的主要内容是从kotlin编译后的源码上分析kotlin中如何实现java中的单利模式。

  • 同时我们也会学习kotlin中引入的新概念,kotlin中的对象表达式和对象声明


二、对象表达式和对象声明

  • 1、在使用Java语言进行开发时,遇到接口的实现我们一般有两种方式:

    • 1、当前类实现对应的接口,然后传递this进行监听或者是直接,优点,集中式管理,缺点,当实现的接口多时,就会比较混乱
    • 2、创建匿名类直接绑定接口;优点,代码集中、可读性比较好,缺点,目前我比较喜欢,所以也就没缺点了。
  • 2、有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。

    • 1、Java 用匿名内部类 处理这种情况。
    • 2、Kotlin 用对象表达式对象声明对这个概念稍微概括了下。

1、对象声明之单例模式

  • 单例模式是一种非常有用的模式,而 Kotlin(继 Scala 之后)使单例声明变得很容易:

  • 例如,我们在Java中使用单例模式,一般是这样子的:(饿汉模式)

      /**
       * Created by 安杰 on 2017/10/30. */public class SingleClass {
    
              private static SingleClass INSTANCE;
    
              private SingleClass() {
    
              }
    
              static {
                      INSTANCE = new SingleClass();
              }
    
              public static SingleClass getInstance() {
                      return INSTANCE;
              }
      }
  • 单例的懒汉模式

      /**
       * Created by 安杰 on 2017/10/30. */public class SingleClass {
    
              private static SingleClass INSTANCE = null;
    
              private SingleClass() {
    
              }
    
              public static SingleClass getInstance() {
                      if(INSTANCE ==null){
                              INSTANCE = new SingleClass();
                      }
                      return INSTANCE;
              }
      }
  • 而在Kotlin中使用单例模式就非常的简单了。

  • 在Kotlin单例模式代码如下:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/30. */
      object SingleClass{
    
      }
  • 就这么一句,是不是很简单?

  • 看似简单,其实内部是kotlin在编译的时候帮我们做了很多事情,因为实现单例的代码基本都是一个套路:

    • 1、创建一个静态的单例类类型的实例;
    • 2、声明一个私有的构造函数,目的是不给外部创建新的实例达到权限控制的木的;
    • 3、提供一个public的方法来给外部提供一个获取实例的方法;
    • 4、创建实例时可使用懒汉模式创建或者饿汉模式创建内部实例
  • 我们看下kotlin反编译后的java代码:

      package com.anjie.demo;
    
      import kotlin.Metadata;
    
      @Metadata(
           mv = {1, 1, 7},
           bv = {1, 0, 2},
           k = 1,
           d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
           d2 = {"Lcom/anjie/demo/SingleClass;", "", "()V", "production sources for module KotlinDemo_main"}
      )
      public final class SingleClass {
           public static final SingleClass INSTANCE;
    
           private SingleClass() {
                  INSTANCE = (SingleClass)this;
           }
    
           static {
                  new SingleClass();
           }
      }
  • 代码解析:

  • Metadata部分可以不用理会,我们之间看下面,ok,是不是恍然大悟,其用一个static代码块来创建实例,也就是饿汉模式。

  • 从上面我们也可以看到,Kotlin并没有开辟出一个新的语言,而是通提供一些新的语法来减少我们所需编写的重复性的代码。

  • 相信看了上面的,object的用法也就不需要我再继续讲解了。

2、对象声明之伴生对象

  • 在讲解kotlin中的伴生对象之前,我想先引入一个java的设计模式-工厂设计模式。

  • 一般在java开发中,工厂模式是比较常用的一种模式,而Java中也是通过静态类来实现工厂模式,如下我们创建一个Person类,简单的模拟工厂模式,我们通过传入name来生产不同名字的人:

      public class Person {
              private String name;
          public String getName() {
              return name;
          }
    
          public void setName(String name) {
              this.name = name;
          }
    
              public Person(String name) {
                      this.name = name;
                      System.out.println("生产了一个:" + name);
              }
    
              public static Person.Factory Factory = new Person.Factory();
    
              public static class Factory {
                      private String name = "";
                      private int age = 0;
    
                      public Factory setName(String name) {
                              this.name = name;
                              return this;
                      }
    
                      public final Person create() {
                              return new Person(this.name);
                      }
    
                      private Factory() {
                      }
    
              }
      }
  • 测试代码如下:

      @Test
      public void test1() {
              Person person = Person.Factory.setName("anjie").create();
              System.out.println(person.getName());
      }
  • 结果输出如下:

生产了一个:anjie anjie

  • OK,然后我们来看下kotlin如何实现该模式,代码如下:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/11/1. */class Person {
              var name: String = "";
    
              constructor(name: String) {
                      this.name = name;
                      println("生产了一个:$name");
              }
    
              companion object Factory {
                      var name: String = "";
                      fun setName(name: String): Factory {
                              this.name = name;
                              return this;
                      }
    
                      fun create(): Person = Person(name);
              }
    
      }
  • 测试代码如下:

      @Test
      fun test6() {
              var p = Person.setName("anjie").create();
              println(p.name)
      }
  • 结果输出如下:

生产了一个:anjie anjie

  • 可以看到,实现同样的代码,Kotlin会更简单,有些人说kotlin的语法更发杂了,但是在我肯来,其只是对于java中一些常用的代码进行了封装,减少编写重复的代码而已。

  • 上面也可以用Person.Factory.setName("anjie").create();进行调用,只是kotlin连这步都帮我们省去了而已,你怎么喜欢怎么来。

  • 如上,我们在类内部用companion关键字标识的对象即为伴生对象。相信通过上面的案例,不必说怎么用大家都会用kotlin中的伴生对象了。

通过源码重新认识伴生对象
  • 为了更好的理解kotlin中的伴生对象,我们通过查看反编译后的java代码看一下:

      package com.anjie.demo;
    
      import kotlin.Metadata;
      import kotlin.jvm.internal.DefaultConstructorMarker;
      import kotlin.jvm.internal.Intrinsics;
      import org.jetbrains.annotations.NotNull;
    
      public final class Person {
           @NotNull
          private String name$1;
           @NotNull
          private static String name = "";
           public static final Person.Factory Factory = new Person.Factory((DefaultConstructorMarker)null);
    
           @NotNull
          public final String getName() {
                  return this.name$1;
           }
    
           public final void setName(@NotNull String var1) {
                  Intrinsics.checkParameterIsNotNull(var1, "");
                  this.name$1 = var1;
           }
    
           public Person(@NotNull String name) {
                  Intrinsics.checkParameterIsNotNull(name, "name");
                  super();
                  this.name$1 = "";
                  this.name$1 = name;
                  String var2 = "生产了一个:" + name;
                  System.out.println(var2);
           }
         public static final class Factory {
                @NotNull
        public final String getName() {
                     return Person.name;
                }

                public final void setName(@NotNull String var1) {
                     Intrinsics.checkParameterIsNotNull(var1, "");
                     Person.name = var1;
                }

                @NotNull
        public final Person.Factory setName(@NotNull String name) {
                     Intrinsics.checkParameterIsNotNull(name, "name");
                     this.setName(name);
                     return this;
                }

                @NotNull
        public final Person create() {
                     return new Person(((Person.Factory)this).getName());
                }

                private Factory() {
                }

                // $FF: synthetic method
        public Factory(DefaultConstructorMarker $constructor_marker) {
                     this();
                }
         }
    }
  • 代码解析:
  • 可以看到,其生成的java代码与我们自己写的如出一辙。如果你有java开发经验,推荐通过尝试把kotlin反编译为java来学习其特性。

3、对象表达式

  • 要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:

      window.addMouseListener(object : MouseAdapter() {
              override fun mouseClicked(e: MouseEvent) {
                      // ……
              }
    
              override fun mouseEntered(e: MouseEvent) {
                      // ……
              }
      })
  • 其等价于java的

      window.addMouseListener(new MouseAdapter() {
              override fun mouseClicked(e: MouseEvent) {
                      // ……
              }
    
              override fun mouseEntered(e: MouseEvent) {
                      // ……
              }
      })
  • 如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。

  • 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

    open class A(x: Int) {
            public open val y: Int = x
    }

    interface B {……}

    val ab: A = object : A(1), B {
            override val y = 15
    }
  • 上面这句你可以这样理解,object想象为java的new,也就是其new一个类A,且同时实现了接口B,然后将其值赋值给ab。

  • 任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:

      fun foo() {
              val adHoc = object {
                      var x: Int = 0
                      var y: Int = 0
              }
              print(adHoc.x + adHoc.y)
      }
  • 我们看其编译为java后的代码:

      public final void foo() {
              adHoc = new Object() {
                  private int x;
                  private int y;
    
                  public final int getX() {
                       return this.x;
                  }
    
                  public final void setX(int var1) {
                       this.x = var1;
                  }
    
                  public final int getY() {
                       return this.y;
                  }
    
                  public final void setY(int var1) {
                       this.y = var1;
                  }
           };
           int var2 = (()adHoc).getX() + (()adHoc).getY();
           System.out.print(var2);
      }
  • 可以看到,其就相当于创建了一个Object对象,里面有x,y两个成员,从这可以再次看出,kotlin只是对语法做了些许变化,只要了解其原理,用起来就会非常顺手了。

  • 请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。

  • 也就是说如果你使用匿名对象作为公有函数的 返回类型 或者 用作 公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。

      class C {
              // 私有函数,所以其返回类型是匿名对象类型
              private fun foo() = object {
                      val x: String = "x"
              }
    
              // 公有函数,所以其返回类型是 Any
              fun publicFoo() = object {
                      val x: String = "x"
              }
    
              fun bar() {
                      val x1 = foo().x        // 没问题
                   // val x2 = publicFoo().x  // 错误:未能解析的引用“x”
              }
      }
  • 我们看对应的java代码:

      package com.anjie.demo;
    
      import kotlin.Metadata;
      import org.jetbrains.annotations.NotNull;
    
      public final class C {
           private final <undefinedtype> foo() {
                  return (<undefinedtype>)(new Object() {
                       @NotNull
          private final String x = "x";
    
                       @NotNull
          public final String getX() {
                              return this.x;
                       }
                  });
           }
    
           @NotNull
          public final Object publicFoo() {
                  return new Object() {
                       @NotNull
          private final String x = "x";
    
                       @NotNull
          public final String getX() {
                              return this.x;
                       }
                  };
           }
    
           public final void bar() {
                  String x1 = this.foo().getX();
           }
      }
  • 代码解析:可以看到

  • 1、使用私有函数接收匿名对象时,其直接返回的时(new Object(){})类型,也就是Object的自类型,其包含了新定义的x成员。

  • 2、使用公共函数接收匿名对象时,其返回的时Object类型,因为Object并没有x成员,所以此时调用就会报错,

  • 但是,看到这里,聪明的你肯定会说,将其强转即可,但是,我们的可是匿名函数,你强转为什么类型呢?此时就尴尬了,所以,使用匿名函数时,需要注意这点。

匿名函数内访问外部变量不需要时final
  • 就像 Java 匿名内部类一样,对象表达式中的代码可以访问来自包含它的作用域的变量。 (与 Java 不同的是,这不仅限于 final 变量。)

  • 我们看一下示例代码:

          @Test
          fun test7(){
                  fun a(i:MyInterface){
    
                  }
                  var b:Int = 0;
                  a(object :MyInterface{
                          override fun bar() {
                                  b = 2;
                  }
                  })
          }
  • 对应的java代码

      @org.junit.Test
      public final void test7() {
              a$ = null.INSTANCE;
           final IntRef b = new IntRef();
           b.element = 0;
           a$.invoke((MyInterface)(new MyInterface() {
                  public void bar() {
                       b.element = 2;
                  }
           }));
      }
  • 可以看到,之所以不需要使用final的原因是因为其对操作的对象在kotlin默认为final类型,b的引用地址不可再变化,但是其内部的element成员是可以改变的,所以再接口回调里我们可以随意改变element的值。

  • 在学kotlin时,一定要通过表象看本质,不要被其所蒙蔽。


三、对象表达式和对象声明之间的语义差异

  • 对象表达式和对象声明之间有一个重要的语义差别:
    • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

四、总结

  • 通过本章,相信大家已经能够根据本章节内容使用kotlin实现单例模式和工厂模式等。
  • 对于object的本质也有了初步的认识,相信使用object来声明匿名对象也不在话下。
  • 感谢您的观看,谢谢!

   转载规则

《Kotlin系列(二十一):Kotlin中的object?对象声明和对象属性字》GajAngels 采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。