Переопределение частных методов в Java
Как кратко описано здесь, переопределение закрытых методов в Java недопустимо, потому что закрытые методы родительского класса "автоматически становятся окончательными и скрыты от производного класса". Мой вопрос в значительной степени академический.
Как не является нарушением инкапсуляции, чтобы не допустить переопределения частного метода родителя (т. Е. Реализованного независимо с той же сигнатурой в дочернем классе)? Закрытый метод родителя не может быть доступен или наследован дочерним классом в соответствии с принципами инкапсуляции. Это скрыто.
Итак, почему дочерний класс должен быть ограничен в реализации своего собственного метода с тем же именем / подписью? Есть ли для этого хорошая теоретическая основа или это просто какое-то прагматическое решение? Есть ли в других языках (C++ или C#) другие правила?
10 ответов
Вы не можете переопределить закрытый метод, но можете без проблем ввести его в производный класс. Это хорошо компилируется:
class Base
{
private void foo()
{
}
}
class Child extends Base
{
private void foo()
{
}
}
Обратите внимание, что если вы попытаетесь применить @Override
аннотация к Child.foo()
вы получите ошибку во время компиляции. Пока ваш компилятор /IDE настроен на выдачу предупреждений или ошибок, если вы пропускаете @Override
аннотация, все должно быть хорошо. По общему признанию, я предпочитаю подход C# override
быть ключевым словом, но было очевидно, что было слишком поздно делать это в Java.
Что касается обработки в C# "переопределения" закрытого метода - частный метод не может быть виртуальным, во-первых, но вы, безусловно, можете ввести новый частный метод с тем же именем, что и у закрытого метода в базовом классе.
Что ж, разрешение перезаписывать частные методы может привести к утечке инкапсуляции или угрозе безопасности. Если мы предположим, что это возможно, то получим следующую ситуацию:
Допустим, есть частный метод
boolean hasCredentials()
тогда расширенный класс может просто переопределить его следующим образом:boolean hasCredentials() { return true; }
тем самым нарушая проверку безопасности.
Единственный способ предотвратить исходный класс - это объявить его метод
final
, Но теперь, это утечка информации о реализации через инкапсуляцию, потому что производный класс теперь не может создать методhasCredentials
больше - это будет конфликтовать с определенным в базовом классе.Это плохо: допустим, сначала этот метод не существует в
Base
, Теперь разработчик может законно получить классDerived
и дать ему методhasCredentials
который работает как ожидалось.Но теперь новая версия оригинала
Base
класс выпущен. Его общедоступный интерфейс не изменяется (как и его инварианты), поэтому мы должны ожидать, что он не нарушает существующий код. Только так, потому что теперь есть столкновение имен с методом в производном классе.
Я думаю, что вопрос связан с недоразумением:
Как это / нет / нарушение инкапсуляции, чтобы не позволить "переопределить" закрытый метод родителя (то есть реализованный независимо, с той же самой сигнатурой, в дочернем классе)
Текст в скобках является противоположностью текста перед ним. Java позволяет вам "независимо реализовывать [закрытый метод] с той же сигнатурой в дочернем классе". Не допускание этого нарушит инкапсуляцию, как я объяснил выше.
Но "не допускать" переопределения "частного метода родителя" - это нечто иное, необходимое для обеспечения инкапсуляции.
"У других языков (C++ или C#) есть другие правила?"
Ну, C++ имеет разные правила: статический или динамический процесс привязки функции-члена и принудительное использование прав доступа являются ортогональными.
Предоставление функции-члена private
модификатор привилегий доступа означает, что эта функция может быть вызвана только ее декларирующим классом, но не другими (даже не производными классами). Когда вы объявляете private
функция-член как virtual
даже чисто виртуальный (virtual void foo() = 0;
), вы позволяете базовому классу получать выгоду от специализации, при этом все еще применяя права доступа.
Когда дело доходит до virtual
Функции-члены, права доступа говорят вам, что вы должны делать:
private virtual
означает, что вам разрешено специализировать поведение, но вызов функции-члена выполняется базовым классом, безусловно, контролируемым образомprotected virtual
означает, что вы должны / должны вызывать версию класса-члена верхнего уровня при переопределении
Таким образом, в C++ привилегии доступа и виртуальность не зависят друг от друга. Определение того, должна ли функция быть статически или динамически связанной, является последним шагом в разрешении вызова функции.
Наконец, шаблонный шаблонный метод должен быть предпочтительнее public virtual
функции-члены.
Ссылка: Беседы: практически ваши
В статье дается практическое использование private virtual
функция-член.
ISO / IEC 14882-2003 §3.4.1
Поиск имени может связывать более одного объявления с именем, если он находит имя как имя функции; говорят, что декларации образуют набор перегруженных функций (13.1). Разрешение перегрузки (13.3) происходит после успешного поиска имени. Правила доступа (пункт 11) рассматриваются только после успешного поиска имени и разрешения перегрузки функции (если применимо). Только после поиска имени, разрешения перегрузки функции (если применимо) и проверки доступа успешно выполняются атрибуты, введенные объявлением имени, которые используются в дальнейшем при обработке выражений (пункт 5).
ISO / IEC 14882-2003 §5.2.2
Функция, вызываемая в вызове функции-члена, обычно выбирается в соответствии со статическим типом выражения объекта (пункт 10), но если эта функция isvirtual и не указана с использованием aqualified-idthen, то фактически вызванная функция будет окончательным переопределением (10.3) выбранная функция в динамическом типе выражения объекта [Примечание: динамический тип - это тип объекта, на который указывает или на который ссылается текущее значение выражения объекта.
Закрытый метод родителя не может быть доступен или наследован дочерним классом в соответствии с принципами инкапсуляции. Это скрыто.
Итак, почему дочерний класс должен быть ограничен в реализации своего собственного метода с тем же именем / подписью?
Там нет такого ограничения. Вы можете сделать это без каких-либо проблем, это просто не называется "переопределение".
Переопределенные методы подвергаются динамической диспетчеризации, то есть фактически вызываемый метод выбирается во время выполнения в зависимости от фактического типа вызываемого объекта. С приватным методом этого не происходит (и не должно быть, как указано в вашем первом утверждении). И это то, что подразумевается под выражением "частные методы не могут быть переопределены".
Кажется, это вопрос выбора и определения. Причина, по которой вы не можете сделать это в Java, заключается в том, что в спецификации так сказано, но вопрос был в том, почему в спецификации так сказано.
Тот факт, что C++ допускает это (даже если мы используем виртуальное ключевое слово для принудительной динамической отправки), показывает, что нет внутренней причины, по которой вы не могли бы этого допустить.
Однако, кажется, что вполне законно заменить метод:
class B {
private int foo()
{
return 42;
}
public int bar()
{
return foo();
}
}
class D extends B {
private int foo()
{
return 43;
}
public int frob()
{
return foo();
}
}
Кажется, компилируется нормально (на моем компиляторе), но D.foo не имеет отношения к B.foo (то есть не переопределяет его) - bar() всегда возвращает 42 (вызывая B.foo) и frob() всегда возвращает 43 (вызывая D.foo) независимо от того, вызывается ли он в экземпляре B или D.
Одна из причин, по которой Java не разрешает переопределять метод, состоит в том, что им не нравится разрешать изменение метода, как в примере с Конрадом Рудольфом. Обратите внимание, что C++ отличается здесь тем, что вам нужно использовать ключевое слово "virtual" для получения динамической отправки - по умолчанию его нет, поэтому вы не можете изменить код в базовом классе, который использует метод hasCredentials. Приведенный выше пример также защищает от этого, поскольку D.foo не заменяет вызовы foo из B.
Я думаю, что вы неверно истолковываете, что говорит этот пост. Нельзя сказать, что дочерний класс "ограничен в реализации своего собственного метода с тем же именем / подписью".
Вот код, слегка отредактированный:
public class PrivateOverride {
private static Test monitor = new Test();
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
});
}
}
class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}
И цитата:
Вы можете разумно ожидать, что результат будет "public f()",
Причина этой цитаты в том, что переменная po
на самом деле содержит экземпляр Derived. Однако, поскольку метод определен как закрытый, компилятор фактически смотрит на тип переменной, а не на тип объекта. И это переводит вызов метода в invokespecial (я думаю, что это правильный код операции, я не проверял спецификации JVM), а не invokeinstance.
Я извиняюсь за неправильное использование термина override и не согласуется с моим описанием. Мое описание описывает сценарий. Следующий код расширяет пример Джона Скита, чтобы изобразить мой сценарий:
class Base {
public void callFoo() {
foo();
}
private void foo() {
}
}
class Child extends Base {
private void foo() {
}
}
Использование похоже на следующее:
Child c = new Child();
c.callFoo();
Проблема, с которой я столкнулся, заключается в том, что вызывался родительский метод foo (), хотя, как показывает код, я вызывал callFoo () для дочерней переменной экземпляра. Я думал, что определяю новый закрытый метод foo () в Child (), который вызовет унаследованный метод callFoo (), но я думаю, что то, что сказал kdgregory, может относиться к моему сценарию - возможно, из-за того, как конструктор производного класса вызывает super (), а может и нет.
В Eclipse не было предупреждений компилятора, и код компилировался. Результат был неожиданным.
Когда метод закрыт, он не виден своему дочернему элементу. Так что нет смысла переопределять это.
Помимо всего сказанного ранее, есть очень семантическая причина не допускать переопределения частных методов... ОНИ ЧАСТНЫЕ!!!
Если я напишу класс и укажу, что метод является "закрытым", он должен быть совершенно незаметен для внешнего мира. Никто не должен иметь доступ к нему, переопределить его или что-либо еще. Я просто должен быть в состоянии знать, что это исключительно МОЙ метод, и что никто другой не собирается с ним связываться или зависеть от него. Это не может считаться личным, если кто-то может с этим смириться. Я верю, что это действительно так просто.
Класс определяется тем, какие методы он делает доступными и как они себя ведут. Не так, как это реализовано внутри (например, через вызовы частных методов).
Поскольку инкапсуляция связана с поведением, а не деталями реализации, частные методы не имеют ничего общего с идеей инкапсуляции. В каком-то смысле ваш вопрос не имеет смысла. Это все равно что спросить: "Как положить сливки в кофе не является нарушением инкапсуляции?"
Предположительно, приватный метод используется чем-то публичным. Вы можете переопределить это. При этом вы изменили поведение.