$ sudo teach IT
МОДУЛЬ 6 · УРОК 3

Перегрузка методов (overloading)

Одинаковое имя — разные параметры. Как Java выбирает нужную версию метода

~25 минут Для новичков Java

Что такое перегрузка методов?

Представьте, что вы звоните в ресторан. Вы говорите «принесите мне еду» — вам приносят полное блюдо. Вы говорите «принесите мне еду, но быстро» — вам приносят то же блюдо, но с приоритетом. Вы говорите «принесите мне еду на вынос» — вам упаковывают то же блюдо в контейнер. Всё время звучит одно и то же слово «еда», но в зависимости от того, какие дополнительные уточнения вы даёте, ресторан по-разному выполняет вашу просьбу.

В программировании перегрузка методов (method overloading) работает точно так же. Это когда в одном классе существует несколько методов с одинаковым именем, но с разным набором параметров. Компилятор Java сам разбирается, какой именно метод вы имеете в виду, глядя на то, какие аргументы вы передаёте при вызове.

Давайте посмотрим на простой пример. Допустим, у нас есть класс Calculator, и мы хотим создать метод для сложения чисел. Но числа бывают разные — целые, с плавающей точкой, а может, мы хотим складывать сразу три числа. Вместо того чтобы изобретать новые имена вроде sumInt, sumDouble, sumThree, мы можем просто несколько раз объявить метод с именем sum:

public class Calculator {

    public int sum(int a, int b) {
        return a + b;
    }

    public double sum(double a, double b) {
        return a + b;
    }

    public int sum(int a, int b, int c) {
        return a + b + c;
    }
}

Три метода — три версии. Все они называются sum, но различаются параметрами. Когда вы пишете sum(3, 5), компилятор видит два целых числа и вызывает первую версию. Когда вы пишете sum(2.5, 3.7), компилятор видит два дробных числа и вызывает вторую версию. Когда вы пишете sum(1, 2, 3) — компилятор видит три аргумента и вызывает третью версию.

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

Обратите внимание на важный момент: перегружать методы можно только в пределах одного класса (или между методами и конструкторами). Вы не можете взять два разных класса и объявить в них методы с одинаковым именем и сказать «это перегрузка». Нет — перегрузка работает внутри одного класса.

Зачем нужна перегрузка? Удобство API и читаемость кода

Зачем вообще городить несколько методов с одинаковым именем? Почему бы не дать каждому методу своё уникальное имя? Это логичный вопрос, и ответ на него связан с тем, как мы, люди, думаем и воспринимаем информацию.

Представьте, что вы пользуетесь сервисом доставки еды. В приложении есть кнопка «Заказать». Вы нажимаете «Заказать» — и приложение понимает, что вы хотите сделать, исходя из контекста. Вам не нужно выбирать между «Заказать_для_себя», «Заказать_для_друга», «Заказать_на_вынос». Просто «Заказать» — и система разберётся. Это удобно.

Тот же принцип действует в программировании. Когда вы видите метод print("Привет"), вы сразу понимаете — «надо что-то вывести на экран». Вам не нужно запоминать десять разных имён для десяти разных вариантов вывода. Вместо этого вы просто меняете параметры:

System.out.println("Привет");
System.out.println(42);
System.out.println(3.14);
System.out.println(true);

Это и есть перегрузка! Метод println имеет множество версий: для строки, для целого числа, для дробного числа, для логического значения. Вы не думаете о том, какой именно метод вызываете — вы просто передаёте то, что хотите вывести, и Java сама находит нужную версию.

Без перегрузки пришлось бы писать что-то вроде:

System.out.printlnString("Привет");
System.out.printlnInt(42);
System.out.printlnDouble(3.14);
System.out.printlnBool(true);

Вы согласны, что второй вариант выглядит намного хуже? Придётся запоминать суффиксы, код становится шумным и трудночитаемым. Перегрузка решает эту проблему — одинаковое имя, разные параметры, и вы просто专注于业务逻辑, не думая о технических деталях.

Другой пример из реальной жизни — функция Math.max в Java. Она определена для двух целых чисел, для двух дробных чисел, для двух чисел типа long и для двух чисел типа float:

int a = 5;
int b = 10;
int result1 = Math.max(a, b);

double x = 3.14;
double y = 2.71;
double result2 = Math.max(x, y);

long big1 = 1000000L;
long big2 = 2000000L;
long result3 = Math.max(big1, big2);

Одно и то же имя max — разные типы данных. Программисту не нужно думать о том, какой тип он использует. Просто «найди максимум» — и компилятор сам подберёт нужную версию. Это делает API (программный интерфейс) интуитивно понятным и удобным для использования.

Итого по пользе перегрузки:

  • Код читается проще — одно имя вместо множества
  • API становится удобнее — пользователю не нужно запоминать много имён
  • Код становится выразительнее — имя метода описывает действие, параметры уточняют детали
  • Легче запоминается — вы запоминаете одно имя, а не десять

Перегрузка по количеству параметров

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

Рассмотрим класс Printer, который печатает сообщения. Один метод печатает одно сообщение, другой — два, третий — три:

public class Printer {

    public void print(String message) {
        System.out.println(message);
    }

    public void print(String message1, String message2) {
        System.out.println(message1 + " " + message2);
    }

    public void print(String message1, String message2, String message3) {
        System.out.println(message1 + " " + message2 + " " + message3);
    }
}

Теперь можно использовать эти методы:

Printer printer = new Printer();
printer.print("Привет");
printer.print("Привет", "мир");
printer.print("Привет", "новый", "мир");

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

Ещё один полезный пример — метод для создания пользователей. Возможно, у нас есть обязательные поля (имя) и необязательные (возраст, email). Мы можем перегрузить конструктор:

public class User {
    private String name;
    private int age;
    private String email;

