Чому до інтерфейсів Java 8 додавали статичні методи за замовчуванням, коли у нас вже були абстрактні класи?


99

У Java 8 інтерфейси можуть містити реалізовані методи, статичні методи та так звані "типові" методи (які класам реалізації не потрібно переосмислювати).

На мій (напевно, наївний) погляд, не було потреби порушувати такі інтерфейси. Інтерфейси завжди були контрактом, який ви повинні виконати, і це дуже проста і чиста концепція. Зараз це поєднання кількох речей. На мою думку:

  1. статичні методи не належать до інтерфейсів. Вони належать до корисних класів.
  2. Методи "за замовчуванням" взагалі не повинні бути дозволені в інтерфейсах. Ви завжди могли використовувати для цього абстрактний клас.

Коротко:

Перед Java 8:

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

Після Java 8:

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

Які основні причини таких великих змін та які нові переваги (якщо такі є) вони додають?


30
Питання про бонус: Чому замість цього вони не ввели багаторазове успадкування для класів?

2
статичні методи не належать до інтерфейсів. Вони належать до корисних класів. Ні, вони не належать до @Deprecatedкатегорії! статичні методи - одна з найбільш зловживаних конструкцій на Яві через незнання та лінь. Багато статичних методів зазвичай означають некомпетентного програміста, збільшують зв'язок на кілька порядків і є кошмаром для тестування та рефактора, коли ти зрозумієш, чому вони погана ідея!

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 він скидається на зовнішній масив, а потім сортує це
храповик freak

5
Я знайшов цей документ, де Гец пояснює проблему. Тож я зараз позначаю цю відповідь як рішення.
Містер Сміт

1
@RobertHarvey: Створіть Список двохсот мільйонів елементів <Byte>, використовуйте IEnumerable<Byte>.Appendдля їх приєднання, а потім зателефонуйте Count, а потім розкажіть, як методи розширення вирішують проблему. Якщо б CountIsKnownі Countбули членами Російської Федерації IEnumerable<T>, повернення з них Appendмогло б рекламуватись, CountIsKnownякщо це зробили установчі колекції, але без таких методів це неможливо.
supercat

6
@supercat: Я не маю ні найменшого уявлення про те, про що ти говориш.
Роберт Харві

48

Вірна відповідь насправді знайдена в документації Java , де зазначено:

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

Це стало давнім джерелом болю на 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 .


1
Для подальшого подальшого контролю над моїм останнім пунктом (тобто не застосовуйте контракти в інтерфейсах ), варто зазначити, що defaultметоди не можуть перекрити equals, hashCodeі toString. Дуже інформативний аналіз вартості та вигод, чому це заборонено, можна знайти тут: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
ngreen

Дуже погано, що Java має лише один віртуальний equalsі єдиний hashCodeметод, оскільки існує два різних види рівності, які колекції, можливо, знадобляться для перевірки, і елементи, які реалізують декілька інтерфейсів, можуть бути дотримані суперечливих контрактних вимог. Можливість використання списків, які не збираються змінювати як hashMapключі, є корисним, але бувають і випадки, коли було б корисно зберігати колекції у hashMapвідповідних речах на основі еквівалентності, а не поточного стану [еквівалентність передбачає відповідність стану та незмінності ] .
supercat

Добре, що у Java є вирішення методів порівняння та порівняльних інтерфейсів. Але вони такі потворні, я думаю.
ngreen

Ці інтерфейси підтримуються лише для певних типів колекції, і вони створюють власну проблему: компаратор може сам потенційно інкапсулювати стан (наприклад, спеціалізований компаратор рядків може ігнорувати налаштовувану кількість символів на початку кожного рядка, у цьому випадку кількість символи, які слід ігнорувати, були б частиною стану компаратора), що, в свою чергу, стане частиною стану будь-якої колекції, яка була сортована ним, але немає визначеного механізму запитати два порівняльника, чи є вони еквівалентними.
supercat

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

44

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

Альтернативою є перетворення цих абстрактних базових класів у набір статичних методів та перетворення всіх полів у аргументи. Це дозволило б будь-якому реалізатору інтерфейсу зателефонувати статичним методам та отримати функціональність, але це дуже велика кількість шаблонів мовою, яка вже занадто багатослівна.


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

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інтерфейсі, і його було б легше знайти.


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 #, щоб додати реалізовану функціональність до інтерфейсів.


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()гойдався б.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.