Почему интерфейсы должны быть объявлены в Java? - PullRequest
8 голосов
/ 17 апреля 2011

Иногда у нас есть несколько классов, у которых есть несколько методов с одинаковой сигнатурой, но которые не соответствуют объявленному интерфейсу 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 не a CanAddActionListener, потому что класс не объявлен для реализации этого интерфейса ... однако он "фактически" реализует его .

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

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

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

Ответы [ 6 ]

8 голосов
/ 17 апреля 2011

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

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

  • draw (String graphicalShapeName)
  • ничья (строковое имя пистолета)
  • Draw (String PlayingCardName)

Все три метода имеют подпись 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: программист должен явно объявить свое намерение, и машина проверит его. Предполагается, что программист может совершить семантическую ошибку (графика / пистолет / карта).

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

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

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

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

5 голосов
/ 17 апреля 2011

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

Это более фундаментальная проблема.

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

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

(Название для последнего подхода - "типизирование утки" ... и Java его не поддерживает.)


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

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

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

Справка:

  • «Типы и языки программирования» - Бенджамин С. Пирс, MIT Press, 2002, ISBN 0-26216209-1.
2 голосов
/ 17 апреля 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.

2 голосов
/ 17 апреля 2011

Скорее всего, это функция производительности.

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

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

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

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

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

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

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

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

2 голосов
/ 17 апреля 2011

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

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

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

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

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

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

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

0 голосов
/ 13 июня 2014

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

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

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

...