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

0

 

Приведем пример попытки классифицировать коллекции по признаку – 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"));  }

 

 

 

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

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

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

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

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

 

Например, класс 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), которые, получив ссылку на один и тот же объект, выполняют совершенно разную работу. Этому нет четкого объяснения, и относиться к данным методам следует как к аномалии, способной вызвать настоящую неразбериху.

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

 

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии