Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 5 Средства обобщенного программирования (Generics)

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

//Использование необработанных типов для неизвестных типов

//элементов - так делать не стоит!

static int numElementsInCommon(Set s1, Set s2) { int result = 0;

for (Object o1 : s1)

if (s2.contains(o1)) result++;

return result;

}

Этот метод работает, но он использует необработанные типы, что опасно. Релиз Java 1.5 предоставляет безопасную альтернативу, извест­ ную под названием несвязанный тип подстановки (unbound wildcard type). Если вы хотите использовать обобщенные типы, но не знаете или вам не важны актуальные параметры типов, то вы можете использовать вместо этого знак вопроса. Например, несвязанным типом подстановки для обобщенного типа Set<E> будет Set<?> (читается как «набор како­ го-то типа»). Это наиболее общий тип Set с параметрами, способный содержать любой набор. Вот пример как метод nurnElementsInCommon выглядит с использованием несвязанного типа подстановки:

// Unbounded wildcard type - typesafe and flexible static int numElementsInCommon(Set<?> si, Set<?> s2) {

int result = 0;

for (Object o1 : s1)

if (s2.contains(o1)) result++;

return result;

}

160

С татья 23

Вчем разница между несвязанным типом подстановки Set<?>

инеобработанным типом Set? Дает ли нам что-либо знак вопроса(?)? Об этом можно не задумываться, здесь понятно, что тип под­ становки безопасен, а необработанный тип нет. Вы можете добавить

вколлекцию любой элемент, используя необработанный тип, с легко­ стью повредив инварианты типа коллекции (как показано на примере метода unsafeAdd), но вы не можете добавить любой элемент (кро­ ме нулевого) в коллекцию Collection<?>. При попытке сделать это на этапе компиляции будет выведено сообщение об ошибке:

Wildcard.java:13: cannot find symbol symbol : method add(String)

location: interface Collection<capture#825 of ?> c.add(«verboten»);

л

Данное сообщение недостаточно информативно, но компилятор выполнил свою задачу, защитив инварианты от повреждения. Вы не только не можете добавить любой элемент (кроме нулевого) в Col­ lect ion<? >, но также не можете ничего предположить относительно типа объекта, который извлечете. Если эти ограничения неприемле­ мы, то можно использовать обобщенные методы (generic methods) (статья 27) или связанные типы подстановки (bounded wildcard types) (статья 28).

Есть два небольших исключения из правила: не использовать необработанные типы в новом коде, оба они основаны на факте, что информация об обобщенных типах стирается при запуске (ста­ тья 25). Вы должны использовать необработанные типы в лите­ ральных константах класса или литералах. Спецификация не раз­ решает использование типов с параметрами в данном случае (хотя разрешает типы массивов и примитивные типы) [JLS 15.8.2]. Другими словами, List.class, St ring[].class использовать разрешено,

но List<String>. class и List<?>. class не разрешено.

Второе исключение из правила связано с оператором instanced. Поскольку информация об обобщенном типе стирается при выпол­

161

Глава 5 Средства обобщенного программирования (Generics)

нении, то не разрешается использовать оператор instanceof в типах с параметрами, за исключением несвязанных типов подстановки. И с­ пользование несвязанных типов подстановки вместо необработанных типов никогда не влияет на поведение оператора instanceof. В данном случае угловые скобки и знаки вопроса излишни. Вот как предпоч­ тительнее использовать оператор instanceof с обобщенными типами:

// Разрешенное использование необработанных типов - оператор instanceof if (о instanceof Set) { // Raw type

Set<?> in = (Set<?>) o; // Wildcard type

}

Обратите внимание, что как только вы определите о как Set, то должны передать его типу подстановки Set<?>, а не необработан­ ному типу Set. Это правильная передача параметра и не приведет к предупреждениям со стороны компилятора.

Подводя итоги, можно сказать, что использование необработан­ ных типов может привести к ошибке при выполнении. Не исполь­ зуйте их в новом коде. Они существуют лишь для совместимости и взаимодействия с кодом, написанным до появления средств обоб­ щенного программирования. В качестве небольшого резюме: Set<0bject> — это тип с параметрами, который может содержать объекты любого типа, Set<?> — тип подстановки, который может содержать только объекты некоторого неизвестного типа, и Set — это необрабо­ танный тип, который лишает нас возможности использовать систему обобщенных типов. Первые два безопасны, а вот последний - нет.

Термины, представленные в этой статье (и некоторые представ­ ленные в других местах этой главы), сведены в следующую таблицу:

lepM U H

Пример

С т ат ья

Тип с параметрами

List < Strin g>

С татья

23

А ктуальны й параметр

String

С татья

23

типа

 

 

 

О бобщ енный тип

L i s t < E >

С татьи 23 , 2 6

162

С татья 24

Термин

 

Пример

С т ат ья

Ф орм альн ы й параметр

 

Е

С татья 23

типа

 

 

 

Н есвязанны й тип

 

