Перегрузка и переопределение методов в Java: примеры
Метод в Java — это функция, которая определяет, что умеет делать объект этого класса. Одна из главных задач методов — выполнение действий над данными объекта. Они могут менять значение, преобразовывать данные, выводить их в консоль.
Методы можно перегружать и переопределять. Как это делать и в чём разница между этими двумя механизмами — разберёмся в этой статье.
Перегрузка метода
Перегрузка методов в Java — это использование одного имени метода с разными параметрами.
Чтобы разобраться с этим механизмом, начнём с простого — создадим помощника, который будет здороваться с пользователями.
public class Assistant {
public void sayHello(String name) {
System.out.println("Добрый день, " + name + "!");
}
public static void main(String[] args) {
Assistant assistant = new Assistant();
assistant.sayHello("Михаил");
}
}
В консоли будет выведена фраза «Добрый день, Михаил!».
Допустим, Михаил пришёл не один, а с другом Виталием. Сейчас метод реализован так, что помощник поприветствует только Михаила, а Виталия проигнорирует. Чтобы исправить это, реализуем в классе два метода. Имя у них будет одинаковое. Но параметры они принимают разные.
public class Assistant {
public void sayHello(String firstGuest) {
System.out.println("Добрый вечер, " + firstGuest + "!");
}
public void sayHello(String firstGuest, String secondGuest) {
System.out.println("Добрый день, " + firstGuest + " и " + secondGuest + "!");
}
public static void main(String[] args) {
Assistant assistant = new Assistant();
assistant.sayHello("Михаил", "Виталий");
}
}
Теперь в консоли отобразится фраза «Добрый день, Михаил и Виталий!».
Мы уже перегрузили sayHello(). Теперь программа стала более гибкой — помощник может приветствовать сразу двух гостей. Но что произойдёт, если придут трое, четверо или пятеро? Проверим:
public class Assistant {
public void sayHello(String firstGuest) {
System.out.println("Добрый вечер, " + firstGuest + "!");
}
public void sayHello(String firstGuest, String secondGuest) {
System.out.println("Добрый день, " + firstGuest + " и " + secondGuest + "!");
}
public static void main(String[] args) {
Assistant assistant = new Assistant();
assistant.sayHello("Михаил", "Виталий", "Марина");
}
}
В ответ получим ошибку, потому что sayHello() готов принимать только два аргумента. Решение в лоб — перегружать его дальше. Сделать так, чтобы .sayHello() принимал троих, четверых, пятерых и больше гостей. Но это не похоже на гибкую работу программы. Придётся постоянно дописывать код.
Более гибкое решение — передать в качестве параметра аргумент переменной длины (String… names). Это позволит sayHello() принимать любое количество строк. А чтобы выводить в консоль приветствие каждого гостя, используем цикл.
public class Assistant {
public void sayHello(String firstGuest) {
System.out.println("Добрый вечер, " + firstGuest + "!");
}
public void sayHello(String… names) {
for (String name: names) {
System.out.println("Добрый вечер, " + name + "!");
}
}
public static void main(String[] args) {
Assistant assistant = new Assistant();
assistant.sayHello("Михаил", "Виталий", "Марина", "Андрей", "Анна");
}
В консоли отобразится приветствие каждого переданного гостя:
Добрый вечер, Михаил!
Добрый вечер, Виталий!
Добрый вечер, Марина!
Добрый вечер, Андрей!
Добрый вечер, Анна!
Порядок аргументов
В примере выше мы не думали о порядке аргументов, потому что все они были строками. Нет разницы, с кем здороваться сначала — с Михаилом или с Анной.
Но порядок аргументов имеет значение, если метод принимает, например, строку и число. Посмотрите:
public class User {
public static void sayYourAge(String greeting, int age) {
System.out.println(greeting + " " + age);
}
public static void main(String[] args) {
sayYourAge(20, "Мой возраст - "); //ошибка!
}
}
Здесь вернётся ошибка, потому что при определении sayYourAge() мы задали, что сначала должна быть строка, а затем — число, но аргументы передали в обратном порядке.
Чтобы исправить ошибку, достаточно передать аргументы в правильном порядке:
public class User {
public static void sayYourAge(String greeting, int age) {
System.out.println(greeting + " " + age);
}
public static void main(String[] args) {
sayYourAge("Мой возраст - ", 20);
}
}
Чтобы избежать ошибок, можно сделать перегрузку порядком параметров. Например, вот так:
public class User {
public static void sayYourAge(String greeting, int age) {
System.out.println(greeting + " " + age);
}
public static void sayYourAge(int age, String greeting) {
System.out.println(greeting + " " + age);
}
public static void main(String[] args) {
sayYourAge("Мой возраст - ", 20);
sayYourAge(20, "Мой возраст - ");
}
}
Теперь не имеет значения, в каком порядке передавать аргументы — оба варианты будут понятны программе.
Варианты перегрузки
Из примеров выше можно выделить три варианта перегрузки.
- По количеству параметров.
public class Calculator {
void calculate(int number1, int number2) { }
void calculate(int number1, int number2, int number3) { }
}
- По типам параметров:
public class Calculator {
void calculate(int number1, int number2) { }
void calculate(double number1, double number2) { }
}
- По порядку параметров:
public class Calculator {
void calculate(double number1, int number2) { }
void calculate(int number1, double number2) { }
}
Напоследок повторим, что означает перегрузка метода в Java. Это механизм языка, который позволяет создавать несколько методов с одинаковым названием, но разными параметрами. Так можно делать не во всех языках.
Перегрузка — это часть полиморфизма, одной из ключевых составляющих объектно-ориентированного программирования. Главный плюс перегрузки в Java — можно использовать схожие методы с одинаковыми именами.
Переопределение метода
Переопределение метода в Java позволяет взять метод родительского класса и создать специфическую реализацию в классе-наследнике.
Проще понять на примере. Допустим, вы создаёте класс Animal с методом voice(). Он нужен для того, чтобы животное могло подать голос:
public class Animal {
public void voice() {
System.out.println("Говори!");
}
}
И сразу возникает проблема — все животные издают разные звуки. Можно создать для каждого отдельный метод. Например, у кошки это будет voiceCat(), а у собаки — voiceDog(). Но представьте, сколько строк кода придётся написать, чтобы дать возможность всем животным подать голос?
Здесь на помощь и приходит механизм переопределения в Java. Он позволяет заменить реализацию в классе-наследнике. Посмотрим на примере кошки и собаки:
public class Cat extends Animal {
@Override
public void voice() {
System.out.println("Мяу!");
}
}
public class Dog extends Animal {
@Override
public void voice() {
System.out.println("Гав!");
}
}
В выводе отобразится сначала «Мяу», а затем — «Гав». Чтобы добиться такого результата, нужно:
- В классе-наследнике создать метод с таким же именем, как в родительском классе.
- Добавить перед ним аннотацию @Override (с английского переводится как «переопределён»). Эта аннотация сообщит компилятору, что это не ошибка, вы намеренно переопределяете метод.
Собственная реализация пишется для каждого класса-наследника. Если этого не сделать, то будет использована реализация родительского класса.
Даже после переопределения вы можете обратиться к методу родительского класса при условии, что он не определён модификатором private. Для этого используется ключевое слово super:
super.method();
Ограничения при переопределении
У переопределения методов класса в Java есть ряд ограничений.
- Аргументы должны оставаться такими же, как у метода родителя.
- Тип возвращаемого значения должен быть таким же, как у метода родителя.
- Модификатор доступа должен быть таким же, как у метода родителя.
- Окончательные методы (final) нельзя переопределять. Это один из способов запрета переопределения — объявить метод с помощью ключевого слова final.
Например:
class Parent {
final void show() {}
}
class Child extends Parent {
void show() {}
}
Такой код вернёт ошибку, потому что в родительском классе использовано ключевое слово final.
- Статические методы (static) нельзя переопределять. Если вы определите в классе-наследнике такую же сигнатуру метода, как в родительском классе, то выполните сокрытие. Подробнее об это вы можете прочитать в документации.
- Приватные методы (private) нельзя переопределять, так как они связываются на этапе компиляции, а не выполнения.
- Нельзя сужать модификатор доступа — например, с public до private. Расширение уровня доступа возможно.
- Нельзя менять тип возвращаемого значения, однако можно сузить возвращаемое значение, если они совместимы.
Переопределение класса в Java подчиняется указанным выше правилам, которые необходимо соблюдать.
Собственные правила переопределения есть у отдельных методов. Например, equals() и hashCode(). Самое важное условие — если вы переопределяете equals(), то должны переопределить и hashCode(). В противном случае классы и методы, которые пользуются контрактами стандартной реализации этих двух методов, могут работать с ошибками. Подробнее от этом мы рассказали в отдельной статье.
Заключение
Переопределение и перегрузка методов в Java — важные части полиморфизма, однако это разные механизмы. При перегрузке вы создаёте внутри одного класса много методов с одинаковым названием, но разными параметрами. При переопределении вы берёте один и тот же метод и заставляете его делать разные вещи в зависимости от того, в каком классе его вызвали.
Но есть характеристики, в которых перегрузка и переопределение Java похожи. Оба механизма помогают сделать код чище и читабельнее, а также уменьшить количество ошибок при выполнении программ.