阅读 446

让你迷惑的 Kotlin 代码之 interface 与 类委托

背景

看了 让你迷惑的 Kotlin 代码 有感, 自己前些天也遇到了一些令人困惑的 Kotlin 代码/语法糖实现, 在这里分享一下

上代码

下面反编译的代码都有修改, 优化了可读性

代码是一个简单的 类委托 + interface default method

object App {
    @JvmStatic
    fun main(args: Array<String>) {
        val origin = object : Service {
            override fun getName(): String = "origin"
        }

        val proxy = object : Service by origin {
            override fun getName(): String = "proxy"
        }
        proxy.printName()
    }
}

interface Service {
    fun getName(): String

    fun printName() {
        println(getName())
    }
}
复制代码

大家可以先想一下这段代码会输出什么内容


答案是 "origin", 这里可能会和预期有些不符

但是如果我们给printName() 添加一个 @JvmDefault注释, 答案就会是 "proxy"

interface Service {
    fun getName(): String

    @JvmDefault
    fun printName() {
        println(getName())
    }
}
复制代码

原理

这里涉及到 以下知识点:

  • kotlin interface default method 默认实现 和 加了@JvmDefault下的实现
  • 类委托的原理

interface default method 的原理

默认情况下

通过反编译得到的Java代码, 我们可以看到生成了一个 Service.DefaultImpls 类, 里面有我们定义的 Kotlin 中的默认方法 printName(Service), 该方法需要传入一个 Service 对象(关键点1).

Service 的子类对默认方法 printName() 的调用, 其实是调用了 Service.DefaultImpls 中的方法, 默认传入了 this(关键点2)

new Service() {
   @NotNull
   public String getName() {
      return "origin";
   }

   public void printName() {
      Service.DefaultImpls.printName(this);
   }
};
public interface Service {
   @NotNull
   String getName();

   void printName();

   public static final class DefaultImpls {
      public static void printName(@NotNull Service $this) {
         String var1 = $this.getName();
         boolean var2 = false;
         System.out.println(var1);
      }
   }
}
复制代码

添加 @JvmDefault 注解后

需要添加 gradle 配置

compileKotlin {
    kotlinOptions {
        // or freeCompilerArgs = ['-Xjvm-default=compatibility']
        freeCompilerArgs = ['-Xjvm-default=enable']
        jvmTarget = '1.8'
    }
}
复制代码

这里利用了 java 8 引入的 default 关键字, 使用了默认方法, 所以不会再生成其他对象了

new Service() {
   @NotNull
   public String getName() {
      return "origin";
   }
}
public interface Service {
   @NotNull
   String getName();

   @JvmDefault
   default void printName() {
      String var1 = this.getName();
      boolean var2 = false;
      System.out.println(var1);
   }
 }
复制代码

类委托的原理

我们来看反编译之后的代码

      Service proxy = new Service() {
         // $FF: synthetic field
         private final Service delegate = origin;

         @NotNull
         public String getName() {
            return "proxy";
         }

         public void printName() {
            this.delegate.printName();
         }
      };
复制代码

可以看到, 未重写的 printName() 方法, 内部调用了 delegate.printName()(关键点3)

到现在结论就很明显了:

  1. 在未使用注解时: proxy.printName() -> origin.printName() -> Service.DefaultImpls.printName(origin) -> origin.getName() -> 输出 "origin"

  2. 在使用 @JvmDefault 注解时: proxy.printName() -> proxy.getName() -> 输出 "proxy"

如何避免

  • 添加 @JvmDefault 注解, 这就不用说了

  • 传入指定对象; 既然 Service.printName() 在调用 Service.DefaultImpls.printName() 时会自动给我们传入当前对象, 我们只要给 Service.printName() 添加一个 Service 参数, 传入指定的对象就行了, 代码如下:

    interface Service {
        fun getName(): String
    
        fun printName(service: Service = this) {
            println(service.getName())
        }
    }
    复制代码

    不过这种方式的 Java 实现可能会和你想象中的有所不同, 如果有兴趣的话, 可以自己去反编译看看

文章分类
Android
文章标签