Java8: Чому заборонено визначати метод за замовчуванням для методу з java.lang.Object


130

Методи за замовчуванням - це гарний новий інструмент у нашій панелі інструментів Java. Однак я спробував написати інтерфейс, який визначає defaultверсію toStringметоду. Java каже мені, що це заборонено, оскільки методи, оголошені в, java.lang.Objectможуть не defaultредагуватися. Чому це так?

Я знаю, що існує правило "базовий клас завжди виграє", тому за замовчуванням (pun;) будь-яка defaultреалізація Objectметоду в Objectбудь-якому випадку буде перезаписана методом . Однак я не бачу причин, чому не повинно бути винятку для методів з Objectспецифікації. Спеціально для toStringцього може бути дуже корисним реалізація за замовчуванням.

Отже, яка причина, чому дизайнери Java вирішили заборонити defaultметоди, які переважають методи Object?


1
Зараз я так добре себе почуваю, що 100 разів підкреслюю це, і, таким чином, золотий знак. Гарне питання!
Євген

Відповіді:


186

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

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

  • Бажання зберегти модель спадкування простою;
  • Той факт, що переглядаючи очевидні приклади (наприклад, перетворюючись AbstractListна інтерфейс), ти розумієш, що успадкування рівних / hashCode / toString сильно пов'язане з єдиним успадкуванням і станом, а інтерфейси множиться у спадок і без стану;
  • Це потенційно відкрило двері для деяких дивних поведінок.

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

Звичайно, є певна ступінь вигоди, яка б виправдовувала більшу складність, але в цьому випадку її немає. Методи, про які ми тут говоримо, - це рівні, hashCode та toString. Ці методи по суті стосуються стану об'єкта, і саме клас, який належить державі, а не інтерфейсу, знаходиться в найкращому становищі для визначення того, що означає рівність для цього класу (особливо, оскільки договір на рівність є досить сильним; див. Ефективний Java для деяких дивних наслідків); Автори інтерфейсу просто надто віддалені.

Легко витягнути AbstractListприклад; було б чудово, якби ми могли позбутися AbstractListі вкласти поведінку в Listінтерфейс. Але після того, як ви вийдете за межі цього очевидного прикладу, не знайдеться багато інших хороших прикладів. У корені, AbstractListрозрахований на одинакове успадкування. Але інтерфейси повинні бути розроблені для багаторазового успадкування.

Далі, уявіть, що ви пишете цей клас:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

В Fooписьменник дивиться на супертіпа, не бачить реалізації рівних, і приходять до висновку , що , щоб отримати рівність посилань, все , що йому потрібно зробити , це наслідувати одно від Object. Потім, наступного тижня, сервіс бібліотеки для Bar "з користю" додає equalsреалізацію за замовчуванням . Ой! Тепер семантика Fooбула порушена інтерфейсом в іншій області технічного обслуговування, "корисно", додавши за замовчуванням загальний метод.

За замовчуванням повинні бути дефолти. Додавання за замовчуванням інтерфейсу, де його не було (ніде в ієрархії), не повинно впливати на семантику конкретних класів реалізації. Але якби параметри за замовчуванням могли "перекрити" методи "Об'єкт", це не було б правдою.

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


13
Я радий, що ви знайшли час, щоб пояснити це, і я ціную всі чинники, які були розглянуті. Я погоджуюся, що це було б небезпечно hashCodeі equals, але, я думаю, це було б дуже корисно toString. Наприклад, деякий Displayableінтерфейс може визначити String display()метод, і це дозволить заощадити масу шаблонного , щоб мати можливість визначити , default String toString() { return display(); }в Displayable, замість того , щоб вимагати кожен Displayableреалізувати toString()або розширити DisplayableToStringбазовий клас.
Брендон

8
@Brandon Ви вірні, що дозволити успадкування toString () не було б небезпечним так само, як це було б для рівних () та hashCode (). З іншого боку, тепер ця функція буде ще більш нерегулярною - і ви все одно матимете таку саму додаткову складність щодо правил успадкування заради цього одного методу ... здається, краще чітко провести лінію, де ми це зробили .
Брайан Гец

5
@gexicide Якщо toString()базується лише на методах інтерфейсу, ви можете просто додати щось на зразок default String toStringImpl()інтерфейсу та замінити toString()в кожному підкласі, щоб викликати реалізацію інтерфейсу - трохи некрасиво, але працює і краще, ніж нічого. :) Ще один спосіб зробити це - зробити щось на зразок Objects.hash(), Arrays.deepEquals()і Arrays.deepToString(). Поставив +1 для відповіді @ BrianGoetz!
Сіу Чін Понг -Asuka Kenji-

3
Поведінка лямбда toString () за замовчуванням справді неприємна. Я знаю, що лямбда-фабрика розроблена дуже просто і швидко, але виплюнути висвітлене ім’я класу насправді не корисно. Маючи default toString()перевизначення в функціональному інтерфейсі дозволить нам --at зробити що - то мірі : як виплюнути підпису функції і батьківського класу реалізатора. Ще краще, якби ми могли принести деякі рекурсивні стратегії стрингування, ми могли б пройти через закриття, щоб отримати дійсно хороший опис лямбда, і, таким чином, значно покращити криву навчання лямбда.
Groostav

Будь-яка зміна toString в будь-якому класі, підкласі, члені екземпляра може вплинути на реалізацію класів або користувачів класу. Крім того, будь-яка зміна будь-якого з методів за замовчуванням також, ймовірно, вплине на всі класи реалізації. Отже, що ж такого особливого у toString, hashCode, коли мова йде про те, що хтось змінює поведінку інтерфейсу? Якщо клас поширює інший клас, він також може змінити. Або якщо вони використовують шаблон делегування. Хтось, хто використовує інтерфейси Java 8, повинен буде зробити це оновленням. Попередження / помилка, яка може бути придушена на підкласі, могла бути надана.
ммм

30

Забороняється визначати методи за замовчуванням в інтерфейсах для методів у java.lang.Object, оскільки методи за замовчуванням ніколи не будуть "доступними".

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

Брайан Гец з Oracle надає ще кілька деталей щодо дизайнерського рішення в цій публікації списку розсилки .


3

Я не бачу в голові авторів мови Java, тому ми можемо лише здогадуватися. Але я бачу багато причин і погоджуюся з ними абсолютно в цьому питанні.

Основна причина впровадження методів за замовчуванням - це можливість додавати нові методи в інтерфейси, не порушуючи зворотної сумісності старих реалізацій. Методи за замовчуванням можуть також використовуватися для надання «зручності» методів без необхідності їх визначення в кожному з класів реалізації.

Жодне з них не стосується StSring та інших методів Object. Простіше кажучи, методи за замовчуванням були розроблені, щоб забезпечити поведінку за замовчуванням там, де іншого визначення не існує. Не надавати реалізацій, які будуть "конкурувати" з іншими існуючими реалізаціями.

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

Крім того, будь-які винятки із загальних правил викликають зайву складність та викликають інші питання. Об'єкт є (більш-менш) класом, як і будь-який інший, тож чому він повинен мати іншу поведінку?

І все, і запропоноване вами рішення, ймовірно, принесе більше мінусів, ніж плюсів.


Я не помітив другого пункту відповіді на гексицид, коли розміщував мій. Він містить посилання, яке детальніше пояснює проблему.
Марвін

1

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


1

Щоб дати дуже педантичну відповідь, визначити defaultметод для публічного методу заборонено лише java.lang.Object. Існує 11 методів розгляду, які можна класифікувати за трьома способами відповіді на це питання.

  1. Шість з Objectметодів не може мати defaultметоди , тому що вони finalі не можуть бути перевизначені на всіх: getClass(), notify(), notifyAll(), wait(), wait(long), і wait(long, int).
  2. Три з Objectметодів не може мати defaultметоди з причин , зазначеним вище Брайан Гетц: equals(Object), hashCode(), і toString().
  3. Два з Objectметодів можуть мати defaultметоди, хоча значення таких за замовчуванням в кращому випадку сумнівне: clone()і finalize().

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }

.В чому справа? Ви все ще не відповіли ЧОМУ частину - "Отже, яка причина, чому дизайнери Java вирішили не дозволяти методам за замовчуванням переосмислювати методи від Object?"
pro_cheats
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.