Почему наследование следует использовать с осторожностью в Java

Java задняя часть
Почему наследование следует использовать с осторожностью в Java

Есть два неизбежных недостатка использования наследования в JAVA:

  1. Это нарушает инкапсуляцию и заставляет разработчиков понимать детали реализации суперкласса и связь между подклассами и суперклассами.
  2. Может вызвать ошибки после обновления суперкласса.

Наследование нарушает инкапсуляцию

В связи с этим, вот подробный пример (из эффективного элемента 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и методBAназывается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()Возникает исключение нулевого указателя.

Следовательно, если в конструкторе суперкласса есть зависимость от переопределяемого метода, может возникнуть ошибка наследования.

в заключении

Используйте наследование осторожно, композиция имеет приоритет над наследованием.

Переопределение автономных переопределяемых методов в суперклассе при использовании наследования может привести к ошибкам, и даже без переопределения могут появиться ошибки при обновлении суперкласса.

Если используются и наследование, и композиция, то композиция предпочтительнее, и с помощью композиции можно избежать вышеуказанных недостатков наследования.

Если предполагается использовать наследование, суперкласс должен быть тщательно спроектирован и хорошо задокументирован.