Kotlin 1.4-M3: Generating Default Methods in Interfaces

栏目: IT技术 · 发布时间: 4年前

内容简介:In Kotlin 1.4, we’re adding new experimental ways for generating default methods in interfaces in the bytecode for the Java 8 target. Later, we’re going to be deprecating theIn Kotlin, you can define methods with bodies in interfaces. It works if your code

In Kotlin 1.4, we’re adding new experimental ways for generating default methods in interfaces in the bytecode for the Java 8 target. Later, we’re going to be deprecating the @JvmDefault annotation in favor of generating all the method bodies in interfaces directly when the code is compiled in a special mode. Read more details of how it currently works and what will change, below.

Kotlin 1.4-M3: Generating Default Methods in Interfaces

In Kotlin, you can define methods with bodies in interfaces. It works if your code runs on Java 6 or 7, even before support for the default methods appeared on the JVM.

interface Alien {
   fun speak() = "Wubba lubba dub dub"
}

class BirdPerson : Alien

To make it work for older Java versions, the Kotlin compiler generates an additional class that contains an implementation of a default method as a static member. This is what the generated code looks like under the hood, at the bytecode level:

public interface Alien {
  String speak();

  public static final class DefaultImpls {
     public static String speak(Alien obj) {
        return "Wubba lubba dub dub";
     }
  }
}
public final class BirdPerson implements Alien {
  public String speak() {
    return Alien.DefaultImpls.speak(this);
  }
}

The Kotlin compiler generates the DefaultImpls class with the speak method. This method contains the default implementation. It takes an instance of an interface as a parameter and interprets it as this (in case you call other members of this interface inside). The class BirdPerson implementing the interface contains the same method, which only delegates to the implementation in DefaultImpls passing an actual this as an argument.

In Kotlin 1.2, we added experimental support for @JvmDefault annotation that works if your code targets Java 8. You can annotate each interface method having default implementation with @JvmDefault in order to get the default implementation generated in the bytecode:

interface Alien {
   @JvmDefault
   fun speak() = "Wubba lubba dub dub"
}

class BirdPerson : Alien

That only works in a special compiler mode : you can only use it when you specify the -Xjvm-default compiler argument.

The @JvmDefault annotation is going to be deprecated later. There’s no need to annotate each member with it; most probably you had to annotate all the interface methods with bodies, and it was quite verbose.

Eventually, we want to generate method bodies in interfaces by default when your code targets Java 8 or higher. It’s not easy to quickly make this change: we want to make sure you don’t have problems when you mix the libraries or modules of your application that are compiled with different Kotlin versions and different modes. The Kotlin compiler for future versions will continue to “understand” the old scheme of default methods, but we’ll slowly migrate to the new scheme.

New modes for generating default methods in interfaces

If your code targets Java 8 and you want to generate default methods in interfaces, you can use one of two new modes in Kotlin 1.4: -Xjvm-default=all or -Xjvm-default=all-compatibility .

In all mode, you only have default methods generated by the compiler, no more DefaultImpls objects, and no need to additionally annotate separate methods. This is the generated code for our initial sample:

// -Xjvm-default=all
public interface Alien {
  default String speak() {
     return "Wubba lubba dub dub";
 }
}
public final class BirdPerson implements Alien {}

Note that class BirdPerson implementing the interface doesn’t contain the speak method: it automatically reuses the “super” implementation thanks to the JVM support.

The Kotlin compiler of the newer versions will “understand” the old scheme. If your class compiled with the new scheme implements an interface compiled with the old scheme (with DefaultImpls ), the compiler will recognize this and generate a hidden method in the class that delegates to the corresponding DefaultImpls method, as before.

The only problem that may arise is if you recompile your old code with the default method implementation and some other code depends on it, which you don’t recompile. In this case, use the all-compatibility mode. Then both default method bodies and DefaultImpls classes are generated:

// -Xjvm-default=all-compatibility
public interface Alien {
  default String speak() {
     return "Wubba lubba dub dub";
  }

  public static final class DefaultImpls {
     public static String speak(Alien obj) {
        // Calling the default method from the interface:
        return obj.$default$speak();
     }
  }
}
public final class BirdPerson implements Alien {}

Inside DefaultImpls the Kotlin compiler calls specifically the default method defined in the interface. (To make it a non-virtual call, the compiler makes a special trick: it generates an additional synthetic method inside an interface and calls it instead.)

With all-compatibility mode you don’t need to recompile the classes that already use your interface; they continue to work correctly:

public final class Moopian implements Alien {
  public String speak() {
    return Alien.DefaultImpls.speak(this);
  }
}

all-compatibility mode guarantees binary compatibility for Kotlin clients but generates more methods and classes in the bytecode.

Fixing an issue with delegates

Before, it was a bit confusing to use an interface with @JvmDefault methods together with the “implementation by delegation” feature. If you used an interface with @JvmDefault as a delegate , the default method implementations were called even if the actual delegate type provided its own implementation:

interface Producer {
   fun produce() = "in interface"
}

class ProducerImpl : Producer {
   override fun produce() = "in class"
}

class DelegatedProducer(val p: Producer) : Producer by p

fun main() {
   val prod = ProducerImpl()
   // prints "in interface" if 'produce()' is annotated with @JvmDefault
   // prints "in class" in new jvm-default modes
   println(DelegatedProducer(prod).produce())
}

With the new jvm-default modes, it works as you would expect: the overridden version of produce is called when you delegate your implementation to the ProducerImpl class.

@JvmDefaultWithoutCompatibility

If you compile your code with all-compatibility mode and add a new interface, you can annotate it with the @JvmDefaultWithoutCompatibility annotation. It turns on “no compatibility mode” ( -Xjvm-default=all ) for this specific class. This way, no DefaultImpls objects will be generated. Since you’ve just added a new interface, there’s no code that calls it via the old scheme, and nothing can break.

