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

2 min


Вступление

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

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

Когда класс Java продолжается другой, мы называем это подкласс, Тот, который продлен от становится суперкласс, Теперь основной причиной этого является то, что подкласс может использовать подпрограммы из суперкласса. Тем не менее, в других случаях подкласс может захотеть добавить дополнительную функциональность к тому, что уже имеет суперкласс.

С переопределение методанаследующие классы могут изменить то, как мы ожидаем, что тип класса будет вести себя. И как будет показано в этой статье, это является основой для одного из самых мощных и важных механизмов ООП. Это основа для полиморфизм,

Что такое метод переопределения?

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

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

  1. Одно и то же имя
  2. Такое же количество параметров
  3. Параметры того же типа
  4. Тот же или ковариантный тип возврата

Чтобы лучше понять эти условия, возьмите урок Shape, Это геометрическая фигура, которая имеет вычисляемую площадь:

abstract class Shape {
    abstract Number calculateArea();
}

Давайте тогда расширим этот базовый класс на пару конкретных классов – Triangle и Square:

class Triangle extends Shape {
    private final double base;
    private final double height;

    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    Double calculateArea() {
        return (base / 2) * height;
    }

    @Override
    public String toString() {
        return String.format(
                "Triangle with a base of %s and height of %s",
                new Object[]{base, height});
    }
}

class Square extends Shape {
    private final double side;

    Square(double side) {
        this.side = side;
    }

    @Override
    Double calculateArea() {
        return side * side;
    }

    @Override
    public String toString() {
        return String.format("Square with a side length of %s units", side);
    }
}

Помимо переопределения calculateArea() метод, два класса переопределяют Object«s toString() также. Также обратите внимание, что эти два аннотируют переопределенные методы с @Override,

Так как Shape абстрактно, Triangle и Square классы должен переопределение calculateArea(), так как абстрактный метод не предлагает реализацию.

Тем не менее, мы также добавили toString() переопределения. Метод доступен для всех объектов. И так как две фигуры находятся объекты, они могут переопределить toString(), Хотя это не является обязательным, это делает печать деталей класса удобной для человека.

И это удобно, когда мы хотим записать или распечатать описание класса при тестировании, например:

void printAreaDetails(Shape shape) {
    var description = shape.toString();
    var area = shape.calculateArea();

    // Print out the area details to console
    LOG.log(Level.INFO, "Area of {0} = {1}", new Object[]{description, area});
}

Итак, когда вы запускаете тест, такой как:

void calculateAreaTest() {
    // Declare the side of a square
    var side = 5;

    // Declare a square shape
    Shape shape = new Square(side);

    // Print out the square's details
    printAreaDetails(shape);

    // Declare the base and height of a triangle
    var base = 10;
    var height = 6.5;

    // Reuse the shape variable
    // By assigning a triangle as the new shape
    shape = new Triangle(base, height);

    // Then print out the triangle's details
    printAreaDetails(shape);
}

Вы получите этот вывод:

INFO: Area of Square with a side length of 5.0 units = 25
INFO: Area of Triangle with a base of 10.0 and height of 6.5 = 32.5

Как показывает код, желательно включить @Override обозначение при переопределении. И, как оракул объясняет, это важно, потому что это:

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

Как и когда переопределить

В некоторых случаях переопределение метода является обязательным – если вы реализуете интерфейсНапример, вы должны переопределить его методы. Тем не менее, в других, как правило, программист должен решить, будут ли они переопределять некоторые данные методы или нет.

Возьмем, к примеру, сценарий, в котором расширяется неабстрактный класс. Программист свободен (до некоторой степени) выбирать методы для переопределения из суперкласса.

Методы из интерфейсов и абстрактных классов

Возьми интерфейс, Identifiable, который определяет объект id поле:

public interface Identifiable {
    T getId();
}

T представляет тип класса, который будет использоваться для id, Итак, если мы используем этот интерфейс в приложении базы данных, T может иметь тип Integer, например. Еще одна заметная вещь заключается в том, что T является Serializable,

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

Затем, скажем, мы создаем класс, PrimaryKey, который реализует Identifiable:

class PrimaryKey implements Identifiable {
    private final int value;

    PrimaryKey(int value) {
        this.value = value;
    }

    @Override
    public Integer getId() {
        return value;
    }
}

PrimaryKey должен переопределить метод getId() из Identifiable, Это означает, что PrimaryKey имеет особенности Identifiable, И это важно, потому что PrimaryKey мог бы реализовать несколько интерфейсов.

