Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Перегружая методы, соблюдайте осторожность



 

Приведем пример попытки классифицировать коллекции по признаку - Haбop, список или другой вид коллекций,- предпринятой из лучших побуждений:

 

 

120

 

 

// Ошибка: неверное использование перезагрузки!

public class CollectionClassifier {

public static String classify(Set s) {

 return "Set";  }

public static String classify(List 1) {

return "List"; }

public static String classify(Collection с) {

return "Unknown Collection"; }

public static void main(String[] args) {

Collection[] tests = new Collection[] {

                          new HashSet(),              // Набор

                          new ArrayList(),             // Список

                         new HashMap().values() // Не набор и не список

} ;

for (int i = о; i < tests.length; i++) System.out.println(classify(tests[i]));

}

Возможно, вы ожидаете, что эта программа напечатает сначала "Set", затем "List" и наконец "Unknown Collection". Ничего подобного! Программа напечатает "Unknown Collection" три раза. Почему это происходит? Потому что метод classify перезагружается (overload), и выбор варианта перезагрузки осуществляется на стадии компиляции. Для всех трех проходов цикла параметр на стадии компиляции имеет один и тот же тип Collection. И хотя во время выполнения программы при каждом проходе используется другой тип, это уже не влияет на выбор варианта пере­загрузки. Поскольку во время компиляции параметр имел тип Collection, может применяться только третий вариант перезагрузки: classify(Collection). И именно этот перезагруженный метод вызывается при каждом проходе цикла.

Поведение этой программы такое странное потому, что выбор перезагруженных методов является статическим, тогда как выбор переопределенных методов ­динамическим. Правильный вариант переопределенного метода выбирается при вы­полнении программы, исходя из того, какой тип в этот момент имеет объект, для которого был вызван метод. Напомним, что переопределение (overrid) метода осуще­ствляется тогда, когда подкласс имеет декларацию метода с точно такой же сигнату­рой, что и у декларации метода предка. Если в подклассе метод был переопределен и затем данный метод был вызван для экземпляра этого подкласса, то выполняться

 

 

121

 

 

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

class A{

String name() { return "A"; }

class В extends A {

String name() { return "В"; }

class С extends A {

String name() { return "С"; }

public class Overriding {

public static void main(String[] args) {

A[] tests = new A[] {new A(), new В(), new С() };

for (int i = 0; i < tests.length; i++) System.out.print(tests[i].name());

}

}

Метод пате декларируется в классе Д и пере определяется в классах В и С. Как и ожидалось, эта программа печатает "ABC", хотя на стадии компиляции при каждом проходе в цикле экземпляр имеет тип Д. Тип объекта на стадии компиляции не влияет на то, какой из методов будет исполняться, когда поступит запрос на вызов переопре­деленного метода: всегда выполняется "самый точный" переопределяющий метод. Сравните это с перезагрузкой, когда тип объекта на стадии выполнения уже не влияет на то, какой вариант перезагрузки будет использоваться: выбор осуществляется на стадии компиляции и всецело основывается на том, какой тип имеют параметры на стадии компиляции.

В примере с ColleetionClassi fier программа должна была определять тип пара­метра, автоматически переключаясь на соответствующий перезагруженный метод на основании того, какой тип имеет параметр на стадии выполнения. Именно это делает метод name в примере "ABC". Перезагрузка метода не имеет такой возможности. Ис­править программу можно, заменив все три варианта перезагрузки метода elassify единым методом, который выполняет явную проверку instaneeOf:

 

public static String classify(Collection c) {

return (c instanceof Set ? "Set" :

(c instancepf List ? "List" : "Unknown Collection")); }

 

 

 

 

122

 

Поскольку переопределение является нормой, а перезагрузка - исключением, именно переопределение задает, что люди ожидают увидеть при вызове метода. Как показал пример CollcetionClassifier, перезагрузка может не оправдать эти ожида­ния. Не следует писать код, поведение которого не очевидно для среднего програм­миста. Особенно это касается интерфейсов API. Если рядовой пользователь АРI не знает, какой из перезагруженных методов будет вызван для указанного набора пара­метров, то работа с таким API, вероятно, будет сопровождаться ошибками. Причем ошибки эти проявятся скорее всего только на этапе выполнения в виде некорректного поведения программы, и многие программисты не смогут их диагностировать. Поэтому необходимо избегать запутанных вариантов перезагрузки.

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

Например, рассмотрим класс ObjectOutputStream. Он содержит варианты мето­дов write для каждого простого типа и нескольких ссылочных типов. Вместо того чтобы перезагружать метод write, они применяют такие сигнатуры, как write­Boolean(boolean), writelnt(int) и writeLong(long). Дополнительное преимущество такой схемы именования по сравнению с перезагрузкой заключается в том, что можно создать методы read с соответствующими названиями, например readBoolean(), readlnt() и readLong(). И действительно, в классе ObjectlnputStream есть методы чтения с такими названиями.

В случае с конструкторами у вас нет возможности использовать различные на­звания, несколько конструкторов в классе всегда подлежат перезагрузке. Правда, в отдельных ситуациях вы можете вместо конструктора предоставлять статический метод генерации (статья 1), но это не всегда возможно. Однако, с другой стороны, при применении конструкторов вам не нужно беспокоиться о взаимосвязи между перезагрузкой и переопределением, так как конструкторы нельзя переопределять. Поскольку вам, вероятно, придется предоставлять несколько конструкторов с одним и тем же количеством параметров, полезно знать, в каких случаях это безопасно.

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

 

 

123

 

Например, класс ArrayList имеет конструктор, принимающий параметр int, и конструктор, принимающий параметр типа Collection. Трудно представить себе условия, когда возникнет путаница с вызовом двух этих конструкторов, поскольку простой тип и ссылочный тип совершенно непохожи. Аналогично, у класса BigInteger есть конструктор, принимающий массив типа byte, и конструктор, принимающий String. Это также не создает путаницы. Типы массивов и классы совершенно непохо­жи, за исключением Object. Совершенно непохожи также типы массивов и интерфей­сы (за исключением Serializable и Cloneable). Наконец, в версии 1.4 класс Throwable имеет конструктор, принимающий параметр String, и конструктор, принимающий параметр Throwable. Классы String и Throwable не родственные, иначе говоря, ни один из этих классов не является потомком другого. Ни один объект, не может быть экземпляром двух неродственных классов, а потому неродственные классы совершен­но непохожи.

Можно привести еще несколько примеров, когда для двух типов невозможно выполнить преобразование ни в ту, ни в другую сторону []LS, 5.1.7]. Однако в сложных случаях среднему программисту трудно определить, который из вариан­тов перезагрузки, если таковой имеется, применим к набору реальных параметров. Спецификация, определяющая, какой из вариантов перезагрузки должен использо­ваться, довольно сложна, и все ее тонкости понимают лишь немногие из программистов [JLS, 15.12.1-3] ..

Иногда, подгоняя существующие классы под реализацию новых интерфейсов, вам приходится нарушать вышеприведенные рекомендации. Например, многие типы значений в библиотеках для платформы Jаvа до появления интерфейса Comparable имели методы соmраrеТо с типизацией (self-tуреd). Представим декларацию исходного метода соmраrеТо с типизацией для класса String:

public int compareTo(String s);

С появлением интерфейса Соmраrable все эти классы были перестроены под реализа­цию данного интерфейса, содержащую новый, более общий вариант метода соmраrеТо со следующей декларацией:

public int соmраrеТо( Object о);

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

public int compareTo(Object о) {

return compareTo((String) о); }

 

 

124

 

 

Аналогичная идиома иногда используется и для методов equals:

 

public boolean equals(Object о) {

return о instanceof String && equals((String)o); }

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

Хотя библиотеки для платформы Java в основном следуют приведенным здесь советам, все же можно найти несколько мест, где они нарушаются. Например, класс String передает два перезагруженных статических метода генерации valueOf(char[]) и valueOf(Object), которые, получив ссылку на один и тот же объект, выполняют совершенно разную работу. Этому нет четкого объяснения, и относиться к данным методам следует как к аномалии, способной вызвать настоящую неразбериху.

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

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 202; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.022 с.)
Главная | Случайная страница | Обратная связь