To be precise, in all-compatibility mode you can use @JvmDefaultWithoutCompatibility to annotate all interfaces which aren’t a part of the public API (more correct is to consider public binary interface, and to say ABI), and therefore aren’t used by the existing clients.

More about all-compatibility mode for library authors

The all-compatibility mode is designed specifically for library authors to allow them to switch to the new scheme gradually and guarantee the binary compatibility for the library. And so, the following details and compatibility issues are aimed mainly at library authors.

Guaranteeing the binary compatibility between the new and old schemes is not totally “seamless”.

To prevent compatibility issues that might arise, the compiler reports an error in specific corner cases, while the @JvmDefaultWithoutCompatibility annotation suppresses this error. The following section describes the reasons for it and the use cases.

Consider a class that inherits from a generic interface:

interface LibGeneric<T> {
   fun foo(p: T): T = p
}

open class LibString : LibGeneric<String>

In -Xjvm-default=all-compatibility mode, the Kotlin compiler generates an error. Let’s first see why and then discuss how you can fix it.

Under the hood, to make such code work with DefaultImpls scheme, the Kotlin compiler of the previous version (or without using any -Xjvm-default flags) generates an additional method with the specialized signature in the class:

open class LibString {
   // Generated implicitly:
   fun foo(String): String { ... }
}

Sometimes, this specialized method is called in the generated bytecode. In pure Kotlin, it happens only in rare cases when your LibString class is open and you call foo from a subclass of LibString via super.foo() . In mixed projects, if you use this code from Java, the specialized version gets called every time you call foo on a LibString instance!

Without such override, you could easily break the binary compatibility: if you recompiled your LibString class with the new all-compatibility mode, and run it against the old binaries, you could get a NoSuchMethodError error!

The goal of all-compatibility mode is to guarantee binary compatibility at least for the Kotlin clients. That’s why having unexpected NoSuchMethodError errors is unacceptable. In order to prevent this, the Kotlin compiler could potentially generate the same hidden specialized method as before, however, it would cause problems when updating from all-compatibility to all mode, and it would also have issues with using default methods in diamond hierarchies. Generating such auxiliary implicit methods was necessary with the DefaultImpls scheme but is not needed when default methods are supported on the JVM level and can cause more confusion (for more details see Appendix: why we don’t like implicit methods ).

We decided to prevent this binary compatibility problem by making your choice explicit.

Fixing the compiler error

One option you have is to provide an explicit override:

interface LibGeneric<T> {
   fun foo(p: T): T = p
}

open class LibString : LibGeneric<String> {
   override fun foo(p: String): String = super.foo(p)
}

Yes, it’s a bit of verbosity but for a good reason! If this code can be used from subclasses in Kotlin or from Java, adding explicit override guarantees that the older binaries will continue to work with new versions of your library compiled in all-compatibility mode.

Another option is to annotate your class with the @JvmDefaultWithoutCompatibility annotation. It turns on “no compatibility mode” for this specific class. Then an explicit override method is not required and no implicit methods are generated:

interface LibGeneric<T> {
   fun foo(p: T): T = p
}

@JvmDefaultWithoutCompatibility
open class LibString : LibGeneric<String> {
    // no implicit member
}

Appendix: Why we don’t like implicit methods

Why don’t we generate hidden methods like in the old scheme? Consider the following diagram which represents a diamond hierarchy – the Java class JavaClass implements the Kotlin Base interface both through extending KotlinClass and implementing Derived interface:

Kotlin 1.4-M3: Generating Default Methods in Interfaces

Let’s imagine that Kotlin continues to generate implicit overrides (as was necessary before with the DefaultImpls scheme). Then the code JavaClass().foo() prints 0 and not 42 ! That becomes a new puzzler: there are only two methods (returning 0 and returning 42 ) and it’s really confusing why the method from the base class is called and not the more specific one from Derived . When you take into consideration an implicit method from KotlinClass , the result makes sense. But we really want to avoid such puzzlers by not generating the implicit methods in the first place – and rather force developers to provide explicit methods when it’s necessary for compatibility reasons.

Conclusion

If you used the @JvmDefault annotation before, you can safely remove it and use one of the new modes. If you already used -Xjvm-default=enable , which generated only the default method implementations, you can now replace it with -Xjvm-default=all .

So far this support remains experimental but we’re going to switch the default mode continuously first to all-compatibility and then to all in the future major Kotlin versions. If no -Xjvm-default is specified now, the generated code will continue to use DefaultImpls .

How to try it

You can already try these new modes with Kotlin 1.4-M3 version. See here how to update the Kotlin Plugin to it.

Share your feedback

We’re grateful for all your bug reports in ourissue tracker, and we’ll do our best to fix all the most important issues before the final release.

You are also welcome to join the #eap channel in our Kotlin Slack (get an invite here ). In this channel, you can ask questions, participate in discussions, and get notifications of new preview builds.

Let’s Kotlin!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

社群营销实战手册

社群营销实战手册

秋叶、邻三月、秦阳 / 人民邮电出版社 / 2018-1 / 69.00元

互联网正从“物以类聚”,走向“人以群分”的时代。秋叶等人的“社群营销”,并非单纯靠社群卖东西,而是建立一种中心化的、自行运转的生态,让“同好”们形成紧密的联系,创造出海量营销机会。 《社群营销实战手册 从社群运营到社群经济》共5章内容,从社群的定位、建立、扩张、变现、运营,到社群的生命周期延长、社群运营团队的打造和管理以及社群管理工具,大量干货秘笈一应俱全,并提供丰富的运营实战案例,全面解读社群的......一起来看看 《社群营销实战手册》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换