Почему интерфейсы должны быть объявлены на Java?

Иногда у нас есть несколько классов, которые имеют некоторые методы с одинаковой сигнатурой, но не соответствуют объявленному интерфейсу Java. Например, и JTextField, и JButton (среди нескольких других в javax.swing.*) имеют метод

public void addActionListener(ActionListener l)

Теперь предположим, что я хочу что-то сделать с объектами, у которых есть этот метод; тогда я хотел бы иметь интерфейс (или, возможно, определить его сам), например

  public interface CanAddActionListener {
      public void addActionListener(ActionListener l);
  }

чтобы я мог написать:

  public void myMethod(CanAddActionListener aaa, ActionListener li) {
         aaa.addActionListener(li);
         ....

Но, к сожалению, я не могу:

     JButton button;
     ActionListener li;
     ...
     this.myMethod((CanAddActionListener)button,li);

Это действие было бы незаконным. Компилятор знает, что JButton не CanAddActionListener, потому что класс не объявлен для реализации этого интерфейса ... однако он "фактически" реализует его .

Иногда это доставляет неудобства - и сама Java изменила несколько основных классов, чтобы реализовать новый интерфейс, состоящий из старых методов (например, String implements CharSequence).

У меня вопрос: почему это так? Я понимаю полезность объявления того, что класс реализует интерфейс. Но в любом случае, глядя на мой пример, почему компилятор не может сделать вывод, что класс JButton "удовлетворяет" объявлению интерфейса (заглядывая в него), и принять приведение? Это вопрос эффективности компилятора или есть более фундаментальные проблемы?

Краткое изложение ответов: это случай, когда Java могла сделать скидку на некоторую «структурную типизацию» (своего рода утиную типизацию, но проверяемую во время компиляции). Это не так. Помимо некоторых (непонятных для меня) трудностей с производительностью и реализацией, здесь есть гораздо более фундаментальная концепция: в Java объявление интерфейса (и вообще всего) не должно быть просто структурным (чтобы иметь методы с этими сигнатурами), но семантический: предполагается, что методы реализуют какое-то конкретное поведение / намерение. Таким образом, класс, который структурно удовлетворяет некоторому интерфейсу (т. Е. Имеет методы с необходимыми сигнатурами), не обязательно удовлетворяет ему семантически (крайний пример: вспомните маркер интерфейсы », у которых даже нет методов!). Следовательно, Java может утверждать, что класс реализует интерфейс, потому что (и только потому, что) он был явно объявлен. У других языков (Go, Scala) другая философия.


person leonbloy    schedule 17.04.2011    source источник
comment
Возможно, вас заинтересует язык программирования Go. См. golang.org. Он имеет семантику интерфейса в точности так, как вы хотите в своем вопросе.   -  person ChrisH    schedule 17.04.2011


Ответы (6)


Почему компилятор не может сделать вывод, что класс JButton "удовлетворяет" объявлению интерфейса (заглядывая в него), и принять приведение? Это вопрос эффективности компилятора или есть более фундаментальные проблемы?

Это более фундаментальный вопрос.

Задача интерфейса - указать, что существует общий API / набор поведений, поддерживаемых рядом классов. Итак, когда класс объявлен как implements SomeInterface, любые методы в классе, сигнатуры которых соответствуют сигнатурам методов в интерфейсе, предполагается как методы, обеспечивающие такое поведение.

Напротив, если язык просто сопоставляет методы на основе сигнатур ... независимо от интерфейсов ... тогда мы можем получить ложные совпадения, когда два метода с одинаковой сигнатурой фактически означают / делают что-то семантически несвязанное.

(Последний подход называется «утиная печать» ... и Java его не поддерживает.)


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

"Системы типов, такие как Java, в которых имена [типов] имеют значение, а подтипы явно объявлены, называются номинальными. Системы типов, подобные большинству систем в этой книге, в которых имена не являются важными, а подтипы - определенные непосредственно в структуре типов, называются структурными. "

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

Ссылка:

  • «Типы и языки программирования» - Бенджамин С. Пирс, MIT Press, 2002, ISBN 0-26216209-1.
person Stephen C    schedule 17.04.2011
comment
+1 Разве тогда не было бы более уместным (больше консенсуса) назвать мое желаемое поведение структурной типизацией, а не утиной типизацией? - person leonbloy; 18.04.2011
comment
@leonbloy - может быть. Но вы, скорее всего, услышите такие вещи, как утиная печать. Если вы назовете это структурной типизацией, вы, вероятно, услышите, что люди говорят, что речь идет о поведении, а не о (репрезентативной) структуре. Проблема в том, что терминология системы типов очень эластична. - person Stephen C; 18.04.2011

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

Так в чем же конкретно выбор дизайна? Все сводится к семантике методов. Задумайтесь: являются ли следующие методы семантически одинаковыми?

  • draw (String graphicalShapeName)
  • draw (String handgunName)
  • ничья (строка playsCardName)

Все три метода имеют подпись draw(String). Человек может сделать вывод о том, что их семантика отличается от имен параметров или прочитав некоторую документацию. Может ли машина сказать, что они разные?

Выбор дизайна Java состоит в том, чтобы потребовать, чтобы разработчик класса явно указывал, что метод соответствует семантике предопределенного интерфейса:

interface GraphicalDisplay {
    ...
    void draw(String graphicalShapeName);
    ...
}

class JavascriptCanvas implements GraphicalDisplay {
    ...
    public void draw(String shape);
    ...
}

Нет сомнений в том, что метод draw в JavascriptCanvas предназначен для соответствия методу draw для графического отображения. Если кто-то попытался пройти мимо предмета, который собирался вытащить пистолет, автомат может обнаружить ошибку.

Выбор дизайна Go более либерален и позволяет определять интерфейсы постфактум. Конкретному классу не нужно объявлять, какие интерфейсы он реализует. Напротив, разработчик нового компонента карточной игры может объявить, что объект, который предоставляет игральные карты, должен иметь метод, соответствующий подписи draw(String). Это имеет то преимущество, что любой существующий класс с этим методом может использоваться без изменения его исходного кода, но недостаток в том, что класс может вытащить пистолет вместо игральной карты.

Выбор языка программирования «утка» - вообще отказаться от формальных интерфейсов и просто сопоставить сигнатуры методов. Любая концепция интерфейса (или «протокола») является чисто идиоматической, без прямой языковой поддержки.

Это всего лишь три из множества возможных вариантов дизайна. Эти три слова можно кратко резюмировать следующим образом:

Java: программист должен явно заявить о своем намерении, и машина его проверит. Предполагается, что программист может допустить семантическую ошибку (графика / пистолет / карта).

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

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

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

Признается, что пример draw(String) намеренно преувеличен, чтобы подчеркнуть суть дела. Реальные примеры будут включать более богатые типы, которые дадут больше ключей для устранения неоднозначности методов.

person WReach    schedule 17.04.2011

Скорее всего это особенность производительности.

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

Позже, во время выполнения, когда объект имеет приведение Class к типу интерфейса, все, что нужно сделать среде выполнения, - это проверить метаданные класса, чтобы узнать, совместим ли класс, который он приводит, также (через интерфейс или иерархия наследования).

Это достаточно дешевая проверка, поскольку компилятор проделал большую часть работы.

Имейте в виду, это не авторитетно. Класс может СКАЗАТЬ, что он соответствует интерфейсу, но это не означает, что фактический метод, отправленный для выполнения, действительно будет работать. Соответствующий класс вполне может быть устаревшим, а метод может просто не существовать.

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

Теперь, если среда выполнения должна убедиться, что объект соответствует интерфейсу, выполнив всю работу самостоятельно, вы увидите, насколько это может быть дороже, особенно с большим интерфейсом. Например, набор JDBC ResultSet содержит более 140 методов и тому подобное.

Утиная печать - это эффективное динамическое сопоставление интерфейсов. Проверьте, какие методы вызываются для объекта, и сопоставьте его во время выполнения.

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

person Will Hartung    schedule 17.04.2011
comment
Хорошее объяснение. Но что касается проблемы с производительностью и динамической утиной типизации, я думаю, что мое желаемое поведение касается только времени компиляции. То, что JButton можно преобразовать в CanAddActionListener, можно решить во время компиляции. - person leonbloy; 18.04.2011
comment
Верно, но не обязательно. Я не знаю специфики различных реализаций DT. Во время компиляции может быть получен большой объем информации. Но если у вас есть список ‹Object›, и я могу делать произвольные вызовы по нему, это стирает много информации, позволяя мне помещать что угодно в этот список. Вся эта отправка и обработка должны выполняться динамически во время выполнения. Так что, вероятно, всегда будет какое-то влияние при первоначальной отправке. - person Will Hartung; 18.04.2011

Утиная типизация может быть опасной по причинам, о которых говорил Стивен С., но это не обязательно зло, которое нарушает всякую статическую типизацию. Статическая и более безопасная версия утиной типизации лежит в основе системы типов Go, а в Scala доступна версия, которая называется «структурной типизацией». Эти версии по-прежнему выполняют проверку во время компиляции, чтобы убедиться, что объект подчиняется требованиям, но имеют потенциальные проблемы, потому что они нарушают парадигму проектирования, согласно которой реализация интерфейса всегда является преднамеренным решением.

См. http://markthomas.info/blog/?p=66 и http://programming-scala.labs.oreilly.com/ch12.html и http://beust.com/weblog/2008/02/11/structural-typing-vs-duck-typing/ для обсуждения функции Scala.

person Philip JF    schedule 17.04.2011

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

Чтобы понять, почему они, возможно, не выбрали использование такого интерфейса, как «CanAddActionListener», вы должны взглянуть на преимущества НЕ использования интерфейса и вместо этого предпочтение абстрактных (и, в конечном счете, конкретных) классов.

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

Итак, я предполагаю, что команда разработчиков Java полностью осознала, что многие из их классов AbstractJ * имеют одни и те же имена методов, было бы невыгодно иметь у них общий интерфейс, поскольку это сделало бы их API жесткими и негибкими.

Подводя итог (спасибо этому сайту):

  • Абстрактные классы можно легко расширить, добавив новые (не абстрактные) методы.
  • Интерфейс не может быть изменен без разрыва его контракта с классами, которые его реализуют. После поставки интерфейса набор его членов постоянно фиксируется. API, основанный на интерфейсах, может быть расширен только путем добавления новых интерфейсов.

Конечно, это не означает, что вы можете сделать что-то подобное в своем собственном коде (расширить AbstractJButton и реализовать интерфейс CanAddActionListener), но помните о подводных камнях при этом.

person JasCav    schedule 17.04.2011

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

В качестве простого примера, если кто-то разрабатывает фреймворк с нуля, можно начать с интерфейса Enumerable<T> (который можно использовать сколь угодно часто для создания перечислителя, который будет выводить последовательность T, но разные запросы могут давать разные последовательности. ), но затем получить от него интерфейс ImmutableEnumerable<T>, который будет вести себя, как указано выше, но гарантирует, что каждый запрос будет возвращать ту же последовательность. Изменяемый тип коллекции будет поддерживать все элементы, необходимые для ImmutableEnumerable<T>, но поскольку запросы на перечисление, полученные после мутации, будут сообщать о последовательности, отличной от запросов, сделанных ранее, он не будет соблюдать контракт ImmutableEnumerable.

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

person supercat    schedule 13.06.2014