    public User(String name) {
        this.name = name;
        this.age = 0;
        this.email = "";
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        this.email = "";
    }

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

Теперь можно создавать пользователей разными способами:

User user1 = new User("Алексей");
User user2 = new User("Мария", 25);
User user3 = new User("Иван", 30, "ivan@mail.ru");

Это очень удобно. Вы не обязаны указывать все поля — только те, которые вам нужны. Остальные получат значения по умолчанию. Без перегрузки пришлось бы создавать методы вроде createUserWithName, createUserWithNameAndAge и так далее.

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

Давайте ещё один пример — метод для вычисления площади. Площадь круга вычисляется по одному радиусу, площадь прямоугольника — по двум сторонам, площадь треугольника — по трём сторонам:

public class AreaCalculator {

    public double area(double radius) {
        return Math.PI * radius * radius;
    }

    public double area(double width, double height) {
        return width * height;
    }

    public double area(double a, double b, double c) {
        double s = (a + b + c) / 2.0;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }
}

Одно имя area, три версии. Вызывающий код читается естественно:

AreaCalculator calc = new AreaCalculator();
double circleArea = calc.area(5.0);
double rectArea = calc.area(4.0, 6.0);
double triangleArea = calc.area(3.0, 4.0, 5.0);

Из кода сразу понятно, что происходит. Не нужно задумываться о названиях методов — само имя area говорит оpurpose, а количество параметров подсказывает формулу.

Перегрузка по типам параметров

Второй тип перегрузки — когда количество параметров одинаковое, но типы различаются. Это тоже очень распространённый случай.

Вспомните пример с Math.max. Там ровно два параметра в каждой версии, но типы разные — int, double, long, float. Компилятор смотрит на типы переданных аргументов и выбирает подходящую версию.

Создадим класс Display, который показывает разные типы данных:

public class Display {

    public void show(int value) {
        System.out.println("Целое число: " + value);
    }

    public void show(double value) {
        System.out.println("Дробное число: " + value);
    }

    public void show(String value) {
        System.out.println("Строка: " + value);
    }

    public void show(boolean value) {
        System.out.println("Логическое значение: " + value);
    }
}

Все четыре метода называются show, все принимают ровно один параметр, но типы параметров разные. Когда вы вызываете show(42), компилятор видит целое число и вызывает первую версию. Когда вы вызываете show(3.14) — видит дробное число и вызывает вторую. Когда show("Привет") — строка, третья версия. И так далее.

Давайте рассмотрим более практический пример — метод для отправки сообщения. Сообщение может быть текстовым, числовым или даже объектом:

public class Messenger {

    public void send(String text) {
        System.out.println("Отправляем текст: " + text);
    }

    public void send(int code) {
        System.out.println("Отправляем код ошибки: " + code);
    }

    public void send(double amount) {
        System.out.println("Отправляем сумму: " + amount);
    }
}

Здесь перегрузка по типу параметра позволяет нам использовать одно имя метода для разных ситуаций. Это делает код более понятным — вы всегда вызываете send, а компилятор сам определяет, какую версию использовать.

Ещё один важный момент — перегрузка работает не только с примитивными типами, но и с объектами. Посмотрим на пример с классами:

public class Logger {

    public void log(String message) {
        System.out.println("[INFO] " + message);
    }

    public void log(Exception e) {
        System.out.println("[ERROR] " + e.getMessage());
    }

    public void log(StringBuilder sb) {
        System.out.println("[DEBUG] " + sb.toString());
    }
}

Три версии метода log — для строки, исключения и объекта StringBuilder. Компилятор различает их по типу параметра. Это очень удобно, потому что вы можете передавать разные объекты, и метод сам определит, как с ними обращаться.

Запомните: Перегрузка по типам параметров означает, что компилятор смотрит на типы переданных аргументов. Если вы передаёте int, вызовется метод с параметром int. Если double — метод с параметром double. Если String — метод с параметром String.

Давайте рассмотрим ещё один пример, который показывает практическую пользу. Представьте класс для работы с датами:

public class DateHelper {

    public String format(int day, int month, int year) {
        return day + "." + month + "." + year;
    }

    public String format(String date) {
        return date.replace("-", ".");
    }

    public String format(long timestamp) {
        java.util.Date date = new java.util.Date(timestamp);
        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy");
        return sdf.format(date);
    }
}

Три версии метода format — для отдельных компонентов даты, для строки в формате "год-месяц-день" и для временной метки. Вызывающий код:

DateHelper helper = new DateHelper();
String result1 = helper.format(15, 6, 2025);
String result2 = helper.format("2025-06-15");
String result3 = helper.format(System.currentTimeMillis());

Одно имя — три способа форматирования. Это делает класс DateHelper удобным в использовании. Программисту не нужно запоминать разные имена методов — просто format и разные типы аргументов.

Смешанная перегрузка: по количеству И типам

В реальной программистской жизни перегрузка редко бывает чисто «по количеству» или чисто «по типам». Чаще всего методы различаются одновременно и по количеству, и по типам параметров. Это нормально и даже предпочтительно.

Рассмотрим класс Logger, который логирует сообщения разного уровня детализации:

public class Logger {

    public void log(String message) {
        System.out.println("[INFO] " + message);
    }

    public void log(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }

    public void log(String level, String message, Exception e) {
        System.out.println("[" + level + "] " + message + " | " + e.getMessage());
    }

    public void log(int code, String message) {
        System.out.println("[CODE:" + code + "] " + message);
    }
}

Здесь четыре версии метода log, и они различаются одновременно по количеству и по типам. Первая версия — просто сообщение. Вторая — уровень и сообщение. Третья — уровень, сообщение и исключение. Четвёртая — код ошибки и сообщение.

Вызывающий код:

Logger logger = new Logger();
logger.log("Сервер запущен");
logger.log("WARNING", "Мало памяти");
logger.log("ERROR", "База данных недоступна", new Exception("timeout"));
logger.log(404, "Страница не найдена");

Каждый вызов неоднозначен — компилятор может точно определить, какую версию метода вызвать. Это возможно потому, что сигнатуры всех четырёх методов различаются.

Давайте рассмотрим ещё один пример — класс StringUtils для работы со строками:

public class StringUtils {

    public String repeat(String text, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(text);
        }
        return sb.toString();
    }

    public String repeat(char c, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(c);
        }
        return sb.toString();
    }

    public String truncate(String text, int maxLength) {
        if (text.length() <= maxLength) {
            return text;
        }
        return text.substring(0, maxLength) + "...";
    }

    public String truncate(String text, int maxLength, String suffix) {
        if (text.length() <= maxLength) {
            return text;
        }
        return text.substring(0, maxLength) + suffix;
    }
}

Четыре метода. Первые два — repeat — различаются типом первого параметра (строка или символ). Вторые два — truncate — различаются количеством параметров. Это типичная смешанная перегрузка.

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

Как Java выбирает нужную версию метода

Теперь давайте разберёмся, как именно Java определяет, какую версию перегруженного метода вызвать. Это происходит на этапе компиляции — компилятор анализирует вызов метода и подбирает наиболее подходящую версию. Этот процесс называется разрешением перегрузки (overload resolution).

Алгоритм подбора работает в несколько этапов:

Этап 1: Сбор кандидатов. Компилятор находит все методы с подходящим именем и количеством параметров. Если вы вызываете sum(3, 5), он ищет все методы sum, которые принимают ровно два параметра.

Этап 2: Проверка типов. Компилятор проверяет, можно ли передать типы ваших аргументов в каждый из найденных методов. При этом он учитывает правила автоматического преобразования типов (widening conversion).

Этап 3: Выбор наиболее подходящего. Если найдено несколько подходящих методов, компилятор выбирает тот, для которого преобразования типов минимальны. Если ни один метод не подходит — ошибка компиляции.

Рассмотрим это на конкретном примере. Допустим, у нас есть класс с тремя версиями метода process:

public class Processor {

    public void process(int a) {
        System.out.println("int версия: " + a);
    }

    public void process(double a) {
        System.out.println("double версия: " + a);
    }

    public void process(int a, int b) {
        System.out.println("два int: " + a + ", " + b);
    }
}

Теперь вызовем методы:

Processor p = new Processor();
p.process(10);
p.process(3.14);
p.process(10, 20);
p.process(5L);

Разберём каждый вызов:

  • process(10) — компилятор находит методы с одним параметром: process(int) и process(double). Число 10 — это int, поэтому вызывается первая версия (точное совпадение).
  • process(3.14) — компилятор находит методы с одним параметром. Число 3.14 — это double, поэтому вызывается вторая версия (точное совпадение).
  • process(10, 20) — компилятор находит методы с двумя параметрами. Только одна версия: process(int, int). Вызывается она.
  • process(5L) — компилятор находит методы с одним параметром: process(int) и process(double). Число 5L — это long. Тип long нельзя преобразовать в int автоматически (это было бы потеря данных), но можно преобразовать в double (без потери). Поэтому вызывается вторая версия.

А теперь сложный случай. Допустим, у нас есть:

public void process(int a, double b) {
    System.out.println("int, double: " + a + ", " + b);
}

public void process(double a, int b) {
    System.out.println("double, int: " + a + ", " + b);
}

Что произойдёт при вызове process(10, 20)? Компилятор видит два метода, и оба могут принять аргументы int и int — первый преобразует второй аргумент в double, второй преобразует первый аргумент в double. Оба варианта равноценны, и компилятор не может выбрать один из них. Вы получите ошибку компиляции!

Правило выбора: Java всегда выбирает наиболее специфичный метод — тот, который требует минимальных преобразований. Если есть точное совпадение — оно выбирается. Если нужно преобразование — выбирается метод с наименее «серьёзным» преобразованием. Если кандидатов несколько и ни один не более специфичен — ошибка компиляции.

Давайте ещё один пример, чтобы закрепить понимание. Рассмотрим класс с перегрузкой по типам:

public class Printer {

    public void print(Object obj) {
        System.out.println("Object: " + obj);
    }

    public void print(String str) {
        System.out.println("String: " + str);
    }

    public void print(int num) {
        System.out.println("int: " + num);
    }
}

Вызов print("Привет"): компилятор находит три метода, которые могут принять строку — print(Object) (строка является объектом), print(String) (точное совпадение) и print(int) (не подходит). Из двух подходящих print(String) более специфичен, потому что String является подклассом Object. Поэтому вызывается print(String).

Вызов print(42): компилятор находит два метода — print(Object) (целое число можно обернуть в объект) и print(int) (точное совпадение). Вызывается print(int), потому что это точное совпадение.

А вот что произойдёт при вызове print(null):

Printer printer = new Printer();
printer.print(null);

Компилятор видит, что null может быть передан в print(Object) и в print(String) (но не в print(int), потому что null — это не число). Из двух подходящих методов print(String) более специфичен, поэтому вызывается он. Это может быть неожиданным — но так работает Java.

Автопродвижение (widening): byte → short → int → long → float → double

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

Автопродвижение (widening conversion) — это когда Java автоматически преобразует значение из типа меньшего объёма в тип большего объёма. Это безопасно, потому что при таком преобразовании данные не теряются.

Цепочка автопродвижения для примитивных типов выглядит так:

byte → short → int → long → float → double
char → int → long → float → double

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

  • byte → short, int, long, float, double
  • short → int, long, float, double
  • int → long, float, double
  • long → float, double
  • float → double

Обратите внимание: int не может автоматически преобразоваться в byte или short, потому что это может привести к потере данных (переполнение). Аналогично long не может стать int, а double — float.

Как это влияет на перегрузку? Посмотрим на пример:

public class WideningDemo {

    public void show(int value) {
        System.out.println("int: " + value);
    }

    public void show(long value) {
        System.out.println("long: " + value);
    }

    public void show(double value) {
        System.out.println("double: " + value);
    }
}

Теперь вызовы:

WideningDemo demo = new WideningDemo();
demo.show(42);       // int — точное совпадение
demo.show(42L);      // long — точное совпадение
demo.show(3.14);     // double — точное совпадение
demo.show((byte) 10); // byte — автопродвижение до int
demo.show((short) 20); // short — автопродвижение до int

В последних двух вызовах передаются значения типа byte и short, но метода с этими типами нет. Компилятор автоматически преобразует их в int (ближайший подходящий тип в цепочке) и вызывает метод show(int).

Сложнее становится, когда несколько методов подходят. Рассмотрим:

public class AmbiguousDemo {

    public void process(int a, long b) {
        System.out.println("int, long");
    }

    public void process(long a, int b) {
        System.out.println("long, int");
    }
}

Вызов process(10, 20): компилятор видит два метода, и оба могут принять аргументы int и int. Первый метод преобразует второй аргумент из int в long. Второй метод преобразует первый аргумент из int в long. Оба преобразования одинаковы, поэтому компилятор не может выбрать — ошибка!

Ещё один важный нюанс —關係到char. Символ char в Java — это беззнаковый 16-битный тип, и он может быть автоматически преобразован в int, long, float или double, но не в byte или short (потому что char беззнаковый, а byte и short — знаковые).

public class CharDemo {

    public void show(int value) {
        System.out.println("int: " + value);
    }

    public void show(char value) {
        System.out.println("char: " + value);
    }
}

CharDemo demo = new CharDemo();
demo.show('A');  // char — точное совпадение
demo.show(65);   // int — точное совпадение
demo.show((char) 65); // char — точное совпадение

Символ 'A' — это значение типа char, поэтому вызывается метод show(char). Число 65 — это значение типа int, поэтому вызывается метод show(int).

Запомните цепочку: byte → short → int → long → float → double. Автопродвижение идёт только вправо по этой цепочке. Обратное преобразование (например, long в int) требует явного приведения типа (casting) и не является автопродвижением.

Рассмотрим ещё один пример, показывающий, как автопродвижение влияет на выбор метода:

public class WideningExample {

    public void test(double d) {
        System.out.println("double: " + d);
    }

    public void test(float f) {
        System.out.println("float: " + f);
    }
}

WideningExample ex = new WideningExample();
ex.test(10);    // int → long → float → double? Нет!

На самом деле здесь происходит следующее: значение 10 — это int. Компилятор ищет метод test, который может принять int. Метода test(int) нет. Но есть test(float) и test(double). Тип int может быть преобразован в float (одно преобразование) и в double (два преобразования: int → float → double). Компилятор выбирает test(float), потому что преобразование короче. Вызывается test(float).

Ограничения: перегрузка по возвращаемому значении НЕ работает

Это одна из самых частых ошибок начинающих программистов. Многие думают, что можно создать два метода с одинаковым именем и одинаковыми параметрами, но с разным типом возвращаемого значения. Нельзя!

Попробуйте написать такой код:

public class WrongOverload {

    public int getValue() {
        return 42;
    }

    public double getValue() {
        return 3.14;
    }
}

Это не скомпилируется! Компилятор выдаст ошибку, потому что методы имеют одинаковую сигнатуру — оба называются getValue и оба не принимают параметров. Тип возвращаемого значения не является частью сигнатуры.

Почему так? Представьте, что вы вызываете getValue(). Как компилятор поймёт, какую версию вызвать? Он не знает, в какую переменную вы хотите записать результат — в int или в double. Вызывающий код:

WrongOverload obj = new WrongOverload();
int a = obj.getValue();  // Какую версию вызвать?
double b = obj.getValue(); // А здесь какую?

Компилятор не может смотреть на контекст вызова (в какую переменную записывается результат) при выборе версии метода. Это сделало бы выбор зависимым от того, как используется результат, а не от того, что передаётся в метод. Это противоречило бы принципу ясности и предсказуемости.

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

Правило: Тип возвращаемого значения не учитывается при перегрузке методов. Два метода с одинаковым именем и одинаковыми параметрами (даже если у них разные типы возвращаемых значений) — это ошибка компиляции.

То же самое касается модификаторов доступа и исключений. Методы с одинаковым именем и параметрами, но разными модификаторами доступа или разным списком исключений — это тоже ошибка:

public class AlsoWrong {

    public void doSomething(int x) {
        System.out.println("public");
    }

    private void doSomething(int x) {
        System.out.println("private");
    }
}

Это тоже не скомпилируется. Модификатор доступа и список исключений не являются частью сигнатуры метода.

А вот что можно перегружать:

public class ValidOverload {

    public void process(int a) {
        System.out.println("один int");
    }

    public void process(int a, int b) {
        System.out.println("два int");
    }

    public void process(double a) {
        System.out.println("один double");
    }

    public void process(int a, double b) {
        System.out.println("int и double");
    }

    public void process(double a, int b) {
        System.out.println("double и int");
    }
}

Пять методов — все они合法но перегружены, потому что все имеют уникальные сигнатуры. Обратите внимание: process(int, double) и process(double, int) различаются порядком параметров. Порядок параметров — это часть сигнатуры!

Перегрузка vs переопределение (overloading vs overriding)

Многие начинающие программисты путают перегрузку (overloading) с переопределением (overriding). Это разные вещи, и важно понимать разницу.

Перегрузка (overloading) — это когда в одном классе несколько методов имеют одинаковое имя, но разные параметры. Это resolved на этапе компиляции (compile-time). Это не связано с наследованием.

Переопределение (overriding) — это когда дочерний класс предоставляет свою реализацию метода, который уже определён в родительском классе. Метод должен иметь точно такое же имя и точно такие же параметры. Это resolved на этапе выполнения (runtime), потому что Java использует полиморфизм, чтобы определить, какую реализацию вызвать.

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

public class Animal {

    public void speak() {
        System.out.println("Животное издаёт звук");
    }

    public void speak(String sound) {
        System.out.println("Животное издаёт звук: " + sound);
    }
}

public class Dog extends Animal {

    @Override
    public void speak() {
        System.out.println("Собака лает: Гав!");
    }
}

Dog dog = new Dog();
dog.speak();          // "Собака лает: Гав!" — переопределение
dog.speak("Гав!");    // "Животное издаёт звук: Гав!" — перегрузка
Animal animal = new Dog();
animal.speak();       // "Собака лает: Гав!" — полиморфизм (runtime)
animal.speak("Гав!"); // "Животное издаёт звук: Гав!" — перегрузка (compile-time)

Вот ключевые отличия:

Перегрузка:

  • Происходит в пределах одного класса (или между классом и его предками)
  • Имена методов совпадают, параметры различаются
  • Выбор метода происходит на этапе компиляции
  • Не связана с наследованием
  • Модификатор доступа может быть разным (но на практике это плохой стиль)

Переопределение:

  • Происходит между родительским и дочерним классами
  • Имя метода и параметры точно совпадают
  • Выбор метода происходит на этапе выполнения (динамическая диспетчеризация)
  • Напрямую связана с наследованием
  • Модификатор доступа дочернего метода не может быть строже родительского

Простая аналогия: Перегрузка — это как когда у повара есть несколько блюд с одним названием «Салат», но разным составом (с курицей, с морепродуктами, вегетарианский). Переопределение — это когда другой повар делает «Цезарь» по-своему, с другим рецептом, хотя название блюда то же самое.

Практические примеры: перегрузка конструкторов и удобные методы

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

Рассмотрим класс Point для работы с точками на плоскости:

public class Point {

    private int x;
    private int y;

    public Point() {
        this.x = 0;
        this.y = 0;
    }

    public Point(int x) {
        this.x = x;
        this.y = 0;
    }

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Point(Point other) {
        this.x = other.x;
        this.y = other.y;
    }

    public double distanceTo(Point other) {
        int dx = this.x - other.x;
        int dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    public double distanceTo(int x, int y) {
        int dx = this.x - x;
        int dy = this.y - y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

Четыре конструктора — четыре способа создать точку:

Point p1 = new Point();          // (0, 0)
Point p2 = new Point(5);         // (5, 0)
Point p3 = new Point(3, 4);      // (3, 4)
Point p4 = new Point(p3);        // копия p3

И два метода distanceTo — один принимает объект Point, другой — координаты:

Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2));    // 5.0
System.out.println(p1.distanceTo(3, 4));  // 5.0

Оба вызова дают одинаковый результат, но второй способ более удобен, когда у вас есть координаты, а не объект Point.

Ещё один практический пример — класс Color для работы с цветами:

public class Color {

    private int red;
    private int green;
    private int blue;

    public Color(int red, int green, int blue) {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    public Color(int hex) {
        this.red = (hex >> 16) & 0xFF;
        this.green = (hex >> 8) & 0xFF;
        this.blue = hex & 0xFF;
    }

    public Color(String name) {
        switch (name.toLowerCase()) {
            case "red":
                this.red = 255; this.green = 0; this.blue = 0;
                break;
            case "green":
                this.red = 0; this.green = 255; this.blue = 0;
                break;
            case "blue":
                this.red = 0; this.green = 0; this.blue = 255;
                break;
            default:
                this.red = 0; this.green = 0; this.blue = 0;
        }
    }

    public int toHex() {
        return (red << 16) | (green << 8) | blue;
    }

    public String toString() {
        return "RGB(" + red + ", " + green + ", " + blue + ")";
    }
}

Три конструктора — три способа создать цвет:

Color c1 = new Color(255, 0, 0);    // из компонентов
Color c2 = new Color(0xFF0000);      // из HEX-числа
Color c3 = new Color("red");         // из названия

Каждый способ удобен в своей ситуации. Если вы знаете компоненты — используете первый. Если работаете с HEX-представлением — второй. Если хотите задать цвет по названию — третий. Все три конструктора создают один и тот же объект, но принимают разные входные данные.

Давайте рассмотрим ещё один полезный пример — класс StringUtils для работы со строками:

public class StringUtils {

    public static boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }

    public static boolean isNotEmpty(String s) {
        return !isEmpty(s);
    }

    public static String repeat(String text, int count) {
        if (count <= 0) return "";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(text);
        }
        return sb.toString();
    }

    public static String join(String separator, String... parts) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            if (i > 0) sb.append(separator);
            sb.append(parts[i]);
        }
        return sb.toString();
    }

    public static String truncate(String text, int maxLength) {
        if (text == null) return null;
        if (text.length() <= maxLength) return text;
        return text.substring(0, maxLength - 3) + "...";
    }

    public static String truncate(String text, int maxLength, String suffix) {
        if (text == null) return null;
        if (text.length() <= maxLength) return text;
        return text.substring(0, maxLength - suffix.length()) + suffix;
    }
}

Здесь мы видим перегрузку метода truncate — один принимает текст и максимальную длину (и добавляет "..." по умолчанию), другой принимает также пользовательский суффикс:

String text = "Очень длинный текст для примера";
System.out.println(StringUtils.truncate(text, 20));          // "Очень длинный текст..."
System.out.println(StringUtils.truncate(text, 20, "!!!"));   // "Очень длинный текст!!!"

Перегрузка делает API более удобным — вы можете использовать метод с минимальным набором параметров (с дефолтным поведением) или передать дополнительные параметры для настройки.

Частые ошибки при перегрузке

Давайте рассмотрим ошибки, которые чаще всего допускают начинающие программисты при использовании перегрузки.

Ошибка 1: Попытка перегрузить по типу возвращаемого значения

Мы уже обсуждали это, но повторим — это самая частая ошибка:

public class Error1 {

    public int getValue() {
        return 42;
    }

    public double getValue() {
        return 3.14;
    }
}

Компилятор: «Метод getValue() уже определён в классе Error1 с теми же параметрами». Исправление: дайте методам разные имена, например getIntValue() и getDoubleValue().

Ошибка 2: Два метода с одинаковыми параметрами, но разными именами переменных

Имена параметров не влияют на сигнатуру метода:

public class Error2 {

    public void process(int a, int b) {
        System.out.println(a + b);
    }

    public void process(int x, int y) {
        System.out.println(x * y);
    }
}

