Когда Kotlin только появился, он уже обладал всем привычным теперь синтаксическим сахаром в виде неабстрактных методов интерфейсов и параметров функций по умолчанию. Тогда это была версия 1.0.0, совместимая с Java 6. Java 6 и 7 не умели создавать неабстрактные методы интерфейсов — эта возможность появилась только в Java 8.
Чтобы иметь такую функциональность, Kotlin генерировал специальный класс DefaultImpls. В нём располагались статичные методы, которые в Kotlin выглядели как обычные методы. Далее язык пошёл по долгому пути миграции на Java/JVM default method, появившиеся в Java 8.
C точки зрения Java выглядело это примерно так:
public interface BaseService { void doSomething(Object var1); void doSomethingWithDefaultParam(Object var1); public static final class DefaultImpls { public static void doSomething(BaseService2 $this, Object arg1) { System.out.println("do something " + arg1); } public static void doSomethingWithDefaultParam(BaseService2 $this, Object arg1) { System.out.println("do something " + arg1); } } }
Аннотация @JvmDefault появилась как стабильная фича в Kotlin 1.3. Она позволяла точечно (для конкретного неабстрактного метода интерфейса) генерировать настоящий Java/JVM default method вместо прежней схемы. Это было важно прежде всего для Java interop, то есть для вызова Kotlin-кода из Java.
В Kotlin 1.4 появился новый, более комплексный подход: проектные режимы all и all-compatibility. До этого Kotlin в основном продолжал жить через DefaultImpls, но теперь получил способ генерации настоящих JVM default methods для интерфейсов. Вместе с этим появляется новая аннотация:
@Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class JvmDefaultWithoutCompatibility
Для интерфейса генерируются только методы по умолчанию для JVM, без DefaultImpls. Для класса не генерируются реализации, вызывающие суперметоды или DefaultImpls-аксессоры.
Добавление этой аннотации к существующему классу приводит к бинарно несовместимым изменениям.
В Kotlin 1.5 target были объявлены устаревшими компиляция в байт-код Java 6, аннотация @JvmDefault и прежние режимы.
Новая аннотация появилась в Kotlin 1.6.20:
@Target(allowedTargets = [AnnotationTarget.CLASS]) annotation class JvmDefaultWithCompatibility
Она заставляет компилятор генерировать аннотированный класс или интерфейс в режиме -jvm-default=enable. Для интерфейса в дополнение к методам по умолчанию JVM генерирует методы доступа DefaultImpls. Для класса генерируются реализации, вызывающие суперметоды.
Kotlin 1.7 довёл поворот к Java 8 до логического конца: target 1.6 полностью удалили, а значение 1.8 осталось базовой нормой для Kotlin/JVM. В этот момент классы DefaultImpls стали очевидным legacy.
Kotlin 2.2.0 — это, пожалуй, кульминация всей истории. Начиная с этой версии, функции с реализацией в интерфейсах по умолчанию компилируются в JVM default methods, если не указано иное. Управление этим поведением переведели на стабильную опцию -jvm-default, а -Xjvm-default отметили как устаревшую.
enable стал по умолчанию, генерирует как DefaultImpls , так и JVM default.
no-compatibility не создаёт промежуточного класса DefaultImpls, только JVM default.
disable генерирует только DefaultImpls, как и в старых версиях по умолчанию.
Чтобы увидеть, как изменился байт-код, давайте сделаем следующее:
Создадим интерфейс с такими методами
Реализуем их в класс
Вызовем эти функции
Скомпилируем всё с разными параметрами
-jvm-defaultи посмотрим на разницу в байт-коде
interface BaseService { fun doSomethingAbstract(arg1: Any) fun doSomething(arg1: Any) = println("do something $arg1") fun doSomethingAbstractWithDefaultParam(arg1: Any = "default") fun doSomethingWithDefaultParam(arg1: Any = "default") = println("do something $arg1") }
object KotlinMain { @JvmStatic fun main(args: Array<String>) { val service: BaseService = BaseServiceImpl() service.doSomething(args) service.doSomethingWithDefaultParam() service.doSomethingWithDefaultParam("param") service.doSomething("param") service.doSomethingAbstractWithDefaultParam() service.doSomethingAbstractWithDefaultParam("param") } }
Условно, декомпилированные вызовы — это код до Kotlin 2.2 на стандартных настройках без дополнительных аннотаций в интерфейсах. Можно задать параметром ‑jvm‑default=disable:
public static final void main(@NotNull String[] args) { Intrinsics.checkNotNullParameter(args, "args"); BaseService service = (BaseService)(new BaseServiceImpl()); service.doSomething(args); // Видим что происходит вызов метода без переданного аргумента через static методы вложенного класса DefaultImpls. BaseService.DefaultImpls.doSomethingWithDefaultParam$default(service, (Object)null, 1, (Object)null); service.doSomethingWithDefaultParam("param"); service.doSomething("param"); // Здесь тоже BaseService.DefaultImpls.doSomethingAbstractWithDefaultParam$default(service, (Object)null, 1, (Object)null); service.doSomethingAbstractWithDefaultParam("param"); }
Вызовы на Kotlin 2.2 на стандартных настройках без дополнительных аннотаций в интерфейсах. Можно явно задать параметром ‑jvm‑default=enable:
public static final void main(@NotNull String[] args) { Intrinsics.checkNotNullParameter(args, "args"); BaseService service = (BaseService)(new BaseServiceImpl()); service.doSomething(args); // Видим, что static метод теперь находится в самом интерфейсе BaseService.doSomethingWithDefaultParam$default(service, (Object)null, 1, (Object)null); service.doSomethingWithDefaultParam("param"); service.doSomething("param"); // Здесь тоже BaseService.doSomethingAbstractWithDefaultParam$default(service, (Object)null, 1, (Object)null); service.doSomethingAbstractWithDefaultParam("param"); }
В байт-коде эти методы также помечены как synthetic. Отмечу, что для читаемости здесь приведена только часть байт-кода.
‑jvm‑default=disable
public abstract interface org/example/kotlin/defs/BaseService { @Lkotlin/Metadata... ... d2={"Lorg/example/kotlin/defs/BaseService;", "", "doSomethingAbstract", "", "arg1", "doSomething", "doSomethingAbstractWithDefaultParam", "doSomethingWithDefaultParam", "DefaultKotlinMethods"}) public final static INNERCLASS org/example/kotlin/defs/BaseService$DefaultImpls org/example/kotlin/defs/BaseService DefaultImpls public abstract doSomethingAbstract(Ljava/lang/Object;) public abstract doSomething(Ljava/lang/Object;) public abstract doSomethingAbstractWithDefaultParam(Ljava/lang/Object; public abstract doSomethingWithDefaultParam(Ljava/lang/Object;) }
‑jvm‑default=enable
public abstract interface org/example/kotlin/defs/BaseService { @Lkotlin/Metadata... ... d2={"Lorg/example/kotlin/defs/BaseService;", "", "doSomethingAbstract", "", "arg1", "doSomething", "doSomethingAbstractWithDefaultParam", "doSomethingWithDefaultParam", "DefaultKotlinMethods"}) public final static INNERCLASS org/example/kotlin/defs/BaseService$DefaultImpls org/example/kotlin/defs/BaseService DefaultImpls public abstract doSomethingAbstract(Ljava/lang/Object;) public abstract doSomethingAbstractWithDefaultParam(Ljava/lang/Object;)V public static synthetic doSomethingAbstractWithDefaultParam$default(Lorg/example/kotlin/defs/BaseService;Ljava/lang/Object;ILjava/lang/Object;)V public default doSomethingWithDefaultParam(Ljava/lang/Object;)V public static synthetic doSomethingWithDefaultParam$default(Lorg/example/kotlin/defs/BaseService;Ljava/lang/Object;ILjava/lang/Object;)V public static synthetic access$doSomething$jd(Lorg/example/kotlin/defs/BaseService;Ljava/lang/Object;)V public static synthetic access$doSomethingWithDefaultParam$jd(Lorg/example/kotlin/defs/BaseService;Ljava/lang/Object;)V }
Генерация классов тоже изменилась. Здесь для наглядности приведён декомпилированный класс.
-jvm-default=disable
public final class BaseServiceImpl implements BaseService { public void doSomethingAbstract(@NotNull Object arg1) { Intrinsics.checkNotNullParameter(arg1, "arg1"); System.out.println(arg1); } public void doSomethingAbstractWithDefaultParam(@NotNull Object arg1) { Intrinsics.checkNotNullParameter(arg1, "arg1"); System.out.println(arg1); } public void doSomething(@NotNull Object arg1) { BaseService.DefaultImpls.doSomething(this, arg1); } public void doSomethingWithDefaultParam(@NotNull Object arg1) { BaseService.DefaultImpls.doSomethingWithDefaultParam(this, arg1); } }
-jvm-default=enable
public final class BaseServiceImpl implements BaseService { public void doSomethingAbstract(@NotNull Object arg1) { Intrinsics.checkNotNullParameter(arg1, "arg1"); System.out.println(arg1); } public void doSomethingAbstractWithDefaultParam(@NotNull Object arg1) { Intrinsics.checkNotNullParameter(arg1, "arg1"); System.out.println(arg1); } public void doSomething(@NotNull Object arg1) { super.doSomething(arg1); } public void doSomethingWithDefaultParam(@NotNull Object arg1) { super.doSomethingWithDefaultParam(arg1); } }
В нашем проекте при миграции на Kotlin 2.2 именно неожиданное, с точки зрения прокси-провайдера, появление static-метода в интерфейсе и приводило к ошибке при генерации прокси-классов.
Caused by: java.lang.IllegalAccessException: expected a non-static method: ... at java.base/java.lang.invoke.MemberName.makeAccessException(MemberName.java:889) at java.base/java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:3936) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodCommon(MethodHandles.java:4084) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodNoSecurityManager(MethodHandles.java:4077) at java.base/java.lang.invoke.MethodHandles$Lookup.unreflectSpecial(MethodHandles.java:3487) at reactivefeign.methodhandler.DefaultMethodHandler.<init>(DefaultMethodHandler.java:42) ... 61 more
Итак, если при миграции на Kotlin 2.2+ вы с столкнулись с несовместимостями ваших библиотек и новой генерации методов с параметрами по умолчанию или неабстрактными методами интерфейсов в Kotlin, быстрым решением станет установка параметра компилятора ‑jvm‑default=disable.