Рейтинговые книги
Читем онлайн Философия Java3 - Брюс Эккель

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 89 90 91 92 93 94 95 96 97 ... 132

Преобразования типов и предупреждения

Преобразование типа или instanceof с параметром типа не приводит ни к какому эффекту. В следующем контейнере данные хранятся во внутреннем представлении в форме Object и преобразуются к Т при выборке:

//. generics/GenericCast.java

class FixedSizeStack<T> { private int index = 0; private Object[] storage; public FixedSizeStackOnt size) {

storage = new Object[size];

}

public void push(T item) { storage[index++] = item; }

@SuppressWarni ngs("unchecked")

public T popО { return (T)storage[--index], }

}

public class GenericCast {

public static final int SIZE = 10; public static void main(String[] args) { FixedSizeStack<String> strings =

new FixedSizeStack<String>(SIZE); for (String s • "А В С D E F G H I J".splitC' "))

strings.push(s), for(int i = 0. i < SIZE; i++) {

String s = strings pop(); System.out.print(s + " ");

} /* Output:

JIHGFEDCBA

*///:-

Без директивы @SuppressWarnings компилятор выдает для рор() предупреждение о «непроверенном преобразовании». Вследствие стирания он не знает, безопасно преобразование или нет, поэтому метод рор() никакого преобразования не выполняет. Т стирается до первого ограничения, которым по умолчанию является Object, так что рор() на самом деле преобразует Object в Object.

Перегрузка

Следующий пример не компилируется, хотя на первый взгляд выглядит вполне разумно:

// generics/UseList java

// {CompileTimeError} (He компилируется)

import java.util.*;

public class UseList<W.T> { void f(List<T> v) {}

void f(List<W> v) {}

} III ~

Перегрузка метода создает идентичную сигнатуру типа вследствие стирания. В таких случаях следует определять методы с различающимися именами:

II. generics/UseList2 java

import java util.*;

public class UseList2<W.T> { void fl(List<T> v) {} void f2(List<W> v) {}

} III.-

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

Резюме

Мне довелось работать с шаблонами С++ с момента их появления. Скорее всего, приведенный далее аргумент я выдвигал в спорах чаще, чем большинство моих единомышленников. Лишь недавно я задумался над тем, насколько в действительности справедлив этот аргумент, — сколько раз проблема, которую я сейчас опишу, проникала в рабочий код?

Аргумент такой: одним из самых логичных мест для использования механизма параметризации являются контейнерные классы: List, Set, Map и т. д. До выхода Java SE5 объект, помещаемый в контейнер, преобразовывался в Object, и информация типа терялась. Если же вы хотели снова извлечь объект из контейнера, его приходилось преобразовывать к нужному типу. Я пояснял происходящее на примере List с элементами Cat (разновидность этого примера с Apple и Orange приведена в начале главы 11). Без параметризованной версии контейнера из Java SE5 вы помещаете и извлекаете из контейнера Object, поэтому в List с элементами Cat легко поместить объект Dog.

Однако версии Java, существовавшие до появления параметризации, не допускали злоупотреблений объектами, помещаемыми в контейнер. Если вы помещали Dog в контейнер Cat, а затем пытались интерпретировать все элементы контейнера как Cat, то при извлечении ссылки на Dog и ее преобразовании к Cat происходило преобразование RuntimeException. Проблема обнаруживалась, пусть и на стадии выполнения, а не во время компиляции.

В предыдущих изданиях книги я писал:

«Это не просто мелкая неприятность, а потенциальный источник трудноуловимых ошибок. Если одна часть (или несколько частей) программы вставляет объекты в контейнер, а в другой части программы обнаруживается, что в контейнер был йомещен недопустимый объект, вам придется искать, где именно была выполнена неверная операция вставки».

Но позже я задумался над этим аргументом, и у меня появились сомнения. Во-первых, насколько часто это происходит? Не помню, чтобы такая ошибка встретилась в моей программе. Когда я спрашивал людей на конференциях, мне тоже не удалось найти никого, с кем бы это случилось. В другой книге использовался пример списка с именем files, содержащего объекты String, — в этом примере казалось абсолютно логичным добавить в список объект типа File, так что объекту, вероятно, стоило присвоить имя fileNames. Какую бы проверку типов ни обеспечивал язык Java, программист все равно может написать малопонятную программу — а плохо написанная программа, даже если она компилируется, все равно остается плохо написанной. Вероятно, нормальный разработчик присвоит контейнеру понятное имя вроде cats, которое послужит предупреждением для программиста, пытающегося занести в контейнер другой объект, отличный от Cat. Но, даже если это и произойдет, как долго такая ошибка останется скрытой? Здравый смысл подсказывает, что исключение произойдет вскоре после начала тестирования с реальными данными.

Один автор даже предположил, что такая ошибка может «оставаться скрытой несколько лет». Но я что-то не помню потока сообщений от людей, у которых возникали проблемы с поиском ошибок «Dog в списке Cat», или хотя бы с их частым появлением. Так неужели такая заметная и довольно сложная возможность, как параметризация, была включена в Java из-за проблем такого рода?

Я считаю, что побудительной причиной для включения параметризации в язык (не обязательно конкретной реализации ее в Java!) является выразительность, а не создание типизованных контейнеров. Типизованные контейнеры — всего лишь побочный эффект возможности создания универсального кода. Таким образом, хотя аргумент «Dog в списке Cat» часто используется для оправдания параметризации, этот аргумент спорен.

Из-за того, что параметризация была «встроена» в Java (а не проектировалась как составная часть языка с самого начала), некоторые контейнеры получились не такими мощными, как хотелось бы. Для примера взгляните на Map, особенно на методы containsKey(Object key) и get(Object key). Если бы эти классы проектировались в расчете на параметризацию, в этих методах вместо Object использовались бы параметризованные типы; тем самым обеспечивались бы необходимые проверки стадии компиляции. Скажем, в аналогичных контейнерах С++ тип ключа всегда проверяется во время компиляции.

Бесспорно, введение любого механизма параметризации в более позднюю версию языка, получившего широкое распространение, — крайне хлопотная затея. В С++ шаблоны были включены в исходную ISO-версию языка, так что они фактически всегда являлись его составной частью. В Java параметризация была введена лишь спустя 10 лет после выхода первой версии. Этот факт породил немало проблем с миграцией кода, а также оказал значительное влияние на архитектуру. В результате программисты страдают из-за близорукости, проявленной проектировщиками языка при создании версии 1.0. Конечно, при создании исходной версии они знали о шаблонах С++ и даже рассматривали возможность включения их в язык, но по тем или иным причинам решили этого не делать (скорее всего, просто торопились). В результате пострадал как язык, так и работающие на нем программисты. Только время покажет, как подход к параметризации в Java отразится на самом языке.

Массивы

В конце главы 5 было показано, как определить и инициализировать массив.

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

Особенности массивов

В Java существует немало разных способов хранения объектов, почему же массивы занимают особое место?

Массивы отличаются от других контейнеров по трем показателям: эффективность, типизация и возможность хранения примитивов. Массивы Java являются самым эффективным средством хранения и произвольного доступа к последовательности ссылок9 на объекты. Массив представляет собой простую линейную последовательность, благодаря чему обращения к элементам осуществляются чрезвычайно быстро. За скорость приходится расплачиваться тем, что размер объекта массива фиксируется и не может изменяться на протяжении его жизненного цикла. Как говорилось в главе 11, контейнер ArrayList способен автоматически выделять, дополнительное пространство, выделяя новый блок памяти и перемещая в него все ссылки из старого. Хотя обычно ArrayList отдается предпочтение перед массивами, за гибкость приходится расплачиваться, и ArrayList значительно уступает по эффективности обычному массиву.

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

До появления параметризации другим контейнерным классам приходилось работать с объектами так, как если бы они не имели определенного типа. Иначе говоря, объекты рассматривались как принадлежащие к типу Object, корневому для всей иерархии классов в Java. Массивы удобнее «старых» контейнеров тем, что массив создается для хранения конкретного типа. Проверка типов на стадии компиляции не позволит использовать неверный тип или неверно интерпретировать извлекаемый тип. Конечно, Java так или иначе запретит отправить неподходящее сообщение объекту на стадии компиляции или выполнения, так что ни один из способов не является более рискованным по сравнению с другим. Просто будет удобнее, если компилятор укажет на существующую проблему, и снижается вероятность того, что пользователь программы получит неожиданное исключение.

1 ... 89 90 91 92 93 94 95 96 97 ... 132
На этой странице вы можете бесплатно читать книгу Философия Java3 - Брюс Эккель бесплатно.
Похожие на Философия Java3 - Брюс Эккель книги

Оставить комментарий