Статьи

Все, что вам нужно знать о методах по умолчанию

Итак, стандартные методы … вчерашние новости, верно? Да, но после года использования накопилось много фактов, и я хотел собрать их в одном месте для тех разработчиков, которые только начинают их использовать. И, может быть, даже опытные могут найти детали или две, о которых они еще не знали.

Я расширю этот пост в будущем, если появится новое дерьмо . Поэтому я прошу своих читателей (да, вы оба!) Предоставить мне каждый небольшой факт о методах по умолчанию, который вы не можете найти здесь. Если у вас что-то есть, пишите в Твиттере , пишите или оставляйте комментарии.

обзор

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

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

Методы по умолчанию

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

Синтаксис

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

В следующем примере показана модифицированная версия Comparator.thenComparing (Comparator) ( ссылка ) из JDK 8:

Метод по умолчанию в компараторе

1
2
3
4
5
6
default Comparator<T> thenComparing(Comparator<? super T> other) {
    return (o1, o2) -> {
        int res = this.compare(o1, o2);
        return (res != 0) ? res : other.compare(o1, o2);
    };
}

Это выглядит как «обычное» объявление метода, за исключением ключевого слова default . Это необходимо для добавления такого метода в интерфейс без ошибки компиляции и намеков на стратегию разрешения вызовов методов .

Каждый класс, который реализует Comparator , теперь будет содержать открытый метод thenComparing(Comparator) без необходимости реализовывать его самому — так сказать, бесплатно.

Явные вызовы методов по умолчанию

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

Явный вызов реализации по умолчанию

01
02
03
04
05
06
07
08
09
10
class StringComparator implements Comparator<String> {
 
    // ...
 
    @Override
    public Comparator<String> thenComparing(Comparator<? super String> other) {
        log("Call to 'thenComparing'.");
        return Comparator.super.thenComparing(other);
    }
}

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

Стратегия разрешения

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

Правило № 1 :
Классы побеждают интерфейсы. Если класс в цепочке суперклассов имеет объявление для метода (конкретного или абстрактного), все готово, и значения по умолчанию не имеют значения.
Правило № 2 :
Более специфические интерфейсы выигрывают у менее специфичных (где специфичность означает «подтип»). Значение по умолчанию из List превосходит значение по умолчанию из Collection независимо от того, где, как и сколько раз List и Collection входят в граф наследования.
Правило № 3 :
Там нет правила № 3. Если в соответствии с вышеуказанными правилами нет уникального победителя, конкретные классы должны устранить неоднозначность вручную.

Брайан Гетц — 3 марта 2013 г. (формирование шахты)

Прежде всего, это проясняет, почему эти методы называются методами по умолчанию и почему их нужно запускать с ключевым словом default :

Такая реализация является резервной копией на случай, если класс и ни один из его суперклассов даже не рассматривают метод, т.е. не предоставляют реализацию и не объявляют его абстрактным (см. Правило № 1 ). Эквивалентно, метод по умолчанию интерфейса X используется только тогда, когда класс также не реализует интерфейс Y который расширяет X и объявляет тот же метод (либо по умолчанию, либо абстрактный; см. Правило № 2 ).

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

Стратегия разрешения подразумевает несколько интересных деталей …

Решение конфликта

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

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

Что произойдет, если A , X и Y не скомпилированы вместе, и JVM наткнется на эту ситуацию? Интересный вопрос, на который ответ кажется несколько неясным . Похоже, что JVM сгенерирует IncompatibleClassChangeError.

Методы переобобщения

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

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

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

Переопределение методов для «объекта»

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

Ну, во-первых, это было бы бесполезно. Поскольку каждый класс наследует от Object , правило № 1 ясно подразумевает, что эти методы никогда не будут вызываться.

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

В корне методы из Object — такие как toString , equals и hashCode — все о состоянии объекта. Но интерфейсы не имеют состояния; классы имеют гос. Эти методы принадлежат коду, которому принадлежит состояние объекта — класс.

Модификаторы

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

  • видимость зафиксирована для публики (как и для других методов интерфейса)
  • ключевое слово synchronized запрещено (как в абстрактных методах)
  • ключевое слово final запрещено (как в абстрактных методах)

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

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

Немного контекста

Теперь, когда мы знаем все о том, как использовать методы по умолчанию, давайте перенесем это знание в контекст.

Интерфейс Эволюция

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

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

Брайан Гетц — сентябрь 2013

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

Но с введением лямбда-выражений это стало невыносимо. Представьте себе коллективную боль от постоянного написания Stream.of(myList).forEach(...) потому что forEach не может быть добавлен в List .

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

В тех случаях, когда группа считала это возможным без ущерба для удобства использования этого основного варианта использования, они также позволяли использовать методы по умолчанию для создания признаков — или, скорее, чего-то близкого к ним. Тем не менее, на них часто нападали за то, что они не прошли «весь путь» до миксинов и черт, на что часто повторяли ответ: «Да, потому что это не было нашей целью».

Ousting Utility Classes

JDK и особенно распространенные вспомогательные библиотеки, такие как Guava и Apache Commons , полны служебных классов. Их именем обычно является множественная форма интерфейса, для которого они предоставляют свои методы, например, Коллекции или Наборы . Основная причина их существования заключается в том, что эти служебные методы не могут быть добавлены в исходный интерфейс после его выпуска. С методами по умолчанию это становится возможным.

Все те статические методы, которые принимают экземпляр интерфейса в качестве аргумента, теперь могут быть преобразованы в метод по умолчанию в интерфейсе. В качестве примера рассмотрим статический Collections.sort(List) ( ссылка ), который в Java 8 просто делегирует новый экземплярный метод по умолчанию List.sort(Comparator) ( ссылка ). Другой пример приведен в моем посте о том, как использовать методы по умолчанию для улучшения шаблона декоратора . Другие служебные методы, которые не принимают аргументов (обычно это компоновщики), теперь могут стать статическими методами по умолчанию в интерфейсе.

Хотя удаление всех связанных с интерфейсом служебных классов в базе кода возможно, это не рекомендуется. Удобство использования и целостность интерфейса должны оставаться главным приоритетом — не вкладывать туда все мыслимые возможности. Я предполагаю, что имеет смысл перенести самые общие из этих методов в интерфейс, в то время как в одном (или нескольких?) Служебных классах могут остаться более непонятные операции. (Или удалите их полностью , если вы в этом.)

классификация

В своем аргументе в пользу новых тегов Javadoc Брайан Гетц слабо классифицирует методы по умолчанию, которые были введены в JDK до сих пор (формируя мой):

1. Необязательные методы :
Это когда реализация по умолчанию едва соответствует, как, например, следующее из Iterator:

1
2
3
default void remove() {
    throw new UnsupportedOperationException("remove");
}

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

2. Методы с разумными значениями по умолчанию, но которые могут быть переопределены реализациями, которые достаточно заботятся :
Например, снова из Iterator:

1
2
3
4
default void forEach(Consumer<? super E> consumer) {
    while (hasNext())
        consumer.accept(next());
}

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

3. Методы, при которых маловероятно, что кто-либо когда-либо их переопределит
Например, этот метод от Predicate:

1
2
3
4
default Predicate<T> and(Predicate<? super T> p) {
    Objects.requireNonNull(p);
    return (T t) -> test(t) && p.test(t);
}

Брайан Гетц — 31 января 2013

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

Документация

Обратите внимание, что методы по умолчанию были основной причиной введения новых (неофициальных) тегов Javadoc @apiNote , @implSpec и @implNote . JDK часто использует их, поэтому важно понимать их значение. Хороший способ узнать о них — прочитать мой последний пост (гладко, верно?), Который охватывает их во всех деталях.

Наследование и классостроительство

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

Множественное наследование — чего?

При наследовании тип может принимать характеристики другого типа. Существуют три вида характеристик:

  • тип , то есть подтипом типа является другой тип
  • поведение , то есть тип наследует методы и, следовательно, ведет себя так же, как другой тип
  • состояние , т.е. тип наследует переменные, определяющие состояние другого типа

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

Интерфейсы разные: тип может наследоваться от многих интерфейсов и становится подтипом каждого. Таким образом, Java поддерживает этот тип множественного наследования с первого дня.

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

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

Методы по умолчанию против Mixins и черт

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

Примеси

Миксины позволяют наследовать их тип, поведение и состояние. Тип может наследоваться от нескольких миксинов, что обеспечивает множественное наследование всех трех характеристик. В зависимости от языка можно также добавлять миксины в отдельные экземпляры во время выполнения.

Поскольку интерфейсы с методами по умолчанию не допускают наследования состояния, они явно не являются миксинами.

Черты

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

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

  • Как мы уже видели, разрешение вызовов методов не всегда тривиально, что может быстро сделать взаимодействие различных интерфейсов с методами по умолчанию бременем сложности. Черты характера так или иначе облегчают эту проблему.
  • Черты позволяют определенные операции, которые Java не полностью поддерживает. См. Список пунктов маркера после «выбора операций» в статье Википедии о чертах .
  • В статье «Trait-ориентированное программирование в Java 8» исследуется trait-ориентированный стиль программирования с методами по умолчанию и встречаются некоторые проблемы.

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

Методы по умолчанию против абстрактных классов

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

Языковые различия

Давайте сначала констатируем некоторые различия на уровне языка:

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

Тем не менее, остаются некоторые варианты использования, в которых эти различия не имеют значения, и оба подхода технически осуществимы.

Концептуальные различия

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

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

Таким образом, когда абстрактные классы эффективно уменьшаются до малозаметности, скелетные реализации интерфейсов, могут ли методы по умолчанию забрать это? Решительно: нет! Для реализации интерфейсов почти всегда требуются некоторые или все те инструменты построения классов, в которых отсутствуют методы по умолчанию. А если какой-то интерфейс этого не делает, это явно особый случай, который не должен вводить вас в заблуждение. (См. Этот предыдущий пост о том, что может произойти, если интерфейс реализован с методами по умолчанию.)

Больше ссылок

отражение

Эта статья должна была охватить все, что нужно знать о методах по умолчанию. Если вы не согласны, пишите в Твиттере , пишите или оставляете комментарий. Одобрение и +1 также приемлемы.

Ссылка: Все, что вам нужно знать о методах по умолчанию от нашего партнера JCG Николая Парлога в блоге CodeFx .