Бинарная совместимость Java - RFC на предлагаемом решении ковариантного возвращаемого типа с использованием семантики invokevirtual - PullRequest
3 голосов
/ 17 августа 2010

Я пытаюсь развить API. В рамках этой эволюции мне нужно изменить тип возвращаемого значения метода на подкласс (специализация), чтобы продвинутые клиенты могли получить доступ к новым функциям. Пример (игнорируем уродливое:

public interface Entity {
  boolean a();
}

public interface Intf1 {
  Entity entity();
}

public interface Main {
  Intf1 intf();
}

Теперь я хочу, чтобы ExtendedEntity, Intf2 и Main были такими:

public interface ExtendedEntity extends Entity {
  boolean b();
}

public interface Intf2 extends Intf1 {
  ExtendedEntity entity();
}

public interface Main {
  Intf2 intf();
}

Однако, поскольку тип возвращаемого значения метода является частью его подписи, клиенты, уже скомпилированные с предыдущей версией кода, показывают ошибки компоновки (метод не найден в iirc).

Я хотел бы добавить метод Main с другим типом возвращаемого значения. Два метода (один, который возвращает супертип, а другой, который возвращает подтип) должны быть сопоставлены с одним и тем же методом реализации (который возвращает подтип). Примечание. Насколько я понимаю, это разрешено JVM, но не спецификацией Java.

Мое решение, которое , похоже, работает, злоупотребляет (у меня нет другого слова для этого) системой классов Java для добавления необходимого интерфейса.

public interface Main_Backward_Compatible {
  Intf1 intf();
}

public interface Main extends Main_Backward_Compatible{
  Intf2 intf();
}

Теперь у старых клиентов будет правильный метод, возвращаемый в поиск invokevirtual (поскольку метод с правильным типом возврата существует в иерархии типов), и реализация, которая фактически будет работать, будет той, которая возвращает подтип Intf2.

Кажется, работает . Во всех тестах, которые я мог придумать (за исключением рефлексии - но мне все равно, что это за бит), он сделал сработал.
Это всегда будет работать? Верны ли мои рассуждения (о invokevirtual)?

И еще один связанный с этим вопрос - существуют ли инструменты для проверки «реальной» двоичной совместимости? Единственные, что я нашел, рассматривают каждый метод отдельно, но не учитывают иерархию типов.

Спасибо
Ран.

Редактировать - Инструменты, которые я пробовал и нашел "не очень хорошими" (без учета иерархии типов):

  1. Clirr 0.6.
  2. Плагин IntelliJ "APIComparator".

Edit2 - Конечно, моим клиентам запрещено создавать классы реализации для моих интерфейсов (думаю, сервисов). Однако, если вы хотите, чтобы пример был завершен, подумайте об абстрактном классе (для Main) вместо интерфейса.

Ответы [ 3 ]

1 голос
/ 17 августа 2010

Это было достаточно долго, и я признаю, что не все тщательно читал, но, похоже, вы действительно хотите использовать дженерики здесь. Если вы введете Intf1, я думаю, что вы сможете поддерживать бинарную совместимость, вводя специализации:

public interface Intf1<T extends Entity> {
  T entity(); //erasure is still Entity so binary compatibility
}

public interface Intf2 extends Intf1<ExtendedEntity> { //if even needed
}

public interface Main {
  Intf1<ExtendedEntity> intf(); //erasure is still Intf1, the raw type
}

Редактирование # 1: При попытке сохранить двоичную совместимость возникают некоторые предостережения. См. Обобщающее руководство главы 6 и 10 для получения дополнительной информации.

Редактировать # 2:

Вы можете расширить эту концепцию, набрав также Main:

public interface Main<T, I extends Intf1<T>> {
    I intf(); //still has the same erasure as it used to, so binary compatible
}

Тогда старые клиенты смогут использовать необработанный тип Main, как раньше, без необходимости перекомпиляции, а новые клиенты будут печатать свои ссылки на Main:

Main<ExtendedEntity, Intf2> myMain = Factory.getMeAMain();
Intf2 intf = myMain.intf();
1 голос
/ 26 мая 2011

В итоге мы не нуждались в решении, но до этого доказали, что оно работает.

0 голосов
/ 17 августа 2010

Было бы проще вообще не менять существующие интерфейсы.Любой, кто использует ваш новый интерфейс, будет писать новый код в любом случае.

Реализации существующей подписи Main.intf () могут возвращать экземпляр Intf2.

При желании вы можете предоставить новый метод доступа, не требующий приведения:

public interface Main2 extends Main {
  Intf2 intf2();
}
...