Материал предоставлен https://it.rfei.ru

Переопределение и сокрытие методов

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

  • Описание нового метода должно иметь такую же сигнатуру метода (т.е. имя метода и параметры) и такой же возвращаемый тип.

    Если параметры в переопределенном методе должны быть неизменяемыми (final), то это остается на усмотрение подкласса. Сигнатура метода не включает модификатор final для параметров, а только их типы и порядок.

  • Новое описание метода не может иметь более ограниченный доступ к методу, но может иметь более широкий.
  • Описание нового метода может определить только, все или никакие, или подмножество классов исключений (включая их подклассы), задаваемых в оператор throws переопределяемого метода суперкласса.

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

В примере 6.3 новое описание метода getBill() в строке (5) в подклассе TubeLight имеет такую же сигнатуру и такой же возвращаемый тип, как метод в строке (2) в суперклассе Light. Новое описание задает подмножество исключений (ZeroHoursException), выбрасываемых переопределяемым методом (класс исключения invalidHoursException является суперклассом для NegativeHoursException и ZeroHoursException). Новое описание также расширяет уровень доступа (public) по сравнению с тем, который был в не переопределенном варианте (protected). Переопределенный метод также объявляет параметр как final. Вызов метода getBill() объекта подкласса TubeLight через ссылки подкласса и суперкласса в строках (12) и (13) соответственно приводит к выполнению нового описания в строке (5). Вызов метода getBill() объекта суперкласса Light через ссылку суперкласса в строке (14) приводит к выполнению переопределенного метода в строке (2).

Пример 6.3. Переопределение, перегрузка и сокрытие

// Исключения
class InvalidHoursException extends Exception {}
class NegativeHoursException extends InvalidHoursException {}
class ZeroHoursException extends InvalidHoursException {}
class Light {

    protected String billType = "Small bill";     // (1)

    protected double getBill(int noOfHours)
              throws InvalidHoursException {      // (2)
        if (noOfHours < 0)
            throw new NegativeHoursException();
        double smallAmount = 10.0,
               smallBill = smallAmount * noOfHours;
        System.out.println(billType + ": " + smallBill);
        return smallBill;
    }

    public static void printBillType() {          // (3)
        System.out.println("Small bill");
    }

}

class TubeLight extends Light {

    public static String billType = "Large bill"; // (4) скрытое статическое поле.

    public double getBill(final int noOfHours)
           throws ZeroHoursException {     // (5) переопределенный метод экземпляра.
        if (noOfHours == 0)
            throw new ZeroHoursException();
        double largeAmount = 100.0,
               largeBill = largeAmount * noOfHours;
        System.out.println(billType + ": " + largeBill);
        return largeBill;
    }

    public static void printBillType() {          // (6) Скрытый статический метод.
        System.out.println(billType);
    }

    public double getBill() {                     // (7) перегруженный метод.
        System.out.println("No bill");
        return 0.0;
    }
}

public class Client {
    public static void main(String[] args)
            throws InvalidHoursException {        // (8)

        TubeLight tubeLight = new TubeLight();    // (9)
        Light     light1    = tubeLight;          // (10) Псевдонимы.
        Light     light2    = new Light();        // (11)

        System.out.println("Invoke overridden instance method:");
        tubeLight.getBill(5);                     // (12) Вызов метода из (5)
        light1.getBill(5);                        // (13) Вызов метода из (5)
        light2.getBill(5);                        // (14) Вызов метода из (2)
        System.out.println("Access hidden field:");
        System.out.println(tubeLight.billType);   // (15) Доступ к полю в (4)
        System.out.println(light1.billType);      // (16) Доступ к полю в (1)
        System.out.println(light2.billType);      // (17) Доступ к полю в (1)

        System.out.println("Invoke hidden static method:");
        tubeLight.printBillType();                // (18) Вызов метода из (6)
        light1.printBillType();                   // (19) Вызов метода из (3)
        light2.printBillType();                   // (20) Вызов метода из (3)

        System.out.println("Invoke overloaded method:");
        tubeLight.getBill();                      // (21) Вызов метода из (7)
    }
}

Вывод программы:

Invoke overridden instance method:
Large bill: 500.0
Large bill: 500.0
Small bill: 50.0
Access hidden field:
Large bill
Small bill
Small bill
Invoke hidden static method:
Large bill
Small bill
Small bill
Invoke overloaded method:
No bill

Подкласс должен использовать ключевое слово super, чтобы вызвать переопределенный метод суперкласса.

Метод экземпляра в подклассе не может переопределить статический метод суперкласса. Компилятор отметит это как ошибку. Статический метод — это метод класса, и он не является частью никакого объекта, в то время как переопределенные методы вызываются от имени объектов подкласса. Однако статический метод в подклассе может скрыть статический метод суперкласса.

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

Модификатор доступа private для метода значит, что метод не доступен извне класса, в котором определен; поэтому подкласс не может его переопределить. Одна подкласс может дать свое собственное описание такого метода, который может иметь такую же сигнатуру, как метод в его суперклассе.

Сокрытие полей

Подкласс не может переопределить поля суперкласса, зато может их скрыть. Подкласс может определить поля с такими же именами, как в суперклассе, Если это имеет место, то к полям суперкласса нельзя будет обратиться из подкласса простым именам, поэтому они не наследуются подклассом. Код в подклассе может использовать ключевое слово super для доступа к таким членам, включая скрытые поля. Клиент может использовать ссылку на суперкласс для доступа к членам, которые скрыты в подклассе, как объясняется ниже. Конечно, если скрыто является статическим, к нему также можно обратиться по имени суперкласса.

Следует обратить внимание на следующее различие между вызовом методов экземпляра и доступностью полей. Когда вызывается метод экземпляра с помощью ссылки, важен класс текущего объекта, обозначенного ссылкой, а не тип ссылки, что определяет какая реализация метода будет выполнена. В примере 6.3 в строках (12), (13) и (14) очевиден вызов переопределенного метода getBill(): выполняется метод из класса, соответствующего текущему объекту, независимо от типа ссылки. Когда идет обращение к полю объекта, обозначенного ссылкой, играет роль тип ссылки, а не класса текущего объекта , обозначенного ссылкой, что определяет, к какому полю идет обращение. В примере 6.3 в строках (15), (16) и (17) очевидно обращение к скрытому billType: поле, к которому обращаются, объявлено в классе, соответствующем типу ссылки, несмотря на объект, который она обозначает.

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

Сокрытие статического метода

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

Связывание вызова метода с реализацией метода выполняется на этапе компиляции. только если метод имеет модификатор static или final (закрытые (private) методы неявно являются final). В примере 6.3 показан вызов статических методов. По аналогии с доступом к полям метод, вызванный в строках (18), (19) и (20), определяется классом ссылки. В строке (18) типом ссылки является класс TubeLight, поэтому вызывается статический метод printBillType() из строки (6) этого класса. В строках (19) и (20) типом ссылки является класс Light, поэтому вызывается скрытый статический метод printBillType() из строки (3) этого класса. Это показано в результирующем выводе программы.

Скрытый статический метод можно, конечно, вызвать с помощью имени суперкласса. Также можно воспользоваться ключевым словом super в нестатическом коде подкласса чтобы вызвать скрытые статические методы.

Переопределение в сравнении с перегрузкой

Переопределение (overriding) метода не следует путать с перегрузкой метода (overloading). Переопределенный метод имеет такую же сигнатуру (имя и параметры) и такой же возвращаемый тип. Для переопределения подходят только не-final-методы суперкласса, к которым можно напрямую обратиться из подкласса Перегрузка имеет место, когда имена методов совпадают, а список параметров различается. Поэтому, для того чтобы перегрузить метод, нужно, чтобы параметры различались по типу, порядку или количеству. Так как возвращаемый тип не входит в состав сигнатуры, наличия разных возвращаемых типов недостаточно для перегрузки метода.

Метод может быть перегружен в классе, в котором определен, или в подклассе своего класса. Вызов переопределяемого метода суперкласса из подкласса требует специального синтаксиса (т.е. ключевого слова super). Это не является необходимым для вызова перегружаемого метода суперкласса из подкласса. Если передается правильный состав аргументов в вызов метода, имеющий место в подклассе, то будет вызван перегружаемый метод суперкласса. В примере 6.3 метод getBill() в строке (2) из класса Light переопределяется в классе TubeLight в строке (5) и перегружается в строке (7). Когда он вызывается в строке (21), то выполняется реализация из (7).

Разрешение проблем перегрузки метода

В примере 6.4 показано, как разрешается проблема с параметрами, заключающаяся в выборе правильной реализации перегруженного метода. Метод testlfOn() перегружается в строках (1) и (2) в классе OverloadResolution. Вызов client.testlfOn(tubeLight) в строке (3) удовлетворяет спискам параметров обеих реализаций, представленных в (1) и (2), так как ссылка tubeLight, которая обозначает объект класса TubeLight, может также быть присвоена ссылке своего суперкласса Light. Выбирается наиболее специфичный метод, (2), выводящий на терминал false. Вызов client.testIfOn (light) в строке (4) удовлетворяет списку параметров только для реализации из строки (1), выводящей на терминал true.

Пример 6.4. Разрешение проблем перегрузки метода

class Light { /* ... */ }

class TubeLight extends Light { /* ... */ }

public class OverloadResolution {
    boolean testIfOn(Light aLight)         { return true; }    // (1)
    boolean testIfOn(TubeLight aTubeLight) { return false; }   // (2)
    public static void main(String[] args) {

        TubeLight tubeLight = new TubeLight();
        Light     light     = new Light();

        OverloadResolution client = new OverloadResolution();
        System.out.println(client.testIfOn(tubeLight));// (3) ==> метод в (2)
        System.out.println(client.testIfOn(light));    // (4) ==> метод в (1)

    }
}

Объектная ссылка super

Ссылка this доступна из нестатического кода, и она указывает на текущий объект. Когда вызывается метод экземпляра, то ссылка this обозначает объект, метод которого вызывается. Ключевое слово super также может использоваться из нестатического кода (т.е. в теле метода экземпляра), но только в подклассе, чтобы получить доступ к полям и методам суперкласса. Ключевое слово super предоставляет ссылку на текущий объект, как экземпляр своего суперкласса. При вызове метода с помощью ключевого слова super независимо от реального типа объекта и от того, переопределен ли метод в текущем классе, происходит просто вызов метода из суперкласса. Это обычно используется для вызова методов, которые были переопределены, и для доступа к членам, которые были скрыты в подклассе. В отличие от ключевого слова this, ключевое слово super не может использоваться как обыкновенная ссылка. Например, она не может быть присвоена другим ссылкам или преобразована к типам других ссылок.

В примере 6.5 в методе demonstrate() из строки (9) в классе NeonLight используется ключевое слово super для доступа к членам, лежащим в его иерархии наследования выше. В результате вызывается метод banner() в строке (10). Этот метод определен в строке (4) в классе Light, а не в непосредственном суперклассе подкласса NeonLight. Также с помощью ключевого слова super вызываются в строках (12) и (11) переопределенный метод getBill() и его перегруженная версия из (6) и (8) соответственно.

Класс NeonLight — это подкласс класса TubeLight, который является подклассом класса Light, имеющего поле BillType и метод getBill, определенные в строках (1) и (2) соответственно. Может возникнуть желание использовать синтаксис super.super.getBill(20) в подклассе NeonLight для вызова такого метода, но такая конструкция недопустима. Также может захотеться преобразовать ссылку this У класса Light и попробовать еще раз, как показано в строке (13). Результат работы программы показывает, что был выполнен метод getBill() из строки (6) класса TubeLight, а не из класса Light. Причина в том, что преобразование изменяет только тип ссылки (в этом случае на Light), а не класс объекта (который у нас типа NeoLight). Вызываемый метод определяется классом текущего объекта, поэтому в результате выполняется метод getBill() из класса TubeLight. He существует способа вызвать метод getBill() класса Light из подкласса NeonLight.

В строке (14) используется ключевое слово super для доступа к полю BillType в строке (5) класса TubeLight. В строке (15) успешно обращаемся к полю BillType, класса Light с помощью преобразования типа ссылки this, потому что доступ к полю определяется по типу ссылки. Из нестатического кода подкласса возможно напрямую обратиться к полям класса, лежащего выше в иерархии наследования, с помощью преобразования типа ссылки this. Однако бесполезно преобразовывать тип ссылки this для вызова методов экземпляра класса, лежащего выше в иерархии наследования, как показано выше в случае с переопределенным методом getBill().

Наконец, вызовы статических методов в строках (16) и (17) с помощью ссылок super и this обнаруживают на этапе выполнения поведение аналогичное доступу к полям, как обсуждалось ранее.

Пример 6.5. Использование ключевого слова super

// Исключения
class InvalidHoursException extends Exception {}
class NegativeHoursException extends InvalidHoursException {}
class ZeroHoursException extends InvalidHoursException {}

class Light {

    protected String billType  = "Small bill";       // (1)

    protected double getBill(int noOfHours)
              throws InvalidHoursException {         // (2)
        if (noOfHours < 0)
            throw new NegativeHoursException();
        double smallAmount = 10.0,
               smallBill = smallAmount * noOfHours;
        System.out.println(billType + ": " + smallBill);
        return smallBill;
    }

    public static void printBillType() {             // (3)
        System.out.println("Small bill");
    }

    public void banner() {                           // (4)
        System.out.println("Let there be light!");
    }
}

class TubeLight extends Light {

    public static String billType = "Large bill";    // (5) Сокрытие статического поля.

    public double getBill(final int noOfHours)
           throws ZeroHoursException {        // (6) перегрузка метода экземпляра.
        if (noOfHours == 0)
            throw new ZeroHoursException();
        double largeAmount = 100.0,
               largeBill = largeAmount * noOfHours;
        System.out.println(billType + ": " + largeBill);
        return largeBill;
    }

    public static void printBillType() {             // (7) Сокрытие статического метода.
        System.out.println(billType);
    }

    public double getBill() {                        // (8) перегрузка метода.
        System.out.println("No bill");
        return 0.0;
    }
}

class NeonLight extends TubeLight {
    // ...
    public void demonstrate()
            throws InvalidHoursException {           // (9)

        super.banner();                              // (10) Вызов метода из (4)
        super.getBill();                             // (11) Вызов метода из (8)
        super.getBill(20);                           // (12) Вызов метода из (6)
        ((Light) this).getBill(20);                  // (13) Вызов метода из (6)
        System.out.println(super.billType);          // (14) Обращение к полю из (5)
        System.out.println(((Light) this).billType); // (15) Обращение к полю из (1)
        super.printBillType();                       // (16) Вызов метода из (7)
        ((Light) this).printBillType();              // (17) Вызов метода из (3)
    }
}

public class Client {
    public static void main(String[] args)
                       throws InvalidHoursException {
        NeonLight neonRef = new NeonLight();
        neonRef.demonstrate();
    }
}

Вывод программы:

Let there be light!
No bill
Large bill: 2000.0
Large bill: 2000.0
Large bill
Small bill
Large bill
Small bill
Концепции ООПКлючевые слова this() и super()