Java8: почему запрещено определять метод по умолчанию для метода из java.lang.Object

130

Методы по умолчанию - отличный новый инструмент в нашем наборе инструментов Java. Однако я попытался написать интерфейс, определяющий defaultверсию toStringметода. Java сообщает мне, что это запрещено, поскольку методы, объявленные в, java.lang.Objectнельзя defaultредактировать. Почему это так?

Я знаю, что существует правило «базовый класс всегда побеждает», поэтому по умолчанию (каламбур;) любая defaultреализация Objectметода в Objectлюбом случае будет перезаписана этим методом . Однако я не вижу причин, по которым не должно быть исключения для методов из Objectспецификации. Особенно для toStringэтого может быть очень полезно иметь реализацию по умолчанию.

Итак, в чем причина, по которой разработчики Java решили не разрешать defaultметоды, замещающие методы Object?

gexicide
источник
1
Сейчас я так доволен собой, проголосовав за это 100 раз и, таким образом, получил золотой значок. хороший вопрос!
Eugene

Ответы:

186

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

В этом письме много на эту тему (и на другие темы тоже). Было несколько дизайнерских сил, которые объединились, чтобы привести нас к текущему дизайну:

  • Желание сохранить простую модель наследования;
  • Тот факт, что, пропустив очевидные примеры (например, превращение AbstractListв интерфейс), вы поймете, что наследование equals / hashCode / toString сильно привязано к одиночному наследованию и состоянию, а интерфейсы наследуются многократно и не имеют состояния;
  • Это потенциально открыло дверь для некоторых неожиданных поступков.

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

Конечно, есть определенные преимущества, которые оправдывают большую сложность, но в данном случае их нет. Здесь мы говорим о методах equals, 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базовый класс.
Brandon
8
@Brandon Вы правы, что разрешение наследования toString () не было бы опасным так же, как это было бы для equals () и hashCode (). С другой стороны, теперь эта функция будет еще более нерегулярной - и вы все равно столкнетесь с той же дополнительной сложностью в отношении правил наследования, ради этого единственного метода ... кажется, лучше провести черту там, где мы это сделали ,
Brian Goetz
5
@gexicide Если toString()он основан только на методах интерфейса, вы можете просто добавить что-то вроде default String toStringImpl()интерфейса и переопределить toString()в каждом подклассе для вызова реализации интерфейса - немного некрасиво, но работает и лучше, чем ничего. :) Другой способ сделать это - сделать что-то вроде Objects.hash(), Arrays.deepEquals()и Arrays.deepToString(). +1 за ответ @BrianGoetz!
Сиу Чинг Понг - Асука Кенджи -
3
Поведение по умолчанию toString () лямбды действительно неприятно. Я знаю, что лямбда-фабрика разработана так, чтобы быть очень простой и быстрой, но выдавать производное имя класса действительно бесполезно. Наличие default toString()переопределения в функциональном интерфейсе позволило бы нам - по крайней мере - сделать что-то вроде выдачи сигнатуры функции и родительского класса разработчика. Еще лучше, если бы мы могли применить некоторые рекурсивные стратегии toString, мы могли бы пройти через закрытие, чтобы получить действительно хорошее описание лямбда и, таким образом, значительно улучшить кривую обучения лямбда.
Groostav
Любое изменение toString в любом классе, подклассе или члене экземпляра может иметь такой эффект на реализацию классов или пользователей класса. Более того, любое изменение любого из методов по умолчанию также может повлиять на все реализующие классы. Так что же такого особенного в toString, hashCode, когда речь идет о том, что кто-то изменяет поведение интерфейса? Если класс расширяет другой класс, они тоже могут измениться. Или если они используют шаблон делегирования. Кто-то, использующий интерфейсы Java 8, должен будет сделать это путем обновления. Предупреждение / ошибка, которые можно подавить в подклассе, могли быть предоставлены.
ммм
30

Запрещено определять методы по умолчанию в интерфейсах для методов в java.lang.Object, поскольку методы по умолчанию никогда не будут «достижимыми».

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

Брайан Гетц из Oracle предоставляет еще несколько подробностей о дизайнерском решении в этом сообщении списка рассылки .

jarnbjo
источник
3

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

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

Ничего из этого не применимо к toString и другим методам Object. Проще говоря, методы по умолчанию были разработаны для обеспечения поведения по умолчанию там, где нет другого определения. Не предоставлять реализации, которые будут «конкурировать» с другими существующими реализациями.

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

Кроме того, введение ЛЮБЫХ исключений из общих правил вызывает ненужные сложности и вызывает другие вопросы. Объект (более или менее) является классом, как и любой другой, так почему он должен вести себя иначе?

В общем, предлагаемое вами решение принесет больше минусов, чем плюсов.

Марвин
источник
Я не заметил второй абзац ответа гексицида, когда разместил свой. Он содержит ссылку, которая более подробно объясняет проблему.
Marwin 03
1

Рассуждения очень просты, потому что Object является базовым классом для всех классов Java. Таким образом, даже если у нас есть метод объекта, определенный как метод по умолчанию в каком-либо интерфейсе, он будет бесполезен, потому что метод объекта всегда будет использоваться. Вот почему, чтобы избежать путаницы, у нас не может быть методов по умолчанию, которые переопределяют методы класса 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();
            }
        }
    }
    
jaco0646
источник
.В чем смысл? Вы все еще не ответили на вопрос «ПОЧЕМУ» - «Итак, по какой причине разработчики Java решили не разрешать методам по умолчанию заменять методы из Object?»
pro_cheats 02