Вступление
Java определяет метод как единицу задач, которые может выполнять класс. А правильная практика программирования побуждает нас убедиться, что метод делает одно и только одно .
Также нормально, когда один метод вызывает другой метод при выполнении процедуры. Тем не менее, вы ожидаете, что эти методы будут иметь разные идентификаторы, чтобы различать их. Или, по крайней мере, предположить, что делают их внутренности.
Таким образом, интересно, когда классы начинают предлагать методы с одинаковыми именами - или, скорее, когда они перегружают методы, тем самым нарушая стандарты чистого кода, такие как принцип «не повторяйся» (DRY).
Тем не менее, как будет показано в этой статье, иногда полезны методы с одинаковыми / похожими именами. Они могут повысить интуитивность вызовов API, а при экономном и разумном использовании они могут даже улучшить читаемость кода.
Что такое перегрузка метода?
Перегрузка - это определение нескольких методов с одинаковыми именами в одном классе.
Тем не менее, чтобы избежать двусмысленности, Java требует, чтобы такие методы имели разные сигнатуры , чтобы можно было различать их.
Важно напомнить себе о том, как объявлять метод, чтобы получить точное представление о том, как происходит перегрузка.
Видите ли, Java ожидает, что методы будут содержать до шести частей :
- Модификаторы: например,
public
иprivate
- Тип возврата: например,
void
,int
иString
- Допустимое имя / идентификатор метода
- Параметры ( необязательно )
- Throwables ( необязательно ): например,
IllegalArgumentException
иIOException
- Тело метода
Таким образом, типичный метод может выглядеть так:
public void setDetails(String details) throws IllegalArgumentException {
// Verify whether supplied details string is legal
// Throw an exception if it's not
// Otherwise, use that details string
}
Идентификатор и параметры образуют подпись или объявление метода.
Например, сигнатура метода выше - setDetails(String details)
.
Поскольку Java может различать сигнатуры методов, она может допускать перегрузку методов.
Определим класс с перегруженным методом:
public class Address {
public void setDetails(String details) {
//...
}
public void setDetails(String street, String city) {
//...
}
public void setDetails(String street, String city, int zipCode) {
//...
}
public void setDetails(String street, String city, String zip) {
//...
}
public void setDetails(String street, String city, String state, String zip) {
//...
}
}
Здесь есть метод setDetails()
в нескольких разных формах. Некоторым
требуется только строковая details
, а некоторым - street
, city
,
state
, zip
и т. Д.
Вызов setDetails()
с определенным набором аргументов определит, какой
метод будет вызван. Если никакая подпись не соответствует вашему набору
аргументов, произойдет ошибка компилятора.
Зачем нужна перегрузка метода?
Перегрузка метода полезна в двух основных сценариях. Когда вам нужен класс, чтобы:
- Создать значения по умолчанию
- Захват альтернативных типов аргументов
Возьмем, к примеру, класс Address
public class Address {
private String details;
public Address() {
this.details = String.format(
"%s, %s \n%s, %s", // Address display format
new Object[] { // Address details
"[Unknown Street]",
"[Unknown City]",
"[Unknown State]",
"[Unknown Zip]"});
}
// Getters and other setters omitted
public void setDetails(String street, String city) {
setDetails(street, city, "[Unknown Zip]");
}
public void setDetails(String street, String city, int zipCode) {
// Convert the int zipcode to a string
setDetails(street, city, Integer.toString(zipCode));
}
public void setDetails(String street, String city, String zip) {
setDetails(street, city, "[Unknown State]", zip);
}
public void setDetails(String street, String city, String state, String zip) {
setDetails(String.format(
"%s \n%s, %s, %s",
new Object[]{street, city, state, zip}));
}
public void setDetails(String details) {
this.details = details;
}
@Override
public String toString() {
return details;
}
}
Значения по умолчанию
Скажем, вы знаете только street
и city
адреса. Вы должны вызвать
метод setDetails()
с двумя параметрами String
var address = new Address();
address.setDetails("400 Croft Road", "Sacramento");
И, несмотря на получение некоторых деталей, класс все равно будет генерировать подобие полного адреса. Он заполнит недостающие детали значениями по умолчанию.
Таким образом, перегруженные методы снизили требования, предъявляемые к клиентам. Пользователям не обязательно знать адрес полностью, чтобы использовать класс.
Эти методы также создают стандартный способ представления сведений о
классе в удобочитаемой форме. Это особенно удобно при вызове класса
toString()
:
400 Croft Road
Sacramento, [Unknown State], [Unknown Zip]
Как видно из приведенных выше выходных данных, toString()
всегда будет
давать значение, которое легко интерпретировать - без нулей.
Альтернативные типы аргументов
Класс Address
не ограничивает клиентов предоставлением почтового
индекса только в одном типе данных. Помимо приема почтовых индексов в
String
, он также обрабатывает int
.
Таким образом, можно установить Address
, позвонив по одному из
следующих способов:
address.setDetails("400 Croft Road", "Sacramento", "95800");
или же:
address.setDetails("400 Croft Road", "Sacramento", 95800);
Однако в обоих случаях toString
класса выдаст следующее:
400 Croft Road
Sacramento, [Unknown State], 95800
Перегрузка метода против принципа DRY
Конечно, перегрузка метода вводит в класс повторы. И это противоречит самой сути принципа DRY.
У Address
, например, есть пять методов, которые делают примерно то
же самое. Однако при ближайшем рассмотрении вы поймете, что это может
быть не так. Видите ли, каждый из этих методов обрабатывает определенный
сценарий.
public void setDetails(String details) {}
public void setDetails(String street, String city) {}
public void setDetails(String street, String city, int zipCode) {}
public void setDetails(String street, String city, String zip) {}
public void setDetails(String street, String city, String state, String zip) {}
В то время как 1 позволяет клиенту предоставить адрес без ограничения формата, 5 является довольно строгим.
В общей сложности пять методов делают API более удобным. Они позволяют пользователям указывать некоторые детали адреса. Или все. То, что клиент считает удобным.
Таким образом, за счет «сухости» Address
оказывается более читаемым,
чем когда у него есть сеттеры с разными именами.
Перегрузка методов в Java 8+
До Java 8 у нас не было лямбда-выражений , ссылок на методы и т. Д., Поэтому в некоторых случаях перегрузка методов была простым делом.
Скажем, у нас есть класс AddressRepository
, который управляет базой
данных адресов:
public class AddressRepository {
// We declare any empty observable list that
// will contain objects of type Address
private final ObservableList<Address> addresses
= FXCollections.observableArrayList();
// Return an unmodifiable collection of addresses
public Collection<Address> getAddresses() {
return FXCollections.unmodifiableObservableList(addresses);
}
// Delegate the addition of both list change and
// invalidation listeners to this class
public void addListener(ListChangeListener<? super Address> listener) {
addresses.addListener(listener);
}
public void addListener(InvalidationListener listener) {
addresses.addListener(listener);
}
// Listener removal, code omitted
}
Если мы хотим прослушивать изменения в списке адресов, мы должны
присоединить слушателя к ObservableList
, хотя в этом примере мы
делегировали эту процедуру AddressRepository
.
В результате мы удалили прямой доступ к изменяемому ObservableList
.
Видите ли, такое смягчение защищает список адресов от
несанкционированных внешних операций.
Тем не менее, нам необходимо отслеживать добавление и удаление адресов. Итак, в клиентском классе мы могли бы добавить слушателя, объявив:
var repository = new AddressRepository();
repository.addListener(listener -> {
// Listener code omitted
});
Но если вы сделаете это и скомпилируете, ваш компилятор выдаст ошибку:
reference to addListener is ambiguous
both method addListener(ListChangeListener<? super Address>) in AddressRepository and method addListener(InvalidationListener) in AddressRepository match
В результате мы должны включать явные объявления в лямбда-выражения. Мы должны указать на точный перегруженный метод, о котором мы говорим. Следовательно, рекомендуемый способ добавления таких слушателей в Java 8 и более поздних версиях:
// We remove the Address element type from the
// change object for clarity
repository.addListener((Change<?> change) -> {
// Listener code omitted
});
repository.addListener((Observable observable) -> {
// Listener code omitted
});
Напротив, до Java 8 использование перегруженных методов было
однозначным. Например, при добавлении InvalidationListener
мы
использовали бы анонимный класс.
repository.addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
// Listener handling code omitted
}
});
Лучшие практики
Чрезмерное использование перегрузки методов - это запах кода .
Возьмем случай, когда разработчик API неправильно выбрал типы параметров при перегрузке. Такой подход ввел бы в заблуждение пользователей API.
Это, в свою очередь, может сделать их код уязвимым для ошибок. Кроме того, практика создает чрезмерную нагрузку на JVM. Они стараются определить точные типы, к которым относятся плохо спроектированные перегрузки методов.
Тем не менее, одно из самых противоречивых применений перегрузки методов
- это когда в нем используются
varargs
, то есть формальные методы с переменной арностью.
Помните, что перегрузка обычно увеличивает количество параметров,
которые может предоставить клиент, поэтому varargs
вносит
дополнительный уровень сложности. Это потому, что они приспособлены к
различным подсчетам параметров - подробнее об этом через секунду.
Ограничьте использование varargs в перегруженных методах
Есть много дизайнерских решений, которые вращаются вокруг того, как лучше всего фиксировать адреса. Например, дизайнеры пользовательского интерфейса пытаются определить порядок и количество полей, которые нужно использовать для сбора таких деталей.
Программисты тоже сталкиваются с загадкой - они должны учитывать, например, количество фиксированных переменных, необходимое для объекта адреса.
Полное определение адресного объекта может, например, содержать до восьми полей:
- жилой дом
- Вход
- Квартира
- улица
- Город
- Состояние
- Почтовый индекс
- Страна
Тем не менее, некоторые дизайнеры пользовательского интерфейса настаивают на том, что сбор этих деталей в отдельных полях не идеален. Они утверждают, что это увеличивает когнитивную нагрузку на пользователей . Таким образом, они обычно предлагают объединить все данные адреса в единой текстовой области.
В результате Address
в нашем случае содержит сеттер, который принимает
один параметр String
details
. Тем не менее, это само по себе не
способствует ясности кода. Вот почему мы перегрузили этот метод, чтобы
охватить несколько полей адреса.
Но помните, что varargs
- отличный способ удовлетворить различное
количество параметров. Таким образом, мы могли бы значительно упростить
код, включив такой метод установки, как:
// Sets a String[]{} of details
public void setDetails(String... details) {
// ...
}
Таким образом, мы позволили бы клиенту класса делать что-то вроде:
// Set the house, entrance, apartment, and street
address.setDetails("18T", "3", "4C", "North Cromwell");
Тем не менее, это создает проблему. Вызвал ли приведенный выше код этот метод:
public void setDetails(String line1, String line2, String state, String zip){
// ...
}
Или это относилось к:
public void setDetails(String... details) {
// ...
}
Короче говоря, как код должен относиться к этим деталям? Нравится конкретные поля адреса или общие сведения?
Компилятор жаловаться не будет. Он не будет выбирать метод переменной арности . Вместо этого происходит то, что разработчик API создает двусмысленность, и это ошибка, ожидающая своего появления. Такие как это:
address.setDetails();
Вызов выше передает пустой массив String ( new String[]{}
). Хотя это
технически не ошибочно, оно не решает ни одной части проблемы домена.
Таким образом, благодаря varargs
код стал подвержен ошибкам.
Однако есть способ решить эту проблему. Он включает в себя создание метода из метода с наибольшим количеством параметров.
В этом случае с помощью метода:
public void setDetails(String line1, String line2, String state, String zip) {
// ...
}
Создавать:
public void setDetails(String line1, String line2, String state, String zip, String... other) {
// ...
}
Тем не менее, вышеприведенный подход неэлегантен. Несмотря на отсутствие ошибок, он только увеличивает подробность API.
Помните об автобоксе и расширении
Теперь предположим, что у нас есть класс Phone
, помимо Address
:
public class Phone {
public static void setNumber(Integer number) {
System.out.println("Set number of type Integer");
}
public static void setNumber(int number) {
System.out.println("Set number of type int");
}
public static void setNumber(long number) {
System.out.println("Set number of type long");
}
public static void setNumber(Object number) {
System.out.println("Set number of type Object");
}
}
Если мы вызовем метод:
Phone.setNumber(123);
Получим результат:
Set number of type int
Это потому, что компилятор сначала setNumber(int)
Но что, если в Phone
не было метода setNumber(int)
? И снова 123
Получаем на выходе:
Set number of type long
setNumber(long)
- второй выбор компилятора. При отсутствии метода с
примитивом int
JVM отказывается от автобокса для расширения. Помните,
Oracle определяет
автобоксинг
как:
... автоматическое преобразование, которое компилятор Java выполняет между примитивными типами и соответствующими им объектными классами-оболочками.
И расширяется как:
Конкретное преобразование из типа
S
в типT
позволяет обрабатывать выражение типаS
во время компиляции, как если бы оно имело типT
Затем давайте удалим метод setNumber(long)
и установим 123
. Phone
выходы:
Set number of type Integer
Это потому, что JVM автоматически преобразует 123
в Integer
из int
.
После удаления setNumber(Integer)
класс печатает:
Set number of type Object
По сути, JVM автобоксы, а затем расширяет int
123
до возможного
Object
.
Заключение
Перегрузка метода может улучшить читаемость кода при осторожном использовании. В некоторых случаях это даже делает решение проблем предметной области интуитивно понятным.
Тем не менее, перегрузка - сложная тактика, которую нужно освоить. Хотя это выглядит как нечто тривиальное в использовании - это совсем не так. Это заставляет программистов учитывать иерархию типов параметров, например - вводить средства автобокса и расширения Java, и перегрузка методов становится сложной средой для работы.
Более того, Java 8 представила новые функции языка, которые усугубили перегрузки методов. Например, использование функциональных интерфейсов в перегруженных методах снижает удобочитаемость API.
Они заставляют пользователей объявлять типы параметров в клиентском методе. Таким образом, это сводит на нет цель перегрузки метода - простоту и интуитивность.
Вы можете найти код, использованный в этой статье, на GitHub .