Переопределение частных методов в Java - PullRequest
57 голосов
/ 04 января 2010

Как кратко описано здесь , переопределение приватных методов в Java недопустимо, потому что приватные методы родительского класса «автоматически становятся финальными и скрыты от производного класса». Мой вопрос в значительной степени академический.

Как это не нарушение инкапсуляции, чтобы не позволить "переопределить" закрытый метод родителя (то есть реализованный независимо, с той же самой сигнатурой, в дочернем классе)? Закрытый метод родителя не может быть доступен или наследован дочерним классом в соответствии с принципами инкапсуляции. Это скрыто.

Итак, почему дочерний класс должен быть ограничен в реализации своего собственного метода с тем же именем / сигнатурой? Есть ли для этого хорошая теоретическая основа или это просто какое-то прагматическое решение? У других языков (C ++ или C #) есть другие правила?

Ответы [ 10 ]

70 голосов
/ 04 января 2010

Вы не можете переопределить закрытый метод, но вы можете без проблем ввести его в производный класс. Это прекрасно компилируется:

class Base
{
   private void foo()
   {
   }
}

class Child extends Base
{
    private void foo()
    {
    }
}

Обратите внимание, что если вы попытаетесь применить аннотацию @Override к Child.foo(), вы получите ошибку во время компиляции. Пока ваш компилятор / IDE настроен на выдачу предупреждений или ошибок, если вы пропускаете @Override аннотацию, все должно быть хорошо. По общему признанию я предпочитаю подход C # override, являющийся ключевым словом, но, очевидно, было слишком поздно делать это в Java.

Что касается обработки в C # «переопределения» закрытого метода - частный метод не может быть виртуальным, во-первых, но вы, безусловно, можете ввести новый частный метод с тем же именем, что и закрытый метод в базовом классе.

27 голосов
/ 04 января 2010

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

  1. Скажем, есть приватный метод boolean hasCredentials(), тогда расширенный класс может просто переопределить его следующим образом:

    boolean hasCredentials() { return true; }
    

    тем самым нарушая проверку безопасности.

  2. Единственный способ предотвратить исходный класс - это объявить его метод final. Но теперь это утечка информации о реализации через инкапсуляцию, потому что производный класс не может создавать метод hasCredentials больше - он будет конфликтовать с тем, который определен в базовом классе.

    Это плохо: допустим, этот метод не существует сначала в Base. Теперь разработчик может законно получить класс Derived и присвоить ему метод hasCredentials, который работает, как и ожидалось.

    Но теперь вышла новая версия оригинального Base класса. Его общедоступный интерфейс не изменяется (как и его инварианты), поэтому мы должны ожидать, что он не нарушает существующий код. Только так, потому что теперь в производном классе есть конфликт имен с методом.

Я думаю, что вопрос связан с недоразумением:

Как это / нет / нарушение инкапсуляции, чтобы не допустить "переопределения" частного метода родителя (т. Е. Реализованного независимо с той же сигнатурой в дочернем классе)

Текст в скобках - это напротив текста перед ним. Java действительно позволяет вам «независимо реализовывать [закрытый метод] с той же сигнатурой в дочернем классе». Если вы не допустите это, это нарушит инкапсуляцию, как я объяснил выше.

Но «запретить« переопределение »частного метода родителя» - это нечто иное, и необходимо обеспечить инкапсуляцию .

16 голосов
/ 04 января 2010

"У других языков (C ++ или C #) есть другие правила?"

Что ж, в C ++ есть другие правила: статический или динамический процесс привязки функции-члена и принудительное выполнение прав доступа являются ортогональными.

Предоставление функции-члену модификатора привилегий доступа private означает, что эта функция может вызываться только ее декларирующим классом, но не другими (даже не производными классами). Когда вы объявляете функцию-член private как virtual, даже чисто виртуальную (virtual void foo() = 0;), вы позволяете базовому классу получать выгоду от специализации, в то же время применяя права доступа.

Когда дело доходит до virtual функций-членов, права доступа говорят вам, что вы должны делать:

  • private virtual означает, что вам разрешено специализировать поведение, но вызов функции-члена выполняется базовым классом, безусловно, контролируемым образом
  • protected virtual означает, что вы должны / должны вызвать версию функции-члена верхнего класса при переопределении

Итак, в C ++ привилегии доступа и виртуальность не зависят друг от друга. Определение того, должна ли функция быть статически или динамически связанной, является последним шагом в разрешении вызова функции.

Наконец, шаблон проектирования «Шаблонный метод» следует отдавать предпочтение перед public virtual функциями-членами.

Ссылка: Беседы: виртуально ваши

В статье дается практическое применение функции-члена private virtual.


ИСО / МЭК 14882-2003 §3.4.1

Поиск имени может связывать более одного объявления с именем, если он находит имя как имя функции; говорят, что объявления образуют набор перегруженных функций (13.1). Разрешение перегрузки (13.3) происходит после успешного поиска имени. Правила доступа (пункт 11) рассматриваются только после успешного поиска имени и разрешения перегрузки функции (если применимо). Только после поиска имени, разрешения перегрузки функции (если применимо) и проверки доступа успешно выполняются атрибуты, введенные объявлением имени, которые используются в дальнейшем при обработке выражений (пункт 5).

ИСО / МЭК 14882-2003 §5.2.2

Функция, вызываемая в вызове функции-члена, обычно выбирается в соответствии со статическим типом выражения объекта (пункт 10), но если эта функция isvirtual и не указана с помощью aqualified-id, тогда фактически вызванная функция будет последней переопределенной функцией ( 10.3) выбранной функции в динамическом типе выражения объекта [Примечание: динамический тип - это тип объекта, на который указывает или на который ссылается текущее значение выражения объекта.

7 голосов
/ 04 января 2010

Закрытый метод родителя не может быть доступен или наследован дочерним классом в соответствии с принципами инкапсуляции. Это скрыто.

Итак, почему детский класс должен быть ограничено в реализации своего метод с тем же именем / подписью?

Нет такого ограничения. Вы можете сделать это без каких-либо проблем, это просто не называется «переопределение».

Переопределенные методы подлежат динамической диспетчеризации, то есть фактически вызываемый метод выбирается во время выполнения в зависимости от фактического типа объекта, к которому он вызывается. С приватным методом этого не происходит (и не должно быть, как указано в вашем первом утверждении). И это то, что подразумевается под выражением «частные методы не могут быть переопределены».

3 голосов
/ 04 января 2010

Я думаю, вы неверно истолковали то, что говорится в этом посте. не говорит о том, что дочерний класс «ограничен в реализации своего собственного метода с тем же именем / подписью».

Вот код, слегка отредактированный:

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 .

2 голосов
/ 04 мая 2015

Кажется, это вопрос выбора и определения. Причина, по которой вы не можете сделать это в 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.

0 голосов
/ 15 ноября 2013

Помимо всего сказанного ранее, есть очень семантическая причина, по которой нельзя допускать переопределение частных методов ... ОНИ ЧАСТНЫЕ !!!

Если я напишу класс и укажу, что метод является «закрытым», он должен быть совершенно незаметен для внешнего мира. Никто не должен иметь доступ к нему, переопределить его или что-либо еще. Я просто должен быть в состоянии знать, что это исключительно МОЙ метод, и что никто другой не собирается с ним связываться или зависеть от него. Это не может считаться личным, если кто-то может с этим смириться. Я считаю, что это действительно так просто.

0 голосов
/ 05 января 2010

Я извиняюсь за неправильное использование термина 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 не было предупреждений компилятора, и код компилировался. Результат был неожиданным.

0 голосов
/ 04 января 2010

Когда метод закрытый, он не виден его дочернему элементу. Так что нет смысла переопределять это.

0 голосов
/ 04 января 2010

Класс определяется тем, какие методы он делает доступными и как они ведут себя. Не так, как это реализовано внутри (например, через вызовы частных методов).

Поскольку инкапсуляция связана с поведением, а не деталями реализации, частные методы не имеют ничего общего с идеей инкапсуляции. В каком-то смысле ваш вопрос не имеет смысла. Это все равно что спросить: «Как положить сливки в кофе не является нарушением инкапсуляции?»

Предположительно, приватный метод используется чем-то публичным. Вы можете изменить это. При этом вы изменили поведение.

...