Это ошибка компиляции, потому что оба метода имеют сигнатуру process(int, int). Имена переменных a, b и x, y не имеют значения.

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

public class Error3 {

    public void doWork() {
        System.out.println("public");
    }

    private void doWork() {
        System.out.println("private");
    }
}

Модификатор доступа не является частью сигнатуры. Ошибка компиляции.

Ошибка 4: Смешивание перегрузки с автопродвижением, которое создаёт неоднозначность

public class Error4 {

    public void show(int a) {
        System.out.println("int");
    }

    public void show(Integer a) {
        System.out.println("Integer");
    }
}

Error4 e = new Error4();
e.show(42);  // Какой метод вызовется?

Здесь есть нюанс. Значение 42 — это примитив int. Метод show(int) принимает его напрямую (точное совпадение). Метод show(Integer) требует упаковки (autoboxing) — преобразования int в Integer. Компилятор выберет show(int), потому что это точное совпадение, а упаковка — это преобразование.

Но если убрать метод show(int), то:

public class Error4Fixed {

    public void show(Integer a) {
        System.out.println("Integer");
    }
}

Error4Fixed e = new Error4Fixed();
e.show(42);  // Автоматическая упаковка: int → Integer, вызовется show(Integer)

Это работает, потому что компилятор может упаковать int в Integer. Но будьте осторожны — упаковка и распаковка могут привести к неожиданному поведению.

Ошибка 5: Перегрузка, которая выглядит одинаково с точки зрения вызова

public class Error5 {

    public void test(Object obj) {
        System.out.println("Object");
    }

    public void test(String str) {
        System.out.println("String");
    }
}

Error5 e = new Error5();
e.test("Привет"); // Вызовется test(String)
e.test(null);     // Вызовется test(String) — неожиданно!

Вызов test(null) вызывает test(String), потому что String является подклассом Object и поэтому более специфичен. Это может быть неожиданным для программиста, который думал, что null подходит для Object.

Совет: Если вы не уверены, какую версию метода вызовет компилятор, используйте явные приведения типов или упаковку/распаковку. Например, вместо test(null) напишите test((Object) null) или test((String) null), чтобы сделать намерение явным.

Ошибка 6: Перегрузка методов с varargs

public class Error6 {

    public void process(int... numbers) {
        System.out.println("int varargs");
    }

    public void process(int a, int... numbers) {
        System.out.println("int + varargs");
    }
}

Error6 e = new Error6();
e.process(1);       // Какой метод?
e.process(1, 2);    // Какой метод?
e.process(1, 2, 3); // Какой метод?

В первом вызове process(1) оба метода могут принять один аргумент. Компилятор выбирает process(int... numbers), потому что это более специфичный случай (один аргумент без varargs предпочтительнее varargs).

Во втором вызове process(1, 2) снова оба метода подходят. Компилятор выбирает process(int a, int... numbers), потому что он более специфичен.

В третьем вызове process(1, 2, 3) снова оба метода подходят. Компилятор выбирает process(int a, int... numbers).

Это работает, но может сбивать с толку. Лучше избегать такой перегрузки, если она не очевидна.

Разбор сложных случаев перегрузки

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

Случай 1: Массивы и перегрузка

Массивы — это объекты в Java, и перегрузка с ними работает стандартно:

public class ArrayOverload {

    public void show(int[] arr) {
        System.out.println("Массив int");
    }

    public void show(double[] arr) {
        System.out.println("Массив double");
    }

    public void show(Object obj) {
        System.out.println("Объект: " + obj);
    }
}

ArrayOverload ao = new ArrayOverload();
ao.show(new int[]{1, 2, 3});     // "Массив int"
ao.show(new double[]{1.5, 2.5}); // "Массив double"
ao.show(new String[]{"a", "b"}); // "Объект: [Ljava.lang.String;@..."

Обратите внимание на последний вызов — массив строк String[] не подходит ни под int[], ни под double[], поэтому вызывается метод с параметром Object (потому что массив является объектом).

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

Статические методы тоже можно перегружать:

public class MathUtils {

    public static int max(int a, int b) {
        return a > b ? a : b;
    }

    public static double max(double a, double b) {
        return a > b ? a : b;
    }

    public static int max(int a, int b, int c) {
        return max(max(a, b), c);
    }
}

int result1 = MathUtils.max(5, 10);
double result2 = MathUtils.max(3.14, 2.71);
int result3 = MathUtils.max(1, 2, 3);

Перегрузка работает одинаково для статических и нестатических методов.

Случай 3: Перегрузка с параметрами по умолчанию (varargs)

В Java нет параметров по умолчанию (как в Python или C++), но можно эмулировать это поведение с помощью перегрузки:

public class Logger {

    public void log(String message) {
        System.out.println("[INFO] " + message);
    }

    public void log(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }

    public void log(String level, String message, String tag) {
        System.out.println("[" + level + "] [" + tag + "] " + message);
    }
}

Logger logger = new Logger();
logger.log("Сервер запущен");                      // Используем дефолтный уровень INFO
logger.log("WARNING", "Мало памяти");               // Указываем уровень
logger.log("ERROR", "Ошибка БД", "database");      // Указываем уровень и тег

Это эмулирует поведение параметров по умолчанию. Метод с меньшим количеством параметров использует значения по умолчанию, а метод с бóльшим количеством — пользовательские значения.

Случай 4: Перегрузка методов, которые принимают классы из иерархии наследования

public class HierarchyDemo {

    public void process(Object obj) {
        System.out.println("Object: " + obj);
    }

    public void process(String str) {
        System.out.println("String: " + str);
    }

    public void process(Number num) {
        System.out.println("Number: " + num);
    }

    public void process(Integer num) {
        System.out.println("Integer: " + num);
    }
}

HierarchyDemo hd = new HierarchyDemo();
hd.process("Привет");     // String — более специфично
hd.process(42);            // Integer — более специфично (autoboxing)
hd.process(3.14);          // Number (double упакован в Double, Double — подкласс Number)
hd.process(new Object());  // Object