L is t < ? >

С татья 23

подстановки

 

 

 

Н еобработанный тип

 

List

С татья 23

Связанны й параметр

< Е extends N u m b er>

С татья 2 6

типа

 

 

 

Рекурсивная связка

< Т extends Com parable < T > >

С татья 27

типа

 

 

 

Связанны й тип

L is t< ?

Extends N um ber>

С татья 2 8

подстановки

 

 

 

О бобщ енны й метод

Static < E >

L i s t < E > a sL ist(E [] a)

С татья 27

М етка типа

 

String.class

С татья 2 9

S0

Избегайте предупреждений о непроверенном коде

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

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

163

Глава 5 Средства обобщенного программирования (Generics)

Set<Lark> exaltation = new HashSet();

Компилятор аккуратно напомнит вам, что вы сделали не так:

Venery.java:4: warning: [unchecked] unchecked conversion found : HashSet, required: Set<Lark>

Set<Lark> exaltation - new HashSetO;

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

Set<Lark> exaltation = new HashSet<Lark>();

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

Если же вы не можете избежать предупреждений, но сами уве­ рены, что код, о котором вы получили предупреждение, безопасен, то тогда (и только тогда) можно скрыть предупреждения с помощью

аннотации @SupressWarnings(«imchecked»). Если вы скроете ошиб­

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

Аннотация SupressWarnings может использоваться с детали­ зацией любого уровня, от декларирования локальных переменных

164

С татья 24

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

Если вы все же используете аннотацию SupressWarnings для ме­ тода или конструктора длиной более чем в одну строку, то, возможно, сможете свести его до применения на одном декларировании локаль­ ной переменной. Возможно, вам понадобится декларировать новую локальную переменную, но это стоит того. Например, рассмотрим это на методе toArray, который идет из ArrayList:

public <Т> Т[] toArray(T[] а) { if (a.length < size)

return (Т[]) Arrays.copyOf(elements, size, a.getClassO); System.arraycopy(elements, 0, a, 0, size);

if (a.length > size) a[size] = null; return a;

}

Если вы скомпилируете ArrayList, метод выведет следующее предупреждение:

ArrayList.java:305: warning: [unchecked] unchecked cast found : Object[], required: T[]

return (T[ ]) Arrays. copyOf (elements, size, a.getClassO);

He разрешено применять аннотацию SupressWarnings на вы­ ражении возврата, потому что это не декларирование [JLS, 9.7]. Вы можете захотеть поместить аннотацию на весь метод, но делать этого не надо. Вместо этого декларируйте локальную переменную для хранения возвращаемого значения и аннотацию поместите на де­ кларирование, например:

// Adding local variable to reduce scope of @SuppressWarnings public <T> T[] toArray(T[] a) {

