Почему стандартные и статические методы были добавлены к интерфейсам в Java 8, когда у нас уже были абстрактные классы?

99

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

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

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

Короче говоря:

До Java 8:

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

После Java 8:

  • Практически нет разницы между интерфейсом и абстрактным классом (кроме множественного наследования). На самом деле вы можете эмулировать обычный класс с интерфейсом.
  • При программировании реализаций программисты могут забыть переопределить методы по умолчанию.
  • Существует ошибка компиляции, если класс пытается реализовать два или более интерфейсов, имеющих метод по умолчанию с одной и той же сигнатурой.
  • Добавляя метод по умолчанию к интерфейсу, каждый реализующий класс автоматически наследует это поведение. Некоторые из этих классов, возможно, не были разработаны с учетом этой новой функциональности, и это может вызвать проблемы. Например, если кто-то добавляет новый метод default void foo()по умолчанию к интерфейсу Ix, класс, Cxреализующий Ixи имеющий закрытый fooметод с той же сигнатурой, не компилируется.

Каковы основные причины таких серьезных изменений и какие новые преимущества (если таковые имеются) они добавляют?

Мистер смит
источник
30
Дополнительный вопрос: почему вместо этого они не вводили множественное наследование для классов?
2
статические методы не принадлежат интерфейсам. Они относятся к служебным классам. Нет, они относятся к @Deprecatedкатегории! статические методы - одна из самых злоупотребляемых конструкций в Java из-за невежества и лени. Множество статических методов обычно означает некомпетентный программист, увеличивающий связь на несколько порядков и являющийся кошмаром для юнит-тестирования и рефакторинга, когда вы понимаете, почему они плохая идея!
11
@JarrodRoberson Можете ли вы дать еще несколько советов (ссылки были бы хороши) о «кошмар для юнит-тестирования и рефакторинга, когда вы понимаете, почему они плохая идея!»? Я никогда не думал об этом, и я хотел бы узнать больше об этом.
водянистая
11
@Chris Многократное наследование состояния вызывает массу проблем, особенно с распределением памяти ( классическая проблема с алмазом ). Однако множественное наследование поведения зависит только от реализации, соответствующей контракту, уже установленному интерфейсом (интерфейс может вызывать другие методы, которые он объявляет и требует). Тонкое, но очень интересное различие.
ssube
1
Вы хотите добавить метод к существующему интерфейсу, до java8 он был громоздким или почти невозможным, теперь вы можете просто добавить его в качестве метода по умолчанию.
VdeX

Ответы:

59

Хороший мотивирующий пример для методов по умолчанию находится в стандартной библиотеке Java, где у вас теперь есть

list.sort(ordering);

вместо

Collections.sort(list, ordering);

Я не думаю, что они могли бы сделать это иначе без более чем одной идентичной реализации List.sort.

Сору
источник
19
C # преодолевает эту проблему с помощью методов расширения.
Роберт Харви
5
и он позволяет связанному списку использовать O (1) дополнительное пространство и O (n log n) время слияния, потому что связанные списки могут быть объединены на месте, в Java 7 он создает дамп во внешний массив, а затем сортирует это
трещотка урод
5
Я нашел эту статью, где Гетц объясняет проблему. Так что пока я буду отмечать этот ответ как решение.
Мистер Смит,
1
@RobertHarvey: создайте список List <Byte> из двухсот миллионов элементов, используйте IEnumerable<Byte>.Appendих, чтобы присоединиться к ним, а затем вызовите Countи расскажите, как методы расширения решают проблему. Если бы CountIsKnownи Countбыли членами IEnumerable<T>, возвращение Appendмогло бы рекламировать, CountIsKnownесли составляющие коллекции сделали, но без таких методов это невозможно.
Суперкат
6
@supercat: Понятия не имею, о чем ты говоришь.
Роберт Харви
48

Правильный ответ на самом деле находится в документации Java , которая гласит:

[d] методы efault позволяют вам добавлять новые функциональные возможности к интерфейсам ваших библиотек и обеспечивать двоичную совместимость с кодом, написанным для более старых версий этих интерфейсов.

Это было давним источником боли в Java, потому что интерфейсы, как правило, невозможно развивать после того, как они были обнародованы. (Содержание в документации относится к статье, на которую вы ссылались в комментарии: Эволюция интерфейса с помощью методов виртуального расширения .) Кроме того, быстрое внедрение новых функций (например, лямбда-выражений и новых потоковых API) может быть сделано только путем расширения существующие интерфейсы коллекций и предоставление реализаций по умолчанию. Нарушение бинарной совместимости или внедрение новых API означало бы, что пройдет несколько лет, прежде чем наиболее важные функции Java 8 станут широко использоваться.

Причина, по которой статические методы допускаются в интерфейсах, снова раскрывается в документации: [t] он упрощает организацию вспомогательных методов в ваших библиотеках; вы можете хранить статические методы, специфичные для интерфейса, в том же интерфейсе, а не в отдельном классе. Другими словами, статические служебные классы, такие как, java.util.Collectionsтеперь (в конечном итоге) можно считать анти-паттерном, вообще (конечно, не всегда ). Я предполагаю, что добавление поддержки этого поведения было тривиальным, как только были реализованы методы виртуального расширения, иначе это, вероятно, не было бы сделано.

На аналогичном замечании пример того, как эти новые функции могут быть полезны, - рассмотреть один класс, который недавно меня раздражал java.util.UUID. На самом деле он не обеспечивает поддержку типов UUID 1, 2 или 5, и его нельзя легко изменить для этого. Он также застрял с предопределенным генератором случайных чисел, который нельзя переопределить. Реализация кода для неподдерживаемых типов UUID требует либо прямой зависимости от стороннего API, а не интерфейса, либо поддержки кода преобразования и затрат на дополнительную сборку мусора. С помощью статических методов UUIDможно было бы вместо этого определить интерфейс, что позволило бы реализовать сторонние реализации отсутствующих элементов. (Если бы UUIDизначально он был определен как интерфейс, у нас, вероятно, был бы какой-то неуклюжийUuidUtil класс со статическими методами, что тоже было бы ужасно.) Многие из основных API-интерфейсов Java деградируют из-за того, что не основываются на интерфейсах, но в Java 8 количество оправданий для такого плохого поведения, к счастью, уменьшилось.

Неправильно говорить, что здесь [t] практически нет разницы между интерфейсом и абстрактным классом , потому что абстрактные классы могут иметь состояние (то есть объявлять поля), а интерфейсы - нет. Поэтому он не эквивалентен множественному наследованию или даже наследованию в смешанном стиле. Правильные миксины (такие как черты Groovy 2.3 ) имеют доступ к состоянию. (Groovy также поддерживает статические методы расширения.)

По моему мнению, не очень хорошая идея следовать примеру Довала . Интерфейс должен определять контракт, но не должен обеспечивать выполнение контракта. (В любом случае, не в Java.) Правильная проверка реализации является обязанностью набора тестов или другого инструмента. Определение контрактов может быть сделано с помощью аннотаций, и OVal является хорошим примером, но я не знаю, поддерживает ли он ограничения, определенные на интерфейсах. Такая система возможна, даже если она не существует в настоящее время. (Стратегии включают настройку во время компиляции javacчерез процессор аннотацийAPI и генерация байт-кода во время выполнения.) В идеале, контракты должны выполняться во время компиляции, а в худшем случае - с использованием набора тестов, но я понимаю, что принудительное выполнение во время выполнения не одобряется. Еще одним интересным инструментом, который может помочь при программировании контрактов в Java, является Checker Framework .

ngreen
источник
1
Для дальнейшего продолжения моего последнего параграфа (т.е. не применяйте контракты в интерфейсах ), стоит указать, что defaultметоды не могут переопределять equals, hashCodeи toString. Очень информативный анализ затрат и выгод того, почему это запрещено, можно найти здесь: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
ngreen
Очень жаль, что в Java есть только один виртуальный equalsи один hashCodeметод, поскольку существует два разных типа равенства, которые могут понадобиться для тестирования коллекций, и элементы, которые будут реализовывать несколько интерфейсов, могут застрять с противоречивыми договорными требованиями. Возможность использовать списки, которые не будут меняться в качестве hashMapключей, полезна, но бывают также случаи, когда было бы полезно хранить коллекции, в hashMapкоторых совпадают вещи, основанные на эквивалентности, а не на текущем состоянии [эквивалентность подразумевает соответствие состояния и неизменности ] ,
суперкат
Ну, у Java есть обходной путь с интерфейсами Comparator и Comparable. Но это вроде уродливо, я думаю.
ngreen
Эти интерфейсы поддерживаются только для определенных типов коллекций, и они представляют свою собственную проблему: компаратор может сам потенциально инкапсулировать состояние (например, специализированный компаратор строк может игнорировать настраиваемое количество символов в начале каждой строки, и в этом случае число игнорируемые символы будут частью состояния компаратора), что, в свою очередь, станет частью состояния любой коллекции, которая была им отсортирована, но не существует определенного механизма для запроса двух компараторов, эквивалентны ли они.
суперкат
О да, я чувствую боль компараторов. Я работаю над древовидной структурой, которая должна быть простой, но не потому, что так сложно правильно подобрать компаратор. Я, вероятно, собираюсь написать собственный класс дерева, чтобы решить проблему.
ngreen
44

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

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


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

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Нет никакого способа гарантировать, что, когда кто-то реализует интерфейс, popсгенерирует исключение, если стек пуст. Мы могли бы применить это правило, разделив его popна два метода: public finalметод, обеспечивающий выполнение контракта, и protected abstractметод, который выполняет фактическое выталкивание.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

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

Я не уверен, позволяет ли подход Java 8 к добавлению методов к интерфейсам добавлять конечные методы или защищенные абстрактные методы, но я знаю, что язык D позволяет это и обеспечивает встроенную поддержку для Design by Contract . В этом методе нет опасности, поскольку он popявляется окончательным, поэтому ни один реализующий класс не может его переопределить.

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

Кроме того,

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

Это не совсем так, поскольку вы не можете объявить поля в интерфейсе. Любой метод, который вы пишете в интерфейсе, не может полагаться на какие-либо детали реализации.


В качестве примера в пользу статических методов в интерфейсах рассмотрим вспомогательные классы, такие как Collections в Java API. Этот класс существует только потому, что эти статические методы не могут быть объявлены в их соответствующих интерфейсах. Collections.unmodifiableListс таким же успехом мог быть объявлен в Listинтерфейсе, и его было бы легче найти.

Doval
источник
4
Контр-аргумент: поскольку статические методы, если они написаны правильно, являются автономными, они имеют больше смысла в отдельном статическом классе, где их можно собирать и классифицировать по имени класса, и меньше смысла в интерфейсе, где они по сути удобны. это вызывает злоупотребления, такие как удержание статического состояния в объекте или побочные эффекты, что делает статические методы непроверяемыми.
Роберт Харви
3
@RobertHarvey Что мешает вам делать такие же глупости, если ваш статический метод находится в классе? Кроме того, что метод в интерфейсе может вообще не требовать никакого состояния. Возможно, вы просто пытаетесь навязать контракт. Предположим, у вас есть Stackинтерфейс, и вы хотите, чтобы при popвызове с пустым стеком создавалось исключение. Учитывая абстрактные методы boolean isEmpty()и protected T pop_impl(), вы могли бы реализовать final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Это обеспечивает соблюдение контракта на ВСЕХ разработчиков.
Доваль
Чего ждать? Методы Push и Pop в стеке не будут static.
Роберт Харви
@RobertHarvey Я был бы более понятен, если бы не ограничение на количество символов в комментариях, но я делал аргументы в пользу реализации по умолчанию в интерфейсе, а не статических методов.
Доваль
8
Я думаю, что методы интерфейса по умолчанию - это скорее хак, который был введен, чтобы иметь возможность расширять стандартную библиотеку без необходимости адаптировать существующий код на ее основе.
Джорджио
2

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

Эта идея, кажется, связана с тем, как вы можете использовать методы расширения в C # для добавления реализованной функциональности к интерфейсам.

rae1
источник
1
Методы расширения не добавляют функциональности интерфейсам. Методы расширения являются просто синтаксическим сахаром для вызова статических методов в классе с использованием удобной list.sort(ordering);формы.
Роберт Харви
Если вы посмотрите на IEnumerableинтерфейс в C #, вы увидите, как реализация методов расширения для этого интерфейса (как это LINQ to Objectsделает) добавляет функциональность для каждого реализуемого класса IEnumerable. Вот что я имел в виду под добавлением функциональности.
rae1
2
Это отличная вещь о методах расширения; они создают иллюзию того, что вы используете функциональность для класса или интерфейса. Только не путайте это с добавлением реальных методов в класс; Методы класса имеют доступ к закрытым членам объекта, а методы расширения - нет (поскольку они на самом деле просто еще один способ вызова статических методов).
Роберт Харви
2
Точно, и именно поэтому я вижу некоторое отношение к наличию статических или стандартных методов в интерфейсе в Java; реализация основана на том, что доступно интерфейсу, а не самому классу.
rae1
1

Две основные цели, которые я вижу в defaultметодах (некоторые варианты использования служат обеим целям):

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

Если бы речь шла только о второй цели, вы бы не увидели это в совершенно новом интерфейсе Predicate. Все @FunctionalInterfaceаннотированные интерфейсы должны иметь только один абстрактный метод, чтобы лямбда-код мог его реализовать. Добавлены defaultметоды , такие как and, or, negateтолько полезность, и вы не должны переопределить их. Однако иногда статические методы будут лучше .

Что касается расширения существующих интерфейсов - даже там, некоторые новые методы - просто синтаксический сахар. Методы , Collectionкак stream, forEach, removeIf- в основном, это просто утилита вам не нужно переопределить. И тогда есть методы, как spliterator. Реализация по умолчанию является неоптимальной, но, по крайней мере, код компилируется. Прибегайте к этому, только если ваш интерфейс уже опубликован и широко используется.


Что касается staticметодов, я полагаю, что другие достаточно хорошо это понимают: он позволяет интерфейсу быть своим собственным служебным классом. Может быть, мы могли бы избавиться Collectionsв будущем Java? Set.empty()будет качаться

Vlasec
источник