Здесь компилятор выбирает наиболее специфичный метод. Для строки — process(String). Для целого числа — process(Integer). Для дробного числа — process(Number). Для произвольного объекта — process(Object).

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

Автопродвижение и упаковка: тонкости, которые стоит знать

Давайте глубже разберём связь между автопродвижением (widening), упаковкой (autoboxing) и перегрузкой.

Автопродвижение (widening) — преобразование примитива в более широкий тип: byte → short → int → long → float → double.

Упаковка (autoboxing) — преобразование примитива в соответствующий обёрнутый тип: int → Integer, double → Double и т.д.

При выборе метода компилятор предпочитает автопродвижение упаковке. То есть метод, который принимает int, будет выбран раньше, чем метод, который принимает Integer.

public class BoxingDemo {

    public void show(int value) {
        System.out.println("int: " + value);
    }

    public void show(Integer value) {
        System.out.println("Integer: " + value);
    }

    public void show(Object value) {
        System.out.println("Object: " + value);
    }
}

BoxingDemo bd = new BoxingDemo();
bd.show(42);       // int — точное совпадение, показ int
bd.show((Integer) 42); // Integer — точное совпадение, показ Integer
bd.show((Object) 42);  // Object — объект, показ Object

Ещё один важный момент — автопродвижение и упаковка не могут применяться одновременно. Это значит, что byte не может быть автоматически упакован в Long, потому что это потребовало бы и автопродвижения (byte → long), и упаковки (long → Long). Компилятор не делает два преобразования одновременно.

public class DoubleConversionDemo {

    public void process(Long value) {
        System.out.println("Long: " + value);
    }
}

DoubleConversionDemo dcd = new DoubleConversionDemo();
byte b = 10;
// dcd.process(b); // ОШИБКА! byte не может стать Long напрямую
dcd.process((long) b); // Нужно явное приведение

Компилятор не может выполнить два преобразования: byte → long (autowidening) и long → Long (autoboxing). Нужно сделать одно из них явно.

Правило: Компилятор не выполняет «цепочку» из автопродвижения и упаковки. Если для вызова метода нужно и автопродвижение, и упаковка одновременно — это ошибка. Нужно помочь компилятору, сделав одно из преобразований явным.

Перегрузка конструкторов: создание объектов разными способами

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

Рассмотрим класс Student:

public class Student {

    private String name;
    private int age;
    private String group;
    private double gpa;

    public Student() {
        this.name = "Неизвестный";
        this.age = 18;
        this.group = "Н/Д";
        this.gpa = 0.0;
    }

    public Student(String name) {
        this.name = name;
        this.age = 18;
        this.group = "Н/Д";
        this.gpa = 0.0;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
        this.group = "Н/Д";
        this.gpa = 0.0;
    }

    public Student(String name, int age, String group) {
        this.name = name;
        this.age = age;
        this.group = group;
        this.gpa = 0.0;
    }

    public Student(String name, int age, String group, double gpa) {
        this.name = name;
        this.age = age;
        this.group = group;
        this.gpa = gpa;
    }

    public Student(Student other) {
        this.name = other.name;
        this.age = other.age;
        this.group = other.group;
        this.gpa = other.gpa;
    }

    public String toString() {
        return name + " (" + age + ", " + group + ", GPA: " + gpa + ")";
    }
}

Шесть конструкторов — шесть способов создать студента:

Student s1 = new Student();
Student s2 = new Student("Алексей");
Student s3 = new Student("Мария", 20);
Student s4 = new Student("Иван", 21, "ИТ-201");
Student s5 = new Student("Елена", 19, "ИТ-202", 4.8);
Student s6 = new Student(s5);

Вызывающий код читается естественно — вы передаёте только те данные, которые у вас есть. Необязательные поля получают значения по умолчанию. Это удобно и наглядно.

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

На практике перегрузка конструкторов используется повсюду. Класс String в Java имеет более 15 конструкторов! Класс FileInputStream — несколько конструкторов для разных способов открытия файла. Это стандартная практика.

Полезные паттерны перегрузки на практике

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

Паттерн 1: Методы с дефолтными значениями

В Java нет параметров по умолчанию, но перегрузка позволяет эмулировать это поведение:

public class Connection {

    public void connect(String host) {
        connect(host, 80, false);
    }

    public void connect(String host, int port) {
        connect(host, port, false);
    }

    public void connect(String host, int port, boolean useSsl) {
        System.out.println("Подключение к " + host + ":" + port
            + (useSsl ? " (SSL)" : ""));
    }
}

Connection conn = new Connection();
conn.connect("example.com");                // example.com:80
conn.connect("example.com", 8080);          // example.com:8080
conn.connect("example.com", 443, true);     // example.com:443 (SSL)

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

Паттерн 2: Удобные обёртки

Иногда полезно предоставить «короткий» путь для частых случаев:

public class Logger {

    public void info(String message) {
        log("INFO", message);
    }

    public void warn(String message) {
        log("WARN", message);
    }

    public void error(String message) {
        log("ERROR", message);
    }

    public void error(String message, Exception e) {
        log("ERROR", message + " | " + e.getMessage());
    }

    public void log(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }
}

Logger log = new Logger();
log.info("Сервер запущен");
log.warn("Мало памяти");
log.error("Ошибка подключения");
log.error("Ошибка БД", new Exception("timeout"));

Здесь методы info, warn, error — это обёртки над базовым методом log. Они передают уровень логирования. Метод error дополнительно перегружен для возможности передать исключение.

Паттерн 3: Фабричные методы

public class Color {

    private int r, g, b;

    private Color(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public static Color fromRGB(int r, int g, int b) {
        return new Color(r, g, b);
    }

    public static Color fromHex(int hex) {
        return new Color(
            (hex >> 16) & 0xFF,
            (hex >> 8) & 0xFF,
            hex & 0xFF
        );
    }

    public static Color fromName(String name) {
        switch (name.toLowerCase()) {
            case "red":   return new Color(255, 0, 0);
            case "green": return new Color(0, 255, 0);
            case "blue":  return new Color(0, 0, 255);
            default:      return new Color(0, 0, 0);
        }
    }
}

Color c1 = Color.fromRGB(255, 0, 0);
Color c2 = Color.fromHex(0xFF0000);
Color c3 = Color.fromName("red");

Здесь статические методы выполняют роль «конструкторов» — они создают объекты разными способами. Это часто используется, когда нужно создавать объекты из разных источников данных.

Как проверить, что перегрузка работает правильно

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

Способ 1: Добавить вывод в каждую версию

public class VerifyDemo {

    public void show(int a) {
        System.out.println("Вызвана версия: int");
    }

    public void show(double a) {
        System.out.println("Вызвана версия: double");
    }

    public void show(String a) {
        System.out.println("Вызвана версия: String");
    }
}

VerifyDemo vd = new VerifyDemo();
vd.show(42);        // "Вызвана версия: int"
vd.show(3.14);      // "Вызвана версия: double"
vd.show("Привет");  // "Вызвана версия: String"

Способ 2: Использовать оператор instanceof

public void process(Object obj) {
    if (obj instanceof String) {
        System.out.println("Обработка строки");
    } else if (obj instanceof Integer) {
        System.out.println("Обработка целого числа");
    } else {
        System.out.println("Обработка объекта");
    }
}

Способ 3: Команда javap для просмотра байт-кода

Если вы скомпилировали класс, можете использовать команду javap -c ClassName, чтобы увидеть, какие методы реально существуют в байт-коде. Это помогает убедиться, что перегрузка работает на уровне компиляции.

Совет: Если вы сомневаетесь, какая версия метода вызывается, добавьте System.out.println в каждую версию и запустите программу. Это самый простой способ проверить поведение перегрузки.

Перегрузка в реальных библиотеках Java

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

StringBuilder — класс для построения строк:

StringBuilder sb = new StringBuilder();
sb.append("Привет ");     // append(String)
sb.append(42);            // append(int)
sb.append(3.14);          // append(double)
sb.append(true);          // append(boolean)
sb.append('A');           // append(char)

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

Arrays — класс для работы с массивами:

int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr);              // sort(int[])
Arrays.sort(arr, 1, 3);       // sort(int[], int fromIndex, int toIndex)
Arrays.fill(arr, 0);           // fill(int[], int value)
Arrays.fill(arr, 0, 3, 0);    // fill(int[], int fromIndex, int toIndex, int value)
int idx = Arrays.binarySearch(arr, 3); // binarySearch(int[], int)

Методы sort, fill, binarySearch перегружены для разных типов массивов и разного количества параметров.

System.out — стандартный вывод:

System.out.println("Строка");
System.out.println(42);
System.out.println(3.14);
System.out.println(true);
System.out.println('A');
System.out.println(new Object());

Метод println перегружен для каждого примитивного типа и для Object. Это делает вывод информации максимально удобным.

Math — математические функции:

Math.max(5, 10);       // max(int, int)
Math.max(3.14, 2.71);  // max(double, double)
Math.abs(-42);          // abs(int)
Math.abs(-3.14);        // abs(double)
Math.round(3.14);       // round(double) → long
Math.round(3.14f);      // round(float) → int

Методы max, abs, round перегружены для разных типов. Это стандартная практика в Java.

Рекомендации по использованию перегрузки

Перегрузка — мощный инструмент, но его нужно использовать с умом. Вот несколько рекомендаций:

1. Не перегружайте слишком сильно. Если у вас больше 4-5 версий одного метода, возможно, стоит разбить его на несколько разных методов с разными именами. Слишком много версий одного метода может запутать читателя.

2. Делайте перегрузку осмысленной. Каждая версия метода должна иметь чёткую цель. Не создавайте версию только ради того, чтобы она была.

3. Используйте перегрузку для удобства API. Главная цель перегрузки — сделать использование вашего класса удобным. Предоставляйте «короткие» пути для частых случаев.

4. Документируйте перегрузки. Если у метода несколько версий, убедитесь, что в документации (Javadoc) описаны все версии и их различия.

5. Избегайте неоднозначности. Убедитесь, что компилятор всегда может однозначно определить, какую версию метода вызвать. Если есть сомнения — используйте явные приведения типов.

6. Не перегружайте по типу возвращаемого значения. Это не работает и приводит к ошибке компиляции.

Золотое правило: Перегрузка должна делать код проще, а не сложнее. Если перегрузка запутывает читателя или создаёт неоднозначность — от неё лучше отказаться и использовать разные имена методов.

Итоги урока

  • ✅ Перегрузка методов — это создание нескольких методов с одинаковым именем, но разными параметрами в пределах одного класса
  • ✅ Перегрузка бывает по количеству параметров, по типам параметров и смешанная (по количеству и типам одновременно)
  • ✅ Компилятор выбирает наиболее специфичный метод на этапе компиляции, учитывая автопродвижение типов
  • ✅ Автопродвижение (widening) идёт по цепочке: byte → short → int → long → float → double
  • ✅ Тип возвращаемого значения, модификатор доступа и список исключений не являются частью сигнатуры метода и не влияют на перегрузку
  • ✅ Перегрузка (overloading) отличается от переопределения (overriding): перегрузка — в одном классе, разные параметры; переопределение — в наследнике, те же параметры
  • ✅ Перегрузка конструкторов — один из самых частых и полезных паттернов для создания объектов разными способами
  • ✅ Автопродвижение и упаковка не могут применяться одновременно — компилятор не делает два преобразования подряд
  • ✅ Перегрузка должна делать код проще и удобнее, а не сложнее и запутаннее

Следующий урок

В следующем уроке мы разберём переопределение методов (overriding) — как дочерний класс меняет поведение методов родительского класса, аннотацию @Override и динамическую диспетчеризацию.

Перейти к уроку 6.4 →

Тест по уроку 6.3: Перегрузка методов

10 вопросов

Перегрузка методов

Premium