Есть два неизбежных недостатка использования наследования в JAVA:
- Это нарушает инкапсуляцию и заставляет разработчиков понимать детали реализации суперкласса и связь между подклассами и суперклассами.
- Может вызвать ошибки после обновления суперкласса.
Наследование нарушает инкапсуляцию
В связи с этим, вот подробный пример (из эффективного элемента Java 16)
public class MyHashSet<E> extends HashSet<E> {
private int addCount = 0;
public int getAddCount() {
return addCount;
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
Вот обычайHashSet
, переписывает два метода, единственная разница между ним и суперклассом в том, что добавляется счетчик для подсчета количества добавленных элементов.
Напишите тест, чтобы увидеть, работает ли это новое дополнение:
public class MyHashSetTest {
private MyHashSet<Integer> myHashSet = new MyHashSet<Integer>();
@Test
public void test() {
myHashSet.addAll(Arrays.asList(1,2,3));
System.out.println(myHashSet.getAddCount());
}
}
После запуска вы обнаружите, что после добавления 3 элементов значение на выходе счетчика равно 6.
в суперклассaddAll()
Метод найдет причину ошибки: он внутренне вызывает методadd()
метод. Итак, в этом тесте введите подклассaddAll()
метод, счетчик увеличивается на 3, а затем суперклассaddAll()
, суперклассaddAll()
вызовет подклассadd()
Трижды счетчик увеличится на три.
корень проблемы
Абстрагируясь от этой ситуации, вы можете обнаружить, что ошибка связана с тем, что существует переопределяемый метод суперкласса.
(то есть переопределяемый метод в суперклассе вызывает другие переопределяемые методы.) В настоящее время, если подкласс переопределяет некоторые из этих методов, это может вызвать ошибку.
Например, в случае с приведенным выше рисункомFather
В классе есть переопределяемые методыA
и методB
,иA
называетсяB
. ПодклассSon
переопределенный методB
, в это время, если подкласс вызывает унаследованный методA
, то методA
больше не называетсяFather.B()
, а метод в подклассеSon.B()
. Если правильность программы зависит отFather.B()
некоторые операции, при этомSon.B()
Переопределение этих операций может привести к ошибкам.
Дело в том, что подкласс может быть написан без проблем на поверхности, но он будет делать ошибки, что вынуждает разработчиков разбираться в деталях реализации суперкласса, тем самым нарушая объектно-ориентированную инкапсуляцию, потому что инкапсуляция требует сокрытия деталей реализации. Что более опасно, так это то, что ошибки не обязательно легко обнаружить, и если разработчик не знает деталей реализации суперкласса и переписывает его, могут быть скрытые опасности.
Может генерировать ошибки при обновлении суперклассов
Это легче понять, и есть в основном следующие возможности:
- Суперкласс изменяет сигнатуру существующего метода. приведет к ошибке компиляции.
- В суперклассе появились новые методы:
- Метод с той же сигнатурой, что и у существующего метода подкласса, но с другим типом возвращаемого значения, приведет к ошибке компиляции.
- Это то же самое, что и существующая сигнатура метода подкласса, что приведет к тому, что подкласс непреднамеренно перезапишет ее, возвращаясь к первому случаю.
- Нет конфликта с подклассами, но может повлиять на корректность программы. Например, добавление элементов в подкласс в набор должно соответствовать определенным условиям.В это время, если суперкласс добавит метод, который может напрямую вставлять элементы без обнаружения, корректность программы будет поставлена под угрозу.
Проектирование наследуемых классов
При проектировании классов, которые могут наследоваться, следует обратить внимание на:
- Для переопределяемых методов, которые существуют для самостоятельного использования, детали вызова должны быть точно задокументированы.
- Выставляйте как можно меньше защищенных членов, иначе вы выставите слишком много деталей реализации.
- Конструкторы не должны вызывать какие-либо переопределяемые методы.
Объясните подробно третий пункт. это на самом деле и
Обсуждаемая здесь проблема очень похожа, предполагая следующий код:
public class Father {
public Father() {
someMethod();
}
public void someMethod() {
}
}
public class Son extends Father {
private Date date;
public Son() {
this.date = new Date();
}
@Override
public void someMethod() {
System.out.println("Time = " + date.getTime());
}
}
Приведенный выше код выдает ошибки при запуске тестаNullPointerException
:
public class SonTest {
private Son son = new Son();
@Test
public void test() {
son.someMethod();
}
}
Поскольку конструктор суперкласса будет выполняться перед конструктором подкласса, здесь конструктор суперкласса прав.someMethod()
зависимый, при этомsomeMethod()
переопределен, поэтому вызов конструктора суперкласса будетSon.someMethod()
, а подкласс в это время не инициализирован, поэтому выполняетсяdate.getTime()
Возникает исключение нулевого указателя.
Следовательно, если в конструкторе суперкласса есть зависимость от переопределяемого метода, может возникнуть ошибка наследования.
в заключении
Используйте наследование осторожно, композиция имеет приоритет над наследованием.
Переопределение автономных переопределяемых методов в суперклассе при использовании наследования может привести к ошибкам, и даже без переопределения могут появиться ошибки при обновлении суперкласса.
Если используются и наследование, и композиция, то композиция предпочтительнее, и с помощью композиции можно избежать вышеуказанных недостатков наследования.
Если предполагается использовать наследование, суперкласс должен быть тщательно спроектирован и хорошо задокументирован.