Перегрузка методов в Java (overloading) позволяет объявить в классе несколько методов с одинаковым именем, если у них разные списки параметров: другое количество, другие типы или другой порядок типов. Какой из перегруженных методов вызвать, компилятор определяет на этапе компиляции в зависимости от аргументов вызова, а переопределение (overriding) срабатывает во время выполнения по фактическому объекту.
- Что такое перегрузка методов
- Что входит в сигнатуру метода
- Перегрузка конструкторов
- Как Java выбирает перегруженный метод
- Неоднозначный вызов
- Ловушка List.remove
- Переопределение методов
- Перегрузка вместо переопределения: equals
- Чем перегрузка отличается от переопределения
- Минусы перегрузки методов
- Частые вопросы
- Можно ли перегрузить метод только по типу возвращаемого значения?
- Можно ли перегрузить статический метод и main?
- Влияют ли на перегрузку модификаторы доступа?
Что такое перегрузка методов
В Java синтаксис перегрузки ничем не отличается от объявления обычного метода: пишем ещё один метод с тем же именем и другими параметрами. В JavaScript, для сравнения, одноимённые функции не перегружаются, и тип аргумента проверяют в теле через оператор typeof. В следующем примере у калькулятора три варианта метода sum. Без перегрузки пришлось бы придумывать имена sumInt, sumDouble, sumThree, а с ней вызывающему коду проще: имя одно, нужную версию выбирает компилятор. Примеры собраны и запущены на OpenJDK 21, вывод и тексты ошибок javac приведены дословно.
public class Calculator {
static int sum(int a, int b) {
return a + b;
}
static double sum(double a, double b) {
return a + b;
}
static int sum(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
System.out.println(sum(2, 3));
System.out.println(sum(2.5, 0.5));
System.out.println(sum(1, 2, 3));
System.out.println(sum(2, 0.5)); // 2 расширяется до double
}
}
5
3.0
6
2.5
Последний вызов интересен: метода sum(int, double) нет, поэтому компилятор расширил int до double и взял sum(double, double).
Перегруженные методы не обязаны лежать в одном классе. Если наследник объявляет метод с тем же именем, что у родителя, но с другими параметрами, это тоже перегрузка: в наследнике доступны обе версии.
Что входит в сигнатуру метода
Сигнатура метода в Java состоит из имени метода и типов параметров в их порядке. Именно по ней компилятор отличает перегруженные методы. Что в сигнатуру не входит:
- тип возвращаемого значения;
- имена параметров;
- модификаторы доступа (
public,private) иstatic; - список исключений
throws.
Поэтому два метода, которые различаются только типом возвращаемого значения, не компилируются:
public class Parser {
int parse(String s) {
return Integer.parseInt(s);
}
long parse(String s) {
return Long.parseLong(s);
}
}
Parser.java:6: error: method parse(String) is already defined in class Parser
long parse(String s) {
^
1 error
Компилятор видит в вызове parse("42") только имя и аргументы, а тип результата в выборе не участвует. Та же ошибка method f(int) is already defined появится, если объявить void f(int a) и static void f(int b): другое имя параметра и static сигнатуру не меняют.
С дженериками есть своя ловушка: методы g(List<String>) и g(List<Integer>) выглядят разными, но после стирания типов оба становятся g(List), и javac отвечает name clash: g(List<Integer>) and g(List<String>) have the same erasure.
Перегрузка конструкторов
Конструкторы перегружаются по тем же правилам. Короткий конструктор обычно передаёт работу полному с помощью this(...), чтобы логика инициализации жила в одном месте и значения по умолчанию задавались в одной строке:
public class Rect {
final int width, height;
Rect() {
this(1, 1); // вызывает второй конструктор
}
Rect(int width, int height) {
this.width = width;
this.height = height;
}
public static void main(String[] args) {
System.out.println(new Rect().width);
System.out.println(new Rect(3, 4).width);
}
}
1
3

Как Java выбирает перегруженный метод
Компилятор ищет подходящий метод в три фазы и останавливается на первой, где нашёл хотя бы один:
- Без автоупаковки и без varargs (аргументов переменной длины). Разрешено точное совпадение типа и расширение:
byteдоint,intдоlongилиdouble,StringдоObject. - С автоупаковкой и распаковкой (
intвIntegerи обратно), но ещё без varargs. - С аргументами переменной длины.
Вызов через varargs никогда не выбирается, если подходит метод с фиксированным числом параметров. Если на фазе подошло несколько методов, дальше берётся самый специфичный: тот, чьи параметры можно передать в остальные. В следующем классе видны первая и третья фазы, вторая появится, если убрать одну перегрузку:
public class Resolution {
static void show(long x) { System.out.println("show(long)"); }
static void show(Integer x) { System.out.println("show(Integer)"); }
static void show(int... x) { System.out.println("show(int...)"); }
public static void main(String[] args) {
show(5); // фаза 1: расширение до long
show(Integer.valueOf(5)); // фаза 1: точное совпадение
show(5, 6); // фаза 3: два аргумента принимает только varargs
}
}
show(long)
show(Integer)
show(int...)
Для show(5) расширение до long победило упаковку, хотя обёртка кажется ближе по смыслу. Уберите версию с long, и первая строка выведет show(Integer). Уберите и её, тогда вызов уйдёт в show(int...). Внутри метода аргументы переменной длины приходят обычным массивом int[], который обходят циклом for.
Расширение и упаковку компилятор не совмещает. Метод save(Long id) с вызовом save(42) не скомпилируется с ошибкой incompatible types: int cannot be converted to Long. Литерал 42 имеет тип int, упаковать его можно только в Integer, а Integer не наследник Long. Помогает save(42L).
С null правило «самого специфичного» работает так: если есть print(Object) и print(String), вызов print(null) выведет print(String), потому что String наследуется от Object.

Неоднозначный вызов
Ошибка компиляции возникает, когда на одной фазе подходят два метода и ни один не специфичнее другого:
public class Ambiguous {
static void pair(int a, long b) { System.out.println("int, long"); }
static void pair(long a, int b) { System.out.println("long, int"); }
public static void main(String[] args) {
pair(1, 2);
}
}
Ambiguous.java:6: error: reference to pair is ambiguous
pair(1, 2);
^
both method pair(int,long) in Ambiguous and method pair(long,int) in Ambiguous match
1 error
Решение: явный тип в вызове, pair(1, 2L) или pair(1L, 2). Та же ошибка будет с null, если перегрузки принимают два несвязанных ссылочных типа, например String и StringBuilder.
Ловушка List.remove
У List два метода с одинаковым именем: remove(int index) удаляет элемент по позиции, remove(Object o) удаляет значение. На List<Integer> это даёт ошибку, которую не видно глазами:
import java.util.ArrayList;
import java.util.List;
public class RemoveTrap {
public static void main(String[] args) {
List<Integer> ids = new ArrayList<>(List.of(10, 20, 30, 1));
ids.remove(1); // remove(int index): удалён элемент с индексом 1
System.out.println(ids);
ids.remove(Integer.valueOf(1)); // remove(Object o): удалено значение 1
System.out.println(ids);
}
}
[10, 30, 1]
[10, 30]
Хотели удалить единицу, а удалили 20. Вызов remove(1) подходит к remove(int) уже на первой фазе, до автоупаковки дело не доходит. Значение удаляют через Integer.valueOf(1) или (Integer) 1.

Переопределение методов
Переопределение (overriding) нужно, когда подкласс меняет поведение метода родителя. У переопределяющего метода та же сигнатура, а вызывается он по фактическому типу объекта во время выполнения. На этом построен полиморфизм: переменная типа Animal может хранить Dog, и вызов sound() уйдёт в код собаки. Из методов класса Object обычно переопределяют toString(), equals() и hashCode(). На переопределении методов интерфейса держится и подмена реализаций, например порты и адаптеры в архитектурных паттернах.
Условия и ограничения при переопределении:
- имя и типы параметров совпадают с родительским методом;
- тип возвращаемого значения тот же, а для ссылочных типов можно вернуть подтип (ковариантный тип: родитель возвращает
Animal, наследникDog); дляvoidи примитивов тип совпадает точно; - модификатор доступа не уже исходного:
publicостаётсяpublic,protectedможно расширить доpublic; - проверяемых исключений в
throwsне больше, чем у родительского метода; - методы
finalпереопределить нельзя, аprivate-методы не наследуются, поэтому и не переопределяются; static-метод не переопределяется, а скрывается: какой из двух сработает, зависит от типа, через который его вызвали, а@Overrideнад ним даёт ошибкуstatic methods cannot be annotated with @Override.
Если в родителе метод объявлен как public, а в наследнике его переопределили без модификатора, javac остановит сборку:
public class Narrow {
static class Animal {
public String sound() { return "..."; }
}
static class Cat extends Animal {
@Override
String sound() { return "мяу"; }
}
}
Narrow.java:8: error: sound() in Cat cannot override sound() in Animal
String sound() { return "мяу"; }
^
attempting to assign weaker access privileges; was public
1 error
Аннотация @Override просит компилятор проверить, что метод действительно переопределяет метод родителя. Мы советуем ставить её всегда: без неё метод с опечаткой в типе параметра станет перегрузкой, и компилятор промолчит.
Перегрузка вместо переопределения: equals
Классическая ошибка на стыке двух механизмов: метод equals объявлен с параметром своего класса. Код компилируется, но это перегрузка, и Object.equals(Object) остаётся прежним:
import java.util.List;
public class EqualsTrap {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
// перегрузка: Object.equals(Object) не переопределён
public boolean equals(Point other) {
return x == other.x && y == other.y;
}
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
Object o = b;
System.out.println(a.equals(b));
System.out.println(a.equals(o));
System.out.println(List.of(a).contains(b));
}
}
true
false
false
Первый вызов попал в наш метод. Во втором у аргумента объявленный тип Object, и сработал equals(Object) класса Object, который сравнивает ссылки. Третий вызов делает коллекция, и результат тот же. Коллекции и HashMap вызывают equals(Object), поэтому точки с одинаковыми координатами в них считаются разными. Поставьте над методом @Override, и ошибка всплывёт при компиляции:
public class EqualsOverride {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Point other) {
return x == other.x && y == other.y;
}
}
}
EqualsOverride.java:6: error: method does not override or implement a method from a supertype
@Override
^
1 error
Правильная сигнатура: public boolean equals(Object other), и к ней в пару hashCode().

Чем перегрузка отличается от переопределения
При перегрузке метод выбирает компилятор, а при переопределении JVM во время выполнения. В примере ниже оба механизма работают в одной строке:
public class Dispatch {
static class Animal {
String sound() { return "звук животного"; }
}
static class Dog extends Animal {
@Override
String sound() { return "гав"; }
}
static void describe(Animal a) { System.out.println("describe(Animal): " + a.sound()); }
static void describe(Dog d) { System.out.println("describe(Dog): " + d.sound()); }
public static void main(String[] args) {
Animal pet = new Dog();
describe(pet); // перегрузку выбрал компилятор по типу переменной
describe(new Dog());
}
}
describe(Animal): гав
describe(Dog): гав
При выборе перегрузки используется объявленный тип переменной. В pet лежит собака, но объявлена она как Animal, и компилятор выбрал describe(Animal). Внутри метода a.sound() уже отработал по фактическому объекту и вернул «гав».
| Перегрузка | Переопределение | |
|---|---|---|
| Где | в одном классе или в наследнике | в наследнике или в классе, который реализует интерфейс |
| Параметры | обязаны различаться | совпадают |
| Возвращаемый тип | любой | тот же, для ссылочных типов можно подтип |
| Модификатор доступа | любой | не уже, чем в суперклассе |
| Когда выбирается метод | на этапе компиляции | во время выполнения |
| По чему выбирается | объявленные типы аргументов | фактический тип объекта |
static, private, final | перегружаются | не переопределяются |
Перегрузку иногда называют статическим полиморфизмом, а переопределение динамическим. Мы бы не называли перегрузку просто полиморфизмом без уточнения «на этапе компиляции»: в Java под полиморфизмом обычно понимают вызов переопределённого метода через ссылку на родителя.
Минусы перегрузки методов
Перегруженный код сложнее читать. Читатель видит remove(1) и должен в уме повторить три фазы выбора, чтобы понять, какой метод сработает. Мы бы не стали делать перегрузки с одинаковым числом параметров, если версии делают разное, а типы аргументов превращаются друг в друга: int и Object, int и Integer. Именно так устроен remove: одна версия удаляет по индексу, другая по значению. Здесь надёжнее разные имена: removeAt(int index) и removeValue(Object o).
Использовать перегрузку стоит, когда все версии решают одну задачу: тогда ошибка в выборе версии не меняет смысла вызова. Так устроен System.out.println: у него отдельные версии для int, double, char, String, Object и других типов, и каждая просто печатает значение. Если передать int, а версии для него бы не было, сработала бы версия с long, и на экране всё равно оказалось бы число. Безопасна и перегрузка с разным количеством параметров, как у sum в начале статьи: код чище, чем с набором похожих имён.

Частые вопросы
Можно ли перегрузить метод только по типу возвращаемого значения?
Нет. Тип возвращаемого значения не входит в сигнатуру, и javac ответит method ... is already defined in class .... Чтобы методы различались, у них должны отличаться количество, типы или порядок параметров.
Можно ли перегрузить статический метод и main?
Да, статические методы перегружаются как обычные. Метод main тоже можно перегрузить, но программу JVM запускает через public static void main(String[] args). В классе с main(int count) и main(String[] args) сначала выполнится вторая версия, а первую можно вызвать из неё как обычный метод.
Влияют ли на перегрузку модификаторы доступа?
В сигнатуру модификаторы не входят, поэтому такой класс не скомпилируется:
public class Printer {
public void print(int x) { }
private void print(int x) { }
}
Printer.java:3: error: method print(int) is already defined in class Printer
private void print(int x) { }
^
1 error
Но при выборе метода компилятор рассматривает только методы, доступные из места вызова, поэтому private-перегрузку снаружи класса он не увидит.