В таком случае это было бы иметь все возможности интерфейсов, которые он реализует. Вот почему такие отношения называются “имеет” отношения в иерархиях классов.

Давайте рассмотрим другой сценарий. Может быть, у вас есть API, который предоставляет абстрактный класс, Person:

abstract class Person {
    abstract String getName();
    abstract int getAge();
}

Итак, если вы хотите воспользоваться некоторыми подпрограммами, которые работают только на Person типы, вы должны были бы расширить класс. Возьми это Customer класс, например:

class Customer extends Person {
    private final String name;
    private final int age;

    Customer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    String getName() {
        return name;
    }

    @Override
    int getAge() {
        return age;
    }
}

Расширяя Person с помощью CustomerВы вынуждены применять переопределения. Тем не менее, это только означает, что вы ввели класс, который является типа Person, Таким образом, вы ввели “это” отношения. И чем больше вы на это смотрите, тем больше таких заявлений имеет смысл.

Потому что, в конце концов, клиент является персона.

Продление не финального класса

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

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

class Coach {
    void motivateTeam() {
        throw new UnsupportedOperationException();
    }
}

Если Coach не объявлен окончательным, вам повезло. Вы можете просто расширить его, чтобы создать CricketCoach кто может оба analyzeGame() и motivateTeam():

class CricketCoach extends Coach {
    String analyzeGame() {
        throw new UnsupportedOperationException();
    }

    @Override
    void motivateTeam() {
        throw new UnsupportedOperationException();
    }
}

Продление финального класса

Наконец, что бы произошло, если бы мы продлили final учебный класс?

final class CEO {
    void leadCompany() {
        throw new UnsupportedOperationException();
    }
}

И если бы мы попытались повторить CEOфункциональность через другой класс, скажем, SoftwareEngineer:

class SoftwareEngineer extends CEO {}

Нас встретит неприятная ошибка компиляции. Это имеет смысл, так как final Ключевое слово в Java используется, чтобы указать на вещи, которые не должны меняться.

Вы не может продлить final учебный класс.

Как правило, если класс не предназначен для расширения, он помечается как finalтак же, как переменные. Хотя там является Обходной путь, если вы должен идти против первоначального намерения класса и расширить его – до некоторой степени.

Создание класса-оболочки, который содержит экземпляр final Класс, который предоставляет вам методы, которые могут изменить состояние объекта. Тем не менее, это работает, только если обернутый класс реализует интерфейс, который означает, что мы можем предоставить обертку вместо final класс вместо

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

Популярный пример final класс это String учебный класс. это final и поэтому неизменный, Когда вы выполняете «изменения» в строке с помощью любого из встроенных методов, новый String создается и возвращается, создавая иллюзию перемен:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }

    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

Переопределение метода и полиморфизм

Merriam-Webster Словарь определяет полиморфизм как:

Качество или состояние существующих или предполагаемых различных форм

Переопределение методов позволяет нам создавать такую ​​функцию в Java. Как Shape показанный пример, мы можем запрограммировать его для вычисления областей для различных типов фигур.

И что еще более важно, нам даже не важно, каковы фактические реализации форм. Мы просто называем calculateArea() метод на любую форму. Именно конкретный класс формы определяет, какую область он будет предоставлять, в зависимости от его уникальной формулы.

Полиморфизм решает многие подводные камни, связанные с неадекватными ООП-проектами. Например, мы можем вылечить антишаблоны, такие как чрезмерные условные выражения, помеченные классы и служебные классы. Создавая полиморфные иерархии, мы можем уменьшить необходимость для этих анти-паттернов.

Conditionals

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

Рассмотрим два класса ниже, которые описывают звуки, которые Dog и Cat сделать:

class Dog {
    String bark() {
        return "Bark!";
    }

    @Override
    public String toString() {
        return "Dog";
    }
}

class Cat {
    String meow() {
        return "Meow!";
    }

    @Override
    public String toString() {
        return "Cat";
    }
}

Затем мы создаем метод makeSound() чтобы заставить этих животных издавать звуки:

void makeSound(Object animal) {
    switch (animal.toString()) {
        case "Dog":
            LOG.log(Level.INFO, ((Dog) animal).bark());
            break;
        case "Cat":
            LOG.log(Level.INFO, ((Cat) animal).meow());
            break;
        default:
            throw new AssertionError(animal);
    }
}

Теперь типичный тест для makeSound() было бы:

void makeSoundTest() {
    var dog = new Dog();
    var cat = new Cat();

    // Create a stream of the animals
    // Then call the method makeSound to extract
    // a sound out of each animal
    Stream.of(dog, cat).forEach(animal -> makeSound(animal));
}

Который затем выводит:

INFO: Bark!
INFO: Meow!

Хотя приведенный выше код работает, как и ожидалось, он, тем не менее, показывает плохой дизайн ООП. Таким образом, мы должны реорганизовать его, чтобы ввести Animal учебный класс. Это тогда назначит создание звука его конкретным классам:

abstract class Animal {
    // Assign the sound-making
    // to the concrete implementation
    // of the Animal class
    abstract void makeSound();
}

class Dog extends Animal {
    @Override
    void makeSound() {
        LOG.log(Level.INFO, "Bark!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        LOG.log(Level.INFO, "Meow!");
    }
}

Тест ниже показывает, насколько просто стало использовать класс:

void makeSoundTest() {
    var dog = new Dog();
    var cat = new Cat();

    // Create a stream of animals
    // Then call each animal's makeSound method
    // to produce each animal's unique sound
    Stream.of(dog, cat).forEach(Animal::makeSound);
}

У нас больше нет отдельного makeSound Метод, как и прежде, чтобы определить, как извлечь звук из животного. Вместо этого каждый конкретный Animal класс переопределен makeSound ввести полиморфизм. В результате код читабелен и краток.

Если вы хотите больше узнать о лямбда-выражениях и ссылках на методы, показанных в примерах кода выше, мы предоставим вам информацию!

Утилиты

Служебные классы распространены в проектах Java. Они обычно выглядят примерно так java.lang.Math«s min() метод:

public static int min(int a, int b) {
    return (a <= b) ? a : b;
}

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

Как и в сценарии условных вычислений, мы должны изменить классы утилит, чтобы ввести полиморфизм. И отличной отправной точкой было бы найти общее поведение в служебных методах.

Возьмите min() метод в Math служебный класс, например. Эта рутина стремится вернуть int значение. Он также принимает два int значения в качестве ввода. Затем он сравнивает два, чтобы найти меньший.

Итак, по сути, min() показывает нам, что нам нужно создать класс типа Number - для удобства назван Minimum,

На Яве Number класс абстрактный И это хорошо. Потому что это позволит нам переопределить методы, которые относятся только к нашему случаю.

Это, например, даст нам возможность представить минимальное количество в различных форматах. Кроме того intмы могли бы также предложить минимум как long, floatили double, В результате Minimum класс может выглядеть так:

public class Minimum extends Number {

    private final int first;
    private final int second;

    public Minimum(int first, int second) {
        super();
        this.first = first;
        this.second = second;
    }

    @Override
    public int intValue() {
        return (first <= second) ? first : second;
    }

    @Override
    public long longValue() {
        return Long.valueOf(intValue());
    }

    @Override
    public float floatValue() {
        return (float) intValue();
    }

    @Override
    public double doubleValue() {
        return (double) intValue();
    }
}

В фактическом использовании синтаксическая разница между Math«s min и Minimum значительно:

// Find the smallest number using
// Java's Math utility class
int min = Math.min(5, 40);

// Find the smallest number using
// our custom Number implementation
int minimumInt = new Minimum(5, 40).intValue();

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

Некоторые найдут это более читабельным, а некоторые найдут предыдущий подход более читабельным.

Переопределение против перегрузки

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

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

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

Тем не менее, переопределение лучше, чем перегрузка, когда дело доходит до реализации полиморфизма. С перегрузкой вы рискуете создать трудно читаемые API. Напротив, переопределение заставляет человека принять иерархию классов. Это особенно полезно, потому что они заставляют программистов разрабатывать для ООП.

Таким образом, перегрузка и переопределение отличаются в следующих отношениях:

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

Вывод

Переопределение метода является неотъемлемой частью представления мышц ООП Java. Он цементирует иерархии классов, позволяя подклассам обладать и даже расширять возможности своих суперклассов.

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

Например, вам рекомендуется переопределить toString() метод из класса Object, И эта статья показала такую ​​практику, когда она вышла из-под контроля toString() для Shape типы - Triangle и Square,

Наконец, поскольку переопределение методов сочетает в себе наследование и полиморфизм, оно является отличным инструментом для удаления распространенных запахов кода. Такие проблемы, как чрезмерные условия и классы полезности, могут стать менее распространенными благодаря разумному использованию переопределения.

Как всегда, вы можете найти весь код на GitHub,


0 Comments

Ваш адрес email не будет опубликован. Обязательные поля помечены *