Когда 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, как и в старых версиях по умолчанию.

Чтобы увидеть, как изменился байт-код, давайте сделаем следующее:

  1. Создадим интерфейс с такими методами

  2. Реализуем их в класс

  3. Вызовем эти функции

  4. Скомпилируем всё с разными параметрами -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.