if (a.length < size) {

165

Глава 5 Средства обобщенного программирования (Generics)

II This cast

is correct

because the

array we' re creating

// is of the

same type

as the

one passed in, which is T[].

@SuppressWarnings(“unchecked”) T[]

result =

(T[]) Arrays.copyOf(elements,

size,

a.getClass( ));

return result

 

 

 

System.arraycopy(elements, 0, a, 0, size); if (a.length > size)

a[size] = null;

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

Каждый раз при использовании аннотации @SupressWarn- ings(«unchecked») добавляйте комментарий, в котором объясняйте, почему так делать в данном случае безопасно. Это поможет другим понять код и, что более важно, уменьшит шанс того, что код будет изменен и вычисления станут небезопасными. Если вам сложно будет написать такой комментарий, все равно подумайте, как это сделать. В конце концов вы можете прийти к выводу, что данная операция не безопасна в принципе.

Подводя итоги, можно сказать, что предупреждения о непроверен­ ном коде важны. Не стоит их игнорировать. Каждое такое предупре­ ждение представляет собой потенциальную предпосылку возникновения ошибки ClassCastException при выполнении. Сделайте все возмож­ ное, чтобы избежать их. Если же избежать не удается и вы уверены, что код безопасен, скройте предупреждения аннотацией @SupressWarn- ings(«inchecked») на минимальном диапазоне. Опишите в комментарии причину, почему вы приняли решение скрыть предупреждения.

Предпочитайте списки массивам

Массивы отличаются от средств обобщенного программирова­ ния в двух важных аспектах. Во-первых, массивы ковариантны. Это

166

С тать я 25

жуткое слово значит просто, что если Sub является подтипом Super, тогда тип массива Sub[] является подтипом Supeг[ ]. Средства обоб­ щенного программирования, напротив, инвариантны: для любых двух отдельных типов Туре1 иТуре2, List<Type1>He является ни подтипом, ни превосходящим типом для List<Type2> [JLS, 4.10; Naftalin07, 2.5]. Вы можете подумать, что это недостаток средств обобщенного про­ граммирования, но, напротив, недостатками обладают массивы.

Этот фрагмент кода разрешен:

// Выводит ошибку при запуске!

Object[] objectArray = new Long[1];

objectArray[0] = “I don’t fit in”; // Выводит сообщение ArrayStoreException

Аэтот — нет:

//He компилировать!

List<0bject> ol = new ArrayList<Long>(); // Несовместимые типы ol.add(“I don’t fit in”);

В любом случае вы не можете вставить String в контейнер Long, но, применив массив, увидите, что сделали ошибку только при вы­ полнении, в то время как использование списка приведет к ошибке на этапе компиляции.

Вторым важным отличием массивов от средств обобщенного программирования является то, что массивы материальны[JLS, 4.7]. Это значит, что массивы знают и выполняют свои типы элементов при выполнении. Как выше было сказано, если вы попытаетесь со­ хранить String в массив Long, вы получите сообщение об ошибке ArrayStoreException. Средства обобщенного программирования, на­ против, реализуются стиранием [JLS, 4.6]. Это значит, что они вы­ полняют свои ограничения типов только на этапе компиляции и затем выбрасывают (или стирают) информацию о типах элементов при вы­ полнении. Стирание позволяет обобщенным средствам легко взаимо­ действовать с разрешенным кодом, который не использует средства обобщенного программирования (статья 23).

167

Глава 5 Средства обобщенного программирования (Generics)

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

не является разрешенным: newl_ist<E>[ ], newList<St ring>[ ], new E [].

Все выражения приведут к ошибкам создания обобщенных масси­ вов на этапе компиляции.

Почему нельзя создавать обобщенные массивы? Потому что это небезопасно. Если бы это было разрешено, приведение типов, гене­ рируемое компилятором в правильно написанной программе, вызы­ вало бы ошибку при выполнении ClassCastException. Это бы нару­ шило фундаментальные гарантии, которые дает система обобщенных

ТИ П О В.

Чтобы конкретнее разъяснить это, рассмотрим следующий фраг­ мент кода:

// Почему создание обобщенных массивов не разрешено - не компилировать!

List<String>[] stringLists = new List<String>[1]; // (1) List<Integer> intList = Arrays.asList(42); // (2) Object[] objects = stringLists; // (3)

objects[0] = intList; // (4)

String s = stringLists[0].get(O); // (5)

Представим, что строка 1, создающая обобщенный массив, раз­ решена. Строка 2 создает и инициализирует List<Integer>, содержа­ щий единственный элемент. Строка 3 сохраняет массив List<String> в переменную массива Object, что разрешено, потому что масси­ вы ковариантны. Строка 4 сохраняет List<Integer> в единствен­ ный элемент массива Object, что тоже закончится удачно, потому что обобщенные типы реализуются стиранием: выполняемым типом экземпляра List<Integer> является просто List и выполняемым ти­ пом экземпляра List<String>[] является List[], такое назначение не приводит к ошибке ArrayStoreException. Теперь у нас проблема. Мы сохранили экземпляр List<Integer> в массив, который деклари­ рован, чтобы хранить только экземпляры List<String>. В строке 3

168

С татья 25

мы выводим один элемент из списка в этом массиве. Компилятор ав­ томатически передаст извлеченный элемент в String, но он на самом деле Integer, так что мы получим при выполнении ошибку ClassCastException. Чтобы такого не случилось, строка 1 (создающая обоб­ щенный массив) выдает ошибку при компиляции.

Типы, такие как Е, List<E> и List<String>, известны под тех­ ническим названием нематериальных типов [JLS, 4.7]. Говоря ин­ туитивно, нематериальные типы — это такие типы, представление которых содержит меньше информации при выполнении, чем при компиляции. Единственными материальными типами с параметрами являются несвязанные типы подстановки, такие как List<?> и Мар<?, ?> (статья 23). Разрешено, хотя и не очень полезно, создание масси­ вов несвязанных типов подстановки.

Запрет на создание обобщенных массивов может вызвать недоуме­ ние. Это означает, что невозможно обобщенным типам вернуть массив этого типа элемента (но см. статью 29 для частичного решения). Это также означает, что вы можете получить запутанные предупреждения при использовании методов varargs (статья 42), как как каждый раз при запуске метода varargs создается массив для хранения параметров этого метода. Если тип элемента в этом массиве не является матери­ альным, то вы получите предупреждение. Не многое можно сделать, чтобы избежать этих предупреждений, — остается только их скрыть (статья 24) и избегать одновременного использования средств обоб­ щенного программирования и методов varargs в API.

Когда будет выведена ошибка при создании обобщенного масси­ ва, лучшим решением будет использование типа коллекции List<E> вместо типа массива Е[]. Вы можете пожертвовать частично произ­ водительностью или краткостью, но взамен вы получите большую безопасность типов и взаимодействие.

Например, предположим, что у вас есть синхронизированный список (который выводится Collection.synchronizedList) и функ­ ция, которая берет два значения из типов, хранящихся в списке, и возвращает третье. Теперь предположим, что вы хотите написать

169

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]