Может ли переопределяющий метод иметь другой спецификатор доступа, чем в базовом классе?

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

public abstract class A {

    ??? void method();
}

public class B extends A {
    @Override
    public void method(){
        // TODO
    }
}

public class C extends B {
    @Override
    private void method(){
        // TODO
    }
}

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

// Will work
A foo = new B()
foo.method();

// Compiler ?
A foo = new C();
foo.method();

Но, может быть, есть другой способ. Как я могу этого добиться?


person jam    schedule 12.03.2016    source источник
comment
Вы можете сделать переопределенный финал, но не закрытый. Также вы можете сделать его устаревшим и выдать исключение неподдерживаемой операции (guava делает это для неизменяемых коллекций).   -  person Paweł Prażak    schedule 13.03.2016


Ответы (6)


Можно ослабить ограничение, но не сделать его более строгим:

public abstract class A {
    protected void method();
}

public class B extends A {
    @Override
    public void method(){    // OK
    }
}

public class C extends A {
    @Override
    private void method(){    // not allowed
    }
}

Сделать исходный метод private тоже не получится, так как такой метод не виден в подклассах и, следовательно, не может быть переопределен.

Я бы рекомендовал использовать interfaces для выборочного раскрытия или скрытия метода:

public interface WithMethod {
    // other methods
    void method();
}

public interface WithoutMethod {
    // other methods
    // no 'method()'
}

public abstract class A {
    protected void method();
}

public class B extends A implements WithMethod {
    @Override
    public void method(){
      //TODO
    }
}

public class C extends B implements WithoutMethod {
    // no 'method()'
}

... тогда работайте с инстансами только через интерфейсы.

person Jiri Tousek    schedule 12.03.2016
comment
разве я не должен использовать только интерфейсы и отказаться от абстрактного класса? - person jam; 12.03.2016
comment
Разве в вашем примере кода не будет ли экземпляр класса C по-прежнему иметь method() из класса B? С C extends B. Добавление implements WithoutMethod для класса C не может удалить method() из класса C. Верно? Итак, работайте только с экземплярами через интерфейсы. просто давайте притворимся, что C не имеет method()? - person Tersosauros; 12.03.2016
comment
Хм... Интересная идея, но... Таким образом, вы можете просто сделать WithMethod расширением WithoutMethod. Тогда класс C будет неявно реализовывать WithoutMethod. Однако, как и класс B, который может быть или не быть тем, чего хочет OP в первую очередь. - person Sergei Tachenov; 12.03.2016
comment
@Tersosauros Вы правы, в этом случае у C все еще есть метод, но если вы предоставляете его пользователю только как интерфейс WithoutMethod, у пользователя нет доступа к этому методу. В небольших проектах, делая всю иерархию классов (A, B и C) приватной для пакета и открывая только интерфейсы, можно гарантировать, что пользователь не получит код против фактического класса. В более крупном проекте вы можете разделить API и реализацию на два модуля (JAR) и сделать API доступным только во время компиляции. - person Jiri Tousek; 12.03.2016
comment
@SergeyTachenov Точно! Можно было бы делать всевозможные подобные вещи... И даже с разделением JAR, как предлагает @Jiri Tousek, все еще существуют небезопасные среды (т.е. некоторые коллекции, а также такие вещи, как RMI и т. д.), где требуется casting/reflection - и тогда весь этот хак (потому что это действительно то, что есть) сломается. Не говоря уже о таких вещах, как самоанализ! Я думаю, что НАМНОГО лучше просто бросить UnsupportedOperationException или подобное, как говорит @Konstantin Yovkov в ответе ниже. - person Tersosauros; 12.03.2016
comment
@Tersosauros, создание исключения - хорошая идея, только если оно задокументировано в самом верхнем классе, в котором этот метод в первую очередь представлен. То есть он должен быть явно помечен как необязательный, как и некоторые методы в библиотеке коллекций. В противном случае это будет настоящей болью для любого, кто неожиданно поймает это исключение, без каламбура. - person Sergei Tachenov; 12.03.2016
comment
@SergeyTachenov Верно. Очевидно, что вы не захотите throw UnsupportedOperationException, если вы явно не отметили метод (т.е. throws UnsupportedOperationException) и задокументировали его (JavaDoc и т. д.). Но, не зная больше о ситуации с OP, мы можем только догадываться о целесообразности подхода исключения. Если A - это какой-то существующий класс, то это может быть недостижимо. Однако, если OP имеет полный контроль над источником, то изменение подписи method() в A (и ниже) позволит подклассам не реализовывать ее (их реализация throws). - person Tersosauros; 12.03.2016
comment
@Tersosauros: если вы расширяете существующий класс, вы должны предоставить все его методы, иначе вы нарушите LSP. - person Kevin; 12.03.2016
comment
@Кевин Да, точно! Итак, как я сказал в своем первом комментарии: класс C всегда будет иметь method(), потому что B реализует его. - person Tersosauros; 13.03.2016
comment
@Tersosauros: ты упускаешь мою мысль. Если у вас есть существующий класс, предоставляющий method() и не вызывающий генерацию, вы также не можете генерировать генерацию или вы нарушаете LSP. - person Kevin; 13.03.2016
comment
@Кевин Как так? Если класс A имеет abstract method() throws UnsupportedOperationException, а класс B является подклассом класса A, то B должен иметь @Override method(). Но эта реализация method() не должна должна вызывать исключение UnsupportedOperationException, но она все равно должна быть помечена как throws UnsupportedOperationException, чтобы сохранить ту же сигнатуру метода, что и суперкласс. Затем класс C может стать подклассом B и иметь реализацию @Override method(), которая ДЕЛАЕТ throw UnsupportedOperationException. Ничего в этом нигде не нарушило бы LSP? - person Tersosauros; 13.03.2016
comment
@Tersosauros: я говорил о противоположной ситуации: A не бросает (и является чужим классом, так что вы не можете просто изменить это), а B хочет бросать, но не может из-за LSP. - person Kevin; 13.03.2016
comment
@Kevin Правда, как я уже сказал в своем предыдущем комментарии: Если A — это какой-то существующий класс, то это может быть недостижимо. Однако, поскольку UnsupportedOperationException — это не проверено, это будет по-прежнему возможно (зло, но возможно). - person Tersosauros; 13.03.2016
comment
Вы можете сделать переопределенный метод окончательным - person Paweł Prażak; 13.03.2016

При переопределении методов вы можете изменить модификатор только на более широкий, а не наоборот. Например, этот код будет действительным:

public abstract class A {

    protected void method();
}

public class B extends A {
    @Override
    public void method() { }
}

Однако, если вы попытаетесь сузить видимость, вы получите ошибку времени компиляции:

public abstract class A {
    protected void method();
}

public class B extends A {
    @Override
    private void method() {}
}

В вашем случае я бы предложил сделать C не реализующим A, поскольку абстракция A подразумевает, что существует незащищенный method():

public class C {
    private void method(){
      //TODO
    }
}

Другой вариант — сделать реализацию method() в C с выдачей RuntimeException:

public class C extends A {

    @Override
    public void method(){
        throw new UnsupportedOperationException("C doesn't support callbacks to method()");
    }
}
person Konstantin Yovkov    schedule 12.03.2016
comment
если бы я сделал метод в A protected и в C protected, он бы работал, и доступ извне пакета или подкласса был бы невозможен? - person jam; 12.03.2016
comment
Да, но он хочет, чтобы это было конфиденциально. - person Konstantin Yovkov; 12.03.2016

То, о чем вы просите, невозможно по очень веским причинам.

принцип подстановки Лисков в основном гласит: класс S является подклассом другого класса T только тогда, когда можно заменить любое вхождение какого-то "Т-объекта" с каким-то "S-объектом" - не замечая.

Если вы допустите, что S сводит общедоступный метод к частному, вы больше не сможете этого делать. Потому что внезапно этот метод, который можно было вызвать при использовании некоторого T ..., больше не доступен для вызова на S.

Короче говоря: наследование — это не то, что просто падает с неба. Это свойство классов, за которое отвечает вы как программист. Другими словами: наследование означает больше, чем просто запись «класс S расширяет T» в вашем исходном коде!

person GhostCat    schedule 12.03.2016
comment
LSP требует, чтобы методы базового класса существовали и имели определенную функцию во всех производных классах. Это не требует, чтобы функция действительно была полезной во всех производных классах. Например, для контракта производного класса было бы правомерно указать, что некоторое виртуальное свойство базового класса всегда будет возвращать значение false для всех экземпляров этого производного класса. В таких случаях может быть вполне разумно скрыть этот член в производном классе, поскольку не будет причин вызывать его по известной ссылке для идентификации объекта производного класса. - person supercat; 12.03.2016
comment
Вы сами сказали: метод должен существовать в производном классе. Сделать его приватным — это то же самое, что полностью удалить его извне. И, конечно же, вы можете делать с S все, что позволяет S; но в этом весь смысл прозрачного обмена; вы обращаетесь к S как к T. Таким образом, я не уверен, что вы пытаетесь сказать. - person GhostCat; 12.03.2016
comment
Если получатели ссылки на экземпляр какого-либо класса могут захотеть использовать его метод Wizzle(), если он у него есть, или же выполнить какую-то другую последовательность методов, которые могут достичь того же эффекта, но менее эффективно, а некоторые производные смогут Wizzle (), а другие нет, может быть полезно, чтобы базовый класс включал свойство CanWizzle() и метод Wizzle(), с условием, что если CanWizzle() возвращает значение true, метод Wizzle() вернет что-то полезное, и если CanWizzle() возвращает false, возможно, нет. Производный класс, который не может Wizzle()... - person supercat; 12.03.2016
comment
... не будет причин разрешать вызов метода Wizzle() для ссылок, которые, как известно, идентифицируют экземпляры этого производного класса (поскольку такой вызов не может работать). - person supercat; 12.03.2016
comment
Может быть, так. Но такие обсуждения не делают таковыми в таких общих чертах. Как бы я тогда спросил: а если какая-нибудь из ваших рюшечек умеет свистеть; а другие не могут; в чем тогда смысл общего базового класса? - person GhostCat; 12.03.2016
comment
В качестве простого примера рассмотрим базовый класс, представляющий последовательность элементов. Некоторые реализации такого базового класса могли бы эффективно обращаться к N-му элементу для любого N; другие не будут. Предположим, получатель ссылки базового класса на такую ​​вещь нуждается во многих элементах в диапазоне от 50 000 до 50 999. Если элемент не поддерживает случайное чтение, потребителю лучше прочитать 51 000 элементов, игнорируя первые 50 000. Однако если элемент поддерживает случайное чтение, потребителю будет лучше просто прочитать интересующие его элементы. - person supercat; 12.03.2016
comment
Наличие возможности чтения элементов в произвольной последовательности в качестве части базового класса вместе со свойством, указывающим, может ли объект обрабатывать такое чтение, означает, что клиентский код, который знает, как использовать эту возможность, может использовать ее для повышения производительности, оставаясь при этом стабильным. может работать с предметами, которые не могут его поддерживать. Кроме того, элементы-оболочки могут предоставлять своим клиентам любую комбинацию возможностей, поддерживаемых объектами-оболочками, что было бы очень сложно или невозможно, если бы система типов была единственным средством передачи возможностей или их отсутствия. - person supercat; 12.03.2016

Это невозможно из-за полиморфизма. Рассмотрим следующее. У вас есть метод в классе A с некоторым модификатором доступа, отличным от private. Почему не частный? Потому что если бы он был частным, то ни один другой класс не мог бы даже знать о его существовании. Значит, это должно быть что-то другое, и это что-то должно быть доступно откуда-то.

Теперь давайте предположим, что вы передаете экземпляр класса C куда-то. Но вы заранее преобразовали его в A, и в итоге у вас есть этот код где-то:

void somewhereMethod(A instance) {
    instance.method(); // Ouch! Calling a private method on class C.
}

Хороший пример того, как это сломалось, — QSaveFile в Qt. В отличие от Java, C++ фактически позволяет снизить привилегии доступа. Так они и сделали, запретив метод close(). В итоге они получили подкласс QIODevice, который на самом деле больше не является QIODevice. Если вы передадите указатель на QSaveFile какому-то методу, принимающему QIODevice*, они все равно смогут вызывать close(), поскольку он общедоступен в QIODevice. Они «исправили» это, заставив QSaveFile::close() (приватный) вызывать abort(), поэтому, если вы сделаете что-то подобное, ваша программа немедленно рухнет. Не очень красивое «решение», но лучшего нет. И это просто пример плохого ООП-дизайна. Вот почему Java не позволяет этого.

Изменить

Не то чтобы я упустил из виду, что ваш класс абстрактный, но я также упустил тот факт, что B extends C, а не A. Таким образом, то, что вы хотите сделать, совершенно невозможно. Если метод public в B, он также будет общедоступным во всех подклассах. Единственное, что вы можете сделать, это задокументировать, что его не следует вызывать и, возможно, переопределить его, чтобы он вызывал UnsupportedOperationException. Но это привело бы к тем же проблемам, что и с QSaveFile. Помните, что пользователи вашего класса могут даже не знать, что это экземпляр C, поэтому у них даже не будет возможности прочитать его документацию.

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

person Sergei Tachenov    schedule 12.03.2016

Вот часть @Override контракта.

Ответ таков: нет никакой возможности достичь того, что у вас есть.

Уровень доступа не может быть более строгим, чем уровень доступа переопределенного метода. Например: если метод суперкласса объявлен общедоступным, то переопределяющий метод в подклассе не может быть ни закрытым, ни защищенным.

Это проблема не только abstract классов, но и всех классов и методов.

person Yassin Hajaj    schedule 12.03.2016

ТЕОРИЯ:

У вас есть определенный порядок модификаторов:

public <- protected <- default-access X<- private

Когда вы переопределяете метод, вы можете увеличить, но не уменьшить уровень модификатора. Например,

public -> []
protected -> [public]
default-access -> [public, default-access]
private -> []

ПРАКТИКА:

В вашем случае вы не можете превратить ??? в какой-либо модификатор, потому что private является самым низким модификатором, а private члены класса не наследуются.

person Andrew Tobilko    schedule 12.03.2016
comment
Это неверно, хотя и является широко распространенным мифом. На самом деле правильный порядок public -> protected -> default-access -> private. На самом деле вы можете получить доступ к защищенным членам из других классов в том же пакете и из подклассов в других пакетах, в то время как доступ по умолчанию обеспечивает только первое. См. JLS 8.4.8.3. . - person Sergei Tachenov; 12.03.2016