Это еще одна проблема языкового дизайна, которая кажется «очевидно хорошей идеей», пока вы не начнете копать и не поймете, что это на самом деле плохая идея.
В этом письме много на эту тему (и на другие темы тоже). Было несколько дизайнерских сил, которые объединились, чтобы привести нас к текущему дизайну:
- Желание сохранить простую модель наследования;
- Тот факт, что, пропустив очевидные примеры (например, превращение
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
была нарушена интерфейсом в другом домене обслуживания «услужливо» добавлением значения по умолчанию для общего метода.
Значения по умолчанию должны быть значениями по умолчанию. Добавление значения по умолчанию к интерфейсу там, где его не было (где-либо в иерархии), не должно влиять на семантику конкретных реализующих классов. Но если бы значения по умолчанию могли «переопределить» методы объекта, это было бы неправдой.
Таким образом, хотя это кажется безобидной функцией, на самом деле она довольно вредна: она добавляет много сложности для небольшой дополнительной выразительности и делает слишком легким для благих намерений, безобидно выглядящих изменений отдельно скомпилированные интерфейсы, чтобы подорвать предполагаемая семантика реализации классов.