Java Challengers #1: Перегрузка методов в JVM
Всем доброго дня.
У нас уже запустился очередной поток курса "Разработчик Java", но у нас ещё осталось немного материалов, которыми бы хотели с вами поделиться.
Добро пожаловать в серию статей Java Challengers! Этот серия статей посвящена особенностям программирования на Java. Их освоение — это ваш путь к становлению высококвалифицированным программистом на Java.
Освоение техник, рассматриваемых в этой серии статей требует некоторых усилий, но они будут иметь большое значение в вашем повседневном опыте в качестве java — разработчика. Избежать ошибок проще когда вы знаете как правильно применять основные техники программирования Java и отслеживать ошибки намного проще, когда вы точно знаете, что происходит в вашем java — коде.
Готовы ли вы приступить к освоению основных концепций программирования на Java? Тогда давайте начнем с нашей первой задачки!
Термин "Перегрузка методов"
Про термин перегрузка разработчики склонны думать, что речь идет о перезагрузке системы, но это не так. В программировании, перегрузка метода означает использование одинакового имени метода с разными параметрами.
Что такое перегрузка методов?
Перегрузка методов — это приём программирования, который позволяет разработчику в одном классе для методов с разными параметрами использовать одно и то же имя. В этом случае мы говорим, что метод перегружен.
В Листинге 1 показаны методы с разными параметрами, которые различаются количеством, типом и порядком.
Листинг 1. Три варианта перегрузки методов.
// Количество параметров
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) { }
}
Перегрузка методов и примитивные типы
В Листинге 1 вы видели примитивные типы int
и double
. Давайте отвлечёмся на минуту и вспомним примитивные типы в Java.
Таблица 1. Примитивные типы в Java
Тип | Диапазон | Значение по умолчанию | Размер | Примеры литералов |
---|---|---|---|---|
boolean | true или false | false | 1 бит | true, false |
byte | -128… 127 | 0 | 8 бит | 1, -90, 128 |
char | Символ юникода или от 0 до 65 536 | \u0000 | 16 бит | 'a', '\u0031', '\201', '\n', 4 |
short | -32,768… 32,767 | 0 | 16 бит | 1, 3, 720, 22,000 |
int | -2 147 483 648… 2 147 483 647 | 0 | 32 бит | -2, -1, 0, 1, 9 |
long | -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 | 0 | 64 бит | -4000L, -900L, 10L, 700L |
float | 3.40282347 x 1038, 1.40239846 x 10-45 | 0.0 | 32 бит | 1.67e200f, -1.57e-207f, .9f, 10.4F |
double | 1.7976931348623157 x 10308, 4.9406564584124654 x 10-324 | 0.0 | 64 бит | 1.e700d, -123457e, 37e1d |
Зачем мне использовать перегрузку методов?
Использование перегрузки делает ваш код чище и проще для чтения, а также помогает избежать ошибок в программе.
В противоположность Листингу 1 представьте программу, где у вас будет много методов calculate()
с именами похожими на calculate1
, calculate2
, calculate3
… не хорошо, правда? Перегрузка метода calculate()
позволяет использовать одно и то же имя и изменять только то, что необходимо — параметры. Также очень легко найти перегруженные методы, поскольку они сгруппированы в коде.
Чем перегрузка не является
Помните, что изменение имени переменной не является перегрузкой. Следующий код не скомпилируется:
public class Calculator {
void calculate(int firstNumber, int secondNumber){}
void calculate(int secondNumber, int thirdNumber){}
}
Вы также не можете перегрузить метод, изменяя возвращаемое значение в сигнатуре метода. Этот код также не скомпилируется:
public class Calculator {
double calculate(int number1, int number2){return 0.0;}
long calculate(int number1, int number2){return 0;}
}
Перегрузка конструктора
Вы можете перегрузить конструктор таким же способом, как и метод:
public class Calculator {
private int number1;
private int number2;
public Calculator(int number1) {
this.number1 = number1;
}
public Calculator(int number1, int number2) {
this.number1 = number1;
this.number2 = number2;
}
}
Решите задачку по перегрузке методов
Готовы ли вы к первому испытанию? Давайте выясним!
Начните с внимательного изучения следующего кода.
Листинг 2. Сложная задача по перегрузке методов
public class AdvancedOverloadingChallenge3 {
static String x = "";
public static void main(String... doYourBest) {
executeAction(1);
executeAction(1.0);
executeAction(Double.valueOf("5"));
executeAction(1L);
System.out.println(x);
}
static void executeAction(int ... var) {x += "a"; }
static void executeAction(Integer var) {x += "b"; }
static void executeAction(Object var) {x += "c"; }
static void executeAction(short var) {x += "d"; }
static void executeAction(float var) {x += "e"; }
static void executeAction(double var) {x += "f"; }
}
Хорошо. Вы изучили код. Какой будет вывод?
- befe
- bfce
- efce
- aecf
Правильный ответ приведён в конце статьи.
Что сейчас произошло? Как JVM компилирует перегруженные методы
Для того чтобы понять что произошло в Листинге 2, вам нужно знать несколько вещей о том, как JVM компилирует перегруженные методы.
Прежде всего, JVM разумно ленива: она всегда будет прилагать наименьшие усилия для выполнения метода. Таким образом, когда вы думаете о том, как JVM обрабатывает перегрузку, имейте в виду три важных особенности компилятора:
- Расширение (widening)
- Упаковка (autoboxing and unboxing)
- Аргументы переменной длины (varargs)
Если вы никогда не сталкивались с этими техниками, то несколько примеров должны вам помочь их понять. Обратите внимание, что JVM выполняет их в том порядке, в котором они указаны.
Вот пример расширения:
int primitiveIntNumber = 5;
double primitiveDoubleNumber = primitiveIntNumber ;
Это порядок расширения примитивных типов:
(Прим. переводчика — В JLS расширение примитивов описано с большими вариациями, например, long может быть расширен во float или в double.)
Пример автоупаковки:
int primitiveIntNumber = 7;
Integer wrapperIntegerNumber = primitiveIntNumber;
Обратите внимание, что происходит за кулисами при компиляции кода:
Integer wrapperIntegerNumber = Integer.valueOf(primitiveIntNumber);
А вот пример распаковки:
Integer wrapperIntegerNumber = 7;
int primitiveIntNumber= wrapperIntegerNumber;
Вот что происходит за кулисами при компиляции этого кода:
int primitiveIntNumber = wrapperIntegerNumber.intValue();
И вот пример метода с аргументами переменной длины. Обратите внимание, что методы переменной длины всегда являются последними для выполнения.
execute(int... numbers){}
Что такое аргументы переменной длины?
Аргументы переменной длины — это просто массив значений, заданный трёмя точками (...). Мы можем передать сколько угодно чисел int
этому методу.
Например:
execute(1,3,4,6,7,8,8,6,4,6,88...); // Можно продолжать...
Аргументы переменной длины (varargs) очень удобны тем, что значения могут передаваться непосредственно в метод. Если бы мы использовали массивы, нам пришлось бы создать экземпляр массива со значениями.
Расширение: практический пример
Когда мы передаем число 1 прямо в метод executeAction()
, JVM автоматически интерпретирует его как int
. Вот почему это число не будет передано в метод executeAction(short var)
.
Аналогично, если мы передаём число 1.0
JVM автоматически распознает, что это double.
Конечно число 1.0
также может быть и float
, но тип таких литералов предопредопределен. Поэтому в Листинге 2 выполняется метод executeAction(double var)
.
Когда мы используем обёртку Double
, есть два варианта: либо число может быть распаковано в примитивный тип, либо оно может быть расширено в Object
. (Помните, что каждый класс в Java расширяет класс Object
.) В этом случае JVM выбирает расширение типа Double
в Object
, потому что это требует меньше усилий, чем распаковка.
Последним мы передаём 1L
и так как, мы указали тип — это long
.
Распространенные ошибки с перегрузкой
К настоящему времени вы, вероятно, поняли, что с перегрузкой методов всё может быть запутано, поэтому давайте рассмотрим несколько проблем, с которыми вы, вероятно, столкнетесь.
Автоупаковка с обёртками (autoboxing with wrappers)
Java — это строго типизированный язык программирования и, когда мы используем автоупаковку с обёртками, есть несколько вещей, которые мы должны учитывать. Во-первых, следующий код не компилируется:
int primitiveIntNumber = 7;
Double wrapperNumber = primitiveIntNumber;
Автоупаковка будет работать только с типом double
потому что, когда вы скомпилируете код, он будет эквивалентен этому:
Double number = Double.valueOf(primitiveIntNumber);
Этот код скомпилируется. Первый int
будет расширен до double
и потом будет упакован в Double
. Но при автоупаковке нет расширения типов и конструктор Double.valueof
ожидает double
, а не int
. В этом случае автоупаковка будет работать, если мы сделаем явное приведение типа, например:
Double wrapperNumber = (double) primitiveIntNumber;
Помните, что Integer
не может быть Long
и Float
и не может быть Double
. Здесь нет наследования. Каждый из этих типов (Integer
, Long
, Float
, и Double
) — Number
и Object
.
Если Вы сомневаетесь, просто помните, что обёртки чисел (wrapper numbers) могут быть расширены до Number
или Object
. (Есть еще много чего, что можно сказать про обёртки, но оставим это для другой статьи.)
Литералы чисел в коде
Когда мы не указываем тип числа-литерала, JVM вычислит тип за нас. Если напрямую используем число 1
в коде, то JVM создаст его как int
. Если мы попытаемся передать 1
напрямую в метод, который принимает short
, то он не скомпилируется.
Например:
class Calculator {
public static void main(String... args) {
// Вызов этого метода не скомпилируется
// Да, может быть char, short, byte, но JVM создает его как int
calculate(1);
}
void calculate(short number) {}
}
Такое же правило будет применяться, когда используется число 1.0
. Хотя это может быть и float
, JVM будет считать его double
.
class Calculator {
public static void main(String... args) {
// Вызов этого метода не скомпилируется
// Да, может быть float, но JVM создает его как double
calculate(1.0);
}
void calculate(float number) {}
}
Другой распространенной ошибкой является предположение, что Double
или любая другая обертка лучше подойдет для метода, получающего double
.
Факт в том, что JVM требуется меньше усилий для расширения обертки Double
в Object
вместо её распаковки в примитивный тип double
.
Подводя итог, при использовании непосредственно в java-коде, 1
будет int
и 1.0
будет double
. Расширение — это самый лёгкий путь к выполнению, далее идёт упаковка или распаковка и последней операцией всегда будут методы переменной длины.
Как любопытный факт. Знаете ли вы, что тип char
принимает числа?
char anyChar = 127; // Да, это странно, но это компилируется
Что необходимо помнить о перегрузке
Перегрузка — это очень мощная техника для случаев, когда вам нужно одинаковое имя метода с разными параметрами. Это полезная техника, потому что использование правильных имён делает код более удобным для чтения. Вместо того, чтобы дублировать имя метода и добавлять беспорядок в ваш код, вы можете просто перегрузить его.
Это позволяет сохранять код чистым и удобным для чтения, а также снижает риск того, что дублирующие методы сломают часть системы.
Что следует иметь в виду: при перегрузке метода JVM сделает наименьшее усилие из возможных.
Вот порядок самого ленивого пути к исполнению:
- Первое — расширение (widening)
- Второе — упаковка (boxing)
- Третье — аргументы переменной длины (varargs)
Что следует учитывать: сложные ситуации возникают при объявлении чисел напрямую: 1
будет int
и 1.0
будет double
.
Также помните, что вы можете объявить эти типы явно, используя синтаксис 1F
или 1f
для float
и 1D
или 1d
для double
.
На этом мы закончим о роли JVM в перегрузке методов. Важно понимать, что JVM по своей сути ленива, и всегда будет следовать по самому ленивому пути.
Ответ
Ответ к Листингу 2 — Вариант 3. efce.
Подробнее о перегрузке методов в Java
Введение в классы и объекты для абсолютных новичков, включая небольшие разделы о методах и перегрузке методов.
Узнайте больше о том, почему важно, что Java является строго типизированным языком и изучите примитивные типы Java.
Изучите ограничения и недостатки перегрузки методов, а также способы их устранения путем использования пользовательских типов и объектов параметров.
shishmakov
Скажите пожалуйста, а где вы взяли, что у boolean размер 1 бит? Не вижу такого в JLS: docs.oracle.com/javase/specs/jls/se11/html/jls-4.html
Возможно это дело JVM?
rjhdby
Подозреваю, что имелся в виду «значащий» размер. Но так-то конечно да — в обычной жизни меньше байта не целесообразно.