Как интерфейс может включать метод, который ссылается на конкретный тип реализации интерфейса в его сигнатуре или типе возврата?

Предположим, что я создаю что-то вроде следующего интерфейса:

public interface MyInterface{
  public MyInterface method1();
  public void method2(MyInterface mi);
}

Однако существует предостережение о том, что возвращаемый тип для method1 и параметр method2 соответствуют конкретной реализации, а не только MyInterface. То есть, если у меня есть MyInterfaceImpl, который реализует MyInterface, он должен иметь следующее:

public class MyInterfaceImpl implements MyInterface{
  @Override
  public MyInterfaceImpl method1(){...}

  @Override
  public void method2(MyInterfaceImpl mi){...}
}

Как указано выше, method1 не приведет к ошибкам компиляции, но нет ничего, гарантирующего, что возвращаемый тип совпадает во всех реализациях. Конечно, method2 даже не компилируется, потому что подпись не соответствует интерфейсу.

Одним из возможных решений является использование самореферентных или рекурсивных границ в дженериках:

public interface MyInterface<T extends MyInterface<T>>{
  public T method1();
  public void method2(T mi);
}

public class MyInterfaceImpl implements MyInterface<MyInterfaceImpl>{
  @Override
  public MyInterfaceImpl method1();

  @Override
  public void method2(MyInterfaceImpl mi);
}

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

public class NotMyInterfaceImpl implements MyInterface<MyInterfaceImpl>{
  @Override
  public MyInterfaceImpl method1();

  @Override
  public void method2(MyInterfaceImpl mi);
} 

Это скомпилировалось бы просто отлично, хотя NotMyInterfaceImpl должен реализовывать MyInterface<NotMyInterfaceImpl>. * Это заставляет меня думать, что мне нужно что-то еще.

* Заметьте, что я не думаю, что пытаюсь нарушить LSP; Я в порядке с возвращаемым типом/параметром, являющимся подклассами NotMyInterfaceImpl.

Поэтому я не знаю, как это сделать. Это заставляет меня думать, что я могу сосредоточиться на деталях реализации интерфейса, но для меня это не похоже. Есть ли способ сделать то, что я описал, или это какой-то запах, который я вкладываю в интерфейс, который там не принадлежит?

Ответ 1

Это точная ситуация, с которой сталкивается интерфейс Comparable (его метод compareTo хочет принять аргумент того же типа, что и объект, на который он вызывается). Так что же он делает? Он просто определяется как Comparable<T>. Идея заключается в том, что реализующий класс "должен" реализовать Comparable с самим собой как параметр (позволяющий ему "сравнивать" с самим собой); но это не применяется (так как нет способа сделать это).

Да, как вы отметили, это позволит любому классу реализовать Comparable с параметром любого другого класса: class Foo implements Comparable<Bar>, где Foo и Bar не имеют отношения друг к другу. Однако это не проблема.

Все методы и классы (сортировка, максимум и т.д.), для которых требуются объекты Comparable, имеют следующее ограничение общего типа <T extends Comparable<? super T>>. Это гарантирует, что объекты типа T сопоставимы с самими собой. Таким образом, он полностью безопасен по типу. Таким образом, принудительное исполнение не выполняется в объявлении интерфейса Comparable, но в тех местах, где оно используется.

(Я заметил, что вы используете <T extends MyInterface<T>>, а Comparable использует просто <T>. Хотя <T extends MyInterface<T>> исключает случаи, когда параметр типа не реализует MyInterface, он не исключает случаев, когда параметр типа реализовать MyInterface, но отличается от класса. Итак, какая точка полуиспускания некоторых случаев? Если вы примете Comparable способ ограничить его, где они используются, это безопасно в любом случае, поэтому нет смысла в добавив больше ограничений.)

Ответ 2

Я считаю, что это невозможно. Просто нет способа ссылаться на класс реализации объекта в рамках generics, а также, насколько я знаю, каким-либо образом построить клетку из чистых дженериков, которая может сдерживать класс реализации в соответствии с параметром типа.

Самое полезное, что я могу предложить, это использовать параметр самореференции, а затем всегда приобретать экземпляры реализаций из методов factory, которые выглядят следующим образом:

public <T extends MyInterface<T>> T newInstance();

Легче, чтобы верблюд проходил через глаз иглы, чем для экземпляра NotMyInterfaceImpl, чтобы пройти через этот тип возврата. Так что, хотя нарушители спокойствия могли писать классы, которые не соответствуют вашему генеральному плану, они не могли вернуть их с фабрик. Если NotMyInterfaceImpl extended MyInterfaceImpl; но тогда, в некотором смысле, это также было бы MyInterfaceImpl, поэтому, возможно, это было бы кошерным?

EDIT: немного более полезная версия этой идеи состоит в том, чтобы всегда передавать экземпляры реализаций интерфейса в подходящем ограничительном держателе, например:

class Holder<T extends MyInterface<T>> {
    public final T value;
}

Если кто-то дает вам Holder<Q>, то вы знаете, что Q должна быть привязана к самой версии MyInterface, и это то, что вам нужно.

Ответ 3

То, что вы пытаетесь сделать, не является законным, потому что вы пытаетесь сузить параметр внедренного типа, и это "не имеет смысла". Вы пытаетесь использовать "ковариантные" параметры, и допускаются только ковариантные типы возвращаемых значений (и даже логика и только поддерживаемые с Java 5).

Я имею в виду, если бы можно было использовать ковариантные типы параметров, вы могли бы делать такие вещи, как:

MyInterface instance = new MyInterfaceImpl();

И затем вызывать на "экземпляре" метод с другой реализацией, поддерживаемой интерфейсом, но не поддерживаемый классом MyInterfaceImpl следующим образом:

instance.method2(new MyInterfaceImpl_2());

Java не может преобразовать MyInterfaceImpl_2 в MyInterfaceImpl, поэтому он не позволяет делать это во время компиляции.

Что вы можете сделать, так это расширить параметр, используя параметр "контравариантный" , который будет логическим. Для более подробной информации об этом, проверьте этот anser:

Продемонстрировать ковариацию и контравариантность в Java?

Единственным обходным решением, о котором я могу думать, является решение проблемы во время выполнения, я имею в виду, что-то вроде этого:

public class MyInterfaceImpl implements MyInterface{
  @Override
  public void method2(MyInterface mi){
        realMethod((MyInterfaceImpl) mi);
  }
  public void realMethod(MyInterfaceImpl) {...}
}

Но вы можете получить исключение ClassCast, конечно.

Ответ 4

Точка возврата интерфейса такова, что метод не заботится о фактической реализации возвращаемого объекта. В вашем случае вы действительно хотите, чтобы этот тип был конкретным под-реализацией этого интерфейса.

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

class MyBaseImpl {
    public final void fixedFlow() {
        MyBaseImpl obj = method1();
        obj.method2(this);
    }
    protected abstract MyBaseImpl method1();
    ....
}

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

Надеюсь, это поможет!

Ответ 5

Это то, что вы ищете?

public interface MyInterface {

    static abstract class MyInterfaceImpl implements MyInterface {

        @Override
        public abstract MyInterfaceImpl method1();

        @Override
        public abstract void method2(MyInterfaceImpl mi);

    }    

    MyInterfaceImpl method1();
    void method2(MyInterfaceImpl mi);

}

И вы даже можете реализовать метод 1 или 2 вместо того, чтобы сделать их абстрактными.