Может любой объяснять недостаток наследования в Java
вы, вероятно, захотите прочитать:
Эффективная Java ™, второе издание Автор: Джошуа Блох
Глава 4. Классы и интерфейсы
Правило 16: Предпочитайте композицию наследованию
Правило 17: Дизайн и документация для наследования или иначе запрещайте его
Правило 18: Предпочитайте интерфейсы абстрактным классам
Если ваш класс не предназначен для наследования, его следует сделать окончательным. Вы должны быть очень осторожны, чтобы убедиться, что вы понимаете, как методы, которые будут переопределены в подклассе, который не был разработан для функции наследования, чтобы понять, как их изменять.
Конкретный пример:
Допустим, у вас есть класс, который управляет списком имен ...
public class MyNameManager {
private List<String> numbers = new LinkedList<String>();
public void add(String value) {
numbers.add(value);
}
public void addAll(Collection<String> values) {
for(String value : values) {
add(value);
}
}
public void remove(String value) { //... }
//...
}
Теперь предположим, что вы хотите создать новый подкласс, который также подсчитывает общее количество раз, когда имя добавляется к list, например:
public class MyCountingNameManager extends MyNameManager {
private int count = 0;
@Override
protected void addAll(Collection<String> values) {
count += values.size();
super.addAll(values);
}
@Override
protected void add(String value) {
count += 1;
super.add(value);
}
}
Выглядит довольно просто, не так ли? Но рассмотрим результат следующего:
MyCountingNameManager m = new MyCountingNameManager();
m.add("bob");
m.add("Sally");
Теперь счет 2, и все хорошо. Но если бы мы сделали следующее:
List<String> family = new List<String>();
family.add("mom");
family.add("dad");
family.add("brother");
MyCountingNameManager m = new MyCountingNameManager();
m.add(family);
Счетчик теперь равен 6, а не 3, как вы, вероятно, ожидали.Это связано с тем, что вызов addAll
добавляет размер Коллекции значений (который равен 3) к счетчику, а затем вызывает метод super.addAll
для выполнения фактической обработки. super.addAll
выполняет итерацию Коллекции и вызывает метод add
для каждого значения. Но поскольку мы работаем в MyCountingNameManager
и , а не в a MyNameManager
, переопределенный метод add
в подклассе вызывается каждый раз . Выполняемый метод MyCountingNameManager.add
также увеличивает счетчик! В результате каждое имя засчитывается дважды!
Я считаю, что этот пример взят из Effective Java. Вы обязательно должны найти копию и прочитать элементы, перечисленные в ответе Виле, для более глубокого понимания некоторых случаев, когда наследование не подходит.
Взять хотя бы статью Аллена Холуба в JavaWorld под названием Почему расширение - зло . Он обсуждает такие вещи, как сильная связь и проблема хрупкого базового класса .
Мы предпочитаем композицию наследованию, потому что, когда мы добавляем (в частности) или изменяем функциональность путем создания подклассов, мы присоединяем эту новую функциональность к классу - так что везде, где нам нужна новая функциональность, нам нужен этот класс. Это распространяется даже на другие подклассы - и с моделью одиночного наследования Java, если у нас есть два новых бита функциональности, которые мы хотим в еще одном классе, нет (простого) способа ввести оба бита, если каждый находится в отдельном подклассе изначальный предок.
Для сравнения: если мы расширяем функциональность посредством композиции, любой класс - в существующей иерархии или вне ее - может включить ее, просто включив крошечный класс, у которого есть новая функция. Это проще, чище, удобнее для повторного использования и легче читать.
Наследование имеет свое место, но оно не подходит для многих, многих профессий.