Руководство по методам перегрузки в Java

Введение Java определяет метод как единицу задач, которые может выполнять класс. А правильная практика программирования побуждает нас убедиться, что метод делает одно и только одно. Также нормально, когда один метод вызывает другой метод при выполнении процедуры. Тем не менее, вы ожидаете, что эти методы будут иметь разные идентификаторы, чтобы различать их. Или, по крайней мере, предположить, что делают их внутренности. Таким образом, интересно, когда классы начинают предлагать методы с одинаковыми именами - или, скорее, когда t

Вступление

Java определяет метод как единицу задач, которые может выполнять класс. А правильная практика программирования побуждает нас убедиться, что метод делает одно и только одно .

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

Таким образом, интересно, когда классы начинают предлагать методы с одинаковыми именами - или, скорее, когда они перегружают методы, тем самым нарушая стандарты чистого кода, такие как принцип «не повторяйся» (DRY).

Тем не менее, как будет показано в этой статье, иногда полезны методы с одинаковыми / похожими именами. Они могут повысить интуитивность вызовов API, а при экономном и разумном использовании они могут даже улучшить читаемость кода.

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

Перегрузка - это определение нескольких методов с одинаковыми именами в одном классе.

Тем не менее, чтобы избежать двусмысленности, Java требует, чтобы такие методы имели разные сигнатуры , чтобы можно было различать их.

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

Видите ли, Java ожидает, что методы будут содержать до шести частей :

  1. Модификаторы: например, public и private
  2. Тип возврата: например, void , int и String
  3. Допустимое имя / идентификатор метода
  4. Параметры ( необязательно )
  5. Throwables ( необязательно ): например, IllegalArgumentException и IOException
  6. Тело метода

Таким образом, типичный метод может выглядеть так:

 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 , например, есть пять методов, которые делают примерно то же самое. Однако при ближайшем рассмотрении вы поймете, что это может быть не так. Видите ли, каждый из этих методов обрабатывает определенный сценарий.

  1. public void setDetails(String details) {}
  2. public void setDetails(String street, String city) {}
  3. public void setDetails(String street, String city, int zipCode) {}
  4. public void setDetails(String street, String city, String zip) {}
  5. 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 в перегруженных методах

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

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

Полное определение адресного объекта может, например, содержать до восьми полей:

  1. жилой дом
  2. Вход
  3. Квартира
  4. улица
  5. Город
  6. Состояние
  7. Почтовый индекс
  8. Страна

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

В результате 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 .

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus