Выбрать главу

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

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

МуТуре aReference;

так как заранее неизвестно, сколько таких ссылок реально потребуется.

В большинстве языков существуют некоторые пути решения этой крайне насущной задачи. В Java предусмотрено несколько способов хранения объектов (или, точнее, ссылок на объекты). Встроенным типом является массив, который мы уже рассмотрели. Библиотека утилит Java (Java.utiL*) также содержит дос­таточно полный набор классов контейнеров (также известных, как классы кол­лекций, но, поскольку имя Collection (коллекция) используется для обозначения определенного подмножества библиотеки Java, я буду употреблять общий тер­мин «контейнер»). Контейнеры обладают весьма изощренными возможностями для хранения объектов и работы с ними, и с их помощью удается решить огром­ное количество задач.

Параметризованные и типизованные контейнеры

Одна из проблем, существовавших при работе с контейнерами до выхода Java SE5, заключалась в том, что компилятор позволял вставить в контейнер объект неверного типа. Для примера рассмотрим один из основных рабочих контейнеров

ArrayList, в котором мы собираемся хранить объекты Apple. Пока рассматривайте ArrayList как «автоматически расширяемый массив». Работать с ним несложно: создайте объект, вставляйте объекты методом add(), обращайтеь к ним мето­дом get(), используйте индексирование — так же, как для массивов, но без квад­ратных скобок. ArrayList также содержит метод size(), который возвращает теку­щее количество элементов в массиве.

В следующем примере в контейнере размещаются объекты Apple и Orange, которые затем извлекаются из него. Обычно компилятор Java выдает предупре­ждение, потому что в данном примере не используется параметризация, однако в Java SE5 существует специальная директива @SuppressWarnings для подавле­ния предупреждений. Директивы начинаются со знака @ и могут получать ар­гументы; в данном случае аргумент означает, что подавляются только «непро­веряемые» предупреждения:

//: hoiding/ApplesAndOrangesWithoutGenerics. java

// Простой пример работы с контейнером

// (компилятор выдает предупреждения).

// {ThrowsException}

import java.util.*;

class Apple {

private static long counter; private final long id = counter++; public long id() { return id; }

}

class Orange {}

public class ApplesAndOrangesWithoutGenerics { @SuppressWarni ngs("unchecked") public static void main(String[] args) {

ArrayList apples = new ArrayListO; for(int i = 0; i < 3; i++)

apples, add (new AppleO); // He препятствует добавлению объекта Orange: apples.add(new OrangeO); for(int i = 0; i < apples.size(). i++) ((Apple)apples.get(i)).id();

// Объект Orange обнаруживается только во время выполнения

}

}

///:-

Директивы Java SE5 будут рассмотрены позднее.

Apple и Orange — совершенно разные классы; они не имеют ничего общего, кроме происхождения от Object (напомню: если в программе явно не указан ба­зовый класс, то в этом качестве используется Object). Так как в ArrayList хранят­ся объекты Object, метод add() может добавлять в контейнер не только объекты Apple, но и Orange, без ошибок компиляции или времени выполнения. Но при вызове метода get() класса ArrayList вы вместо объекта Apple получаете ссылку на Object, которую необходимо преобразовать в Apple. Все выражение должно быть заключено в круглые скобки, чтобы преобразование было выполнено

Параметризованные и типизованные контейнеры 279

перед вызовом метода id() класса Apple. Во время выполнения, при попытке преобразования объекта Orange в Apple, произойдет исключение.

В главе «параметризованные типы» вы узнаете, что создание классов, исполь­зующих механизм параметризации, может быть довольно сложной задачей. С другой стороны, с применением готовых параметризованных классов проблем обычно не бывает. Например, чтобы определить объект ArrayList, предназначен­ный для хранения объектов Apple, достаточно использовать вместо имени ArrayList запись А г ray Li s t< A p p le>. В угловых скобках перечисляются параметры типов (их может быть несколько), указывающие тип объектов, хранящихся в данном экземпляре контейнера.

Механизм параметризации предотвращает занесение объектов неверного типа в контейнер на стадии компиляции. Рассмотрим тот же пример, но с ис­пользованием параметризации:

// hoiding/ApplesAndOrangesWithGenerics java import java.util.*;

public class ApplesAndOrangesWithGenerics { public static void main(String[] args) {

ArrayList<Apple> apples = new ArrayList<Apple>(), forCint i = 0; i < 3; i++)

apples, add (new AppleO); // Ошибка компиляции: // apples.add(new OrangeO); for(int i = 0; i < apples.size(); i++)

System, out. pri ntl n(appl es. get( i) .idO). // Использование синтаксиса foreach: for(Apple с • apples)

System.out.pri nt1n(с.i d());

}

} /* Output: 0 1 2 0 1 2

*///-

На этот раз компилятор не разрешит поместить объекты Orange в контейнер apples, поэтому вы получите ошибку на стадии компиляции (а не на стадии вы­полнения).

Также обратите внимание на то, что выборка данных из List не требует пре­образования типов. Поскольку контейнер знает тип хранящихся в нем элемен­тов, он автоматически выполняет преобразование при вызове get(). Таким обра­зом, параметризация не только позволяет компилятору проверять тип объектов, помещаемых в контейнеры, но и упрощает синтаксис работы с объек­тами в контейнере. Пример также показывает, что, если индексы элементов вам не нужны, для перебора можно воспользоваться синтаксисом foreach.

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

//: hoiding/GenericsAndUpcasting.java import java.util.*;

class   GrannySmith extends Apple {}

class   Gala extends Apple {}

class   Fuji extends Apple {}

class   Braeburn extends Apple {}

public class GenericsAndUpcasting {

public static void main(String[] args) {

ArrayList<Apple> apples = new ArrayList<Apple>();

apples.add(new GrannySmithO);

apples.add(new GalaO);

apples.add(new Fuji()):

apples.add(new BraeburnO);

for(Apple с : apples)

System.out.println(c);

}

} /* Output: (Sample) GrannySmi th@7d772e Gala@llb86e7 Fuji@35ce36 Braeburn@757aef *///:-

Мы видим, что в контейнер, рассчитанный на хранение объектов Apple, мож­но помещать объекты типов, производных от Apple.

В результатах, полученных с использованием метода toStringO объекта Ob­ject, выводится имя класса с беззнаковым шестнадцатеричным представлением хеш-кода объекта (сгенерированного методом hashCode()).

Основные концепции

В библиотеке контейнеров Java проблема хранения объектов делится на две концепции, выраженные в виде базовых интерфейсов библиотеки:

•       Коллекция: группа отдельных элементов, сформированная по некоторым правилам. Класс List (список) хранит элементы в порядке вставки, в клас­се Set (множество) нельзя хранить повторяющиеся элементы, а класс Queue (очередь) выдает элементы в порядке, определяемом спецификой очереди (обычно это порядок вставки элементов в очередь).