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

  1. Что такое переопределение методов?
  2. Методы подставки
  3. Переопределение и статические методы
  4. Переопределение методов в классах наследниках
  5. Аннотация @Override

1. Что такое переопределение методов?

Если в иерархии классов совпадают имена и сигнатуры типов методов из подкласса и суперкласса, то говорят, что метод из подкласса переопределяет метод из суперкласса.

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

В следующем примере в классе M определен метод print(). В его наследнике классе N тоже определен метод print() с такой же сигнатурой, но другим поведением. Это и называется переопределением методов:

public class M {
    public int i;
    public int j;

    public M(int i, int j) {
        this.i = i;
        this.j = j;
    }

    public void print() {
        System.out.println("Метод M i = " + i + " j = " + j);
    }
}

Когда переопределенный метод вызывается из своего подкласса, он всегда ссылается на свой вариант, определенный в подклассе. А вариант метода, определенный в суперклассе, будет скрыт. Из метода someMethod() будет вызван метод того же класса N:

public class N extends M {
    public int k;

    public N(int i, int j, int k) {
        super(i, j);
        this.k = k;
    }

    public void print() {
        System.out.println("Метод N k = " + k);
    }

    public void someMethod() {
        print();
    }
}

Создадим три объекта и для каждого вызовем метод print().

Первая переменная obj1 типа M указывает на объект того же типа M. При вызове метода print() ожидаемо вызовется метод класса M. Вторая переменная obj2 типа N указывает на объект N. При вызове метода print() вызовется метод класса N. Третий вариант самый интересный - переменная obj3 типа M, но указывает на объект N. Какой же метод print() будет использоваться здесь? Выбор необходимого переопределенного метода выбирается JVM на основе ТИПА ОБЪЕКТА, а не типа переменной!!! Поэтому для переменной obj3 вызовется метод класса N.

public class OverrideDemo {
    public static void main(String[] args) {
        M obj1 = new M(7, 8);
        obj1.print();

        N obj2 = new N(4, 5, 6);
        obj2.print();

        M obj3 = new N(1, 2, 3);
        obj3.print();
    }
}

 Результат выполнения:

Метод M i = 7 j = 8
Метод N k = 6
Метод N k = 3

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

Переопределение методов это одна из форм реализации полиморфизма, который позволяет определить в общем классе методы, которые станут общими для всех производных от него классов, а в подклассах - конкретные реализации некоторых или всех этих методов.

Рассмотрим более конкретный пример, который показывает зачем переопределяются методы.

Создадим класс Figure, описывающий какую-то абстрактную фигуру и классы наследники Triangle и Rectangle. Класс Figure содержит метод area(), подсчитывающий площадь фигуры. У каждой фигуры своя формула для подсчета площади, поэтому в классах Triangle и Rectangle метод area() переопределяется соответствующим образом:

public class Figure {
    double dim1;
    double dim2;

    public Figure(double dim1, double dim2) {
        this.dim1 = dim1;
        this.dim2 = dim2;
    }

    public double area() {
        System.out.println("Площадь фигуры не определена.");
        return 0;
    }
}
public class Rectangle extends Figure {
    public Rectangle(double dim1, double dim2) {
        super(dim1, dim2);
    }

    public double area() {
        System.out.println("B области четырехугольника.");
        return dim1 * dim2;
    }
}
public class Triangle extends Figure {
    public Triangle(double dim1, double dim2) {
        super(dim1, dim2);
    }

    public double area() {
        System.out.println("B области треугольника.");
        return dim1 * dim2 / 2;
    }
}

Создадим массив типа Figure, который будет содержать объекты типа Figure, Triangle и Rectangle. Подсчитаем площадь для каждого элемента перебирая элементы массива и вызывая метод area() для каждого элемента. Нам все равно какого типа объект - у каждого есть вызываемый метод area(). JVM с помощью динамической диспетчеризации выбирает нужный вариант метода, основываясь на реальном типе объекта:

public class FindAreas {
    public static void main(String[] args) {
        Figure[] figures = new Figure[3];
        figures[0] = new Figure(10, 10);
        figures[1] = new Rectangle(10, 10);
        figures[2] = new Triangle(10, 10);
        for (Figure figure : figures) {
            figure.area();
        }
    }
}

Результат выполнения кода:

Площадь фигуры не определена.
B области четырехугольника.
B области треугольника.

2. Методы подставки

После выхода Java 5 появилась возможность при переопределении методов указывать другой тип возвращаемого значения, в качестве которого можно использовать только типы, находящиеся ниже в иерархии наследования, чем исходный тип. Такие типы еще называются ковариантными.

Например, класс S наследует класс R и переопределяет метод getInstance(). При переопределении возвращаемый тип метода может или остаться таким же - Box6, или быть изменен на любого наследника класса Box6 - HeavyBox, ColorBox или Shipment:

public class R {
    Box6 getInstance() {
        return new Box6();
    }
}
public class S extends R {
    HeavyBox getInstance() {
        return new HeavyBox();
    }
}

3. Переопределение и статические методы

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

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

Создадим в суперклассе и наследнике статические методы с одинаковой сигнатурой:

public class Base {
    public static void go() {
        System.out.println("метод из Base");
    }
}
public class Sub extends Base {
    public static void go() {
        System.out.println("метод из Sub");
    }
}

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

public class Runner {
    public static void main(String[] args) {
        Base ob = new Sub();
        ob.go();
        Sub.go();
    }
}

Результат выполнения:

метод из Base
метод из Sub

4. Переопределение методов в классах наследниках

Методы объявленные как private никто, кроме самого класса не видит. Поэтому их наличие/отсутствие никак не отражается на классах-наследниках. Они с легкостью могут объявлять методы с такой же сигнатурой и любыми модификаторами. Но это плохой тон! Также класс наследник может расширить видимость protected метода до public. Сузить видимость класс-наследник не может.  

5. Аннотация @Override

Необязательная аннотация @Override используется с методом для указания того, что он переопределен. Если метод переопределен неверно, код не скомпилируется:

public class S extends R {
    @Override
    HeavyBox getInstance() {
        return new HeavyBox();
    }
}
Read also:
Comments