До появления контейнеров Java SE5 одна из главных проблем заключалась в том, что компилятор позволял вставить в контейнер объект неправильного типа. Допустим, вы храните набор объектов Apple
с использованием простейшего контейнера ArrayList
. Пока считайте, что ArrayList
— массив, который автоматически расширяется по мере надобности. Использовать ArrayList
несложно: создайте объект контейнера, вставьте в него объекты методом add()
и обращайтесь к ним методом get()
с указанием индекса точно так же, как с масивами, но без квадрантных скобок. Класс ArrayList
также содержит метод size()
, который сообщает, сколько элементов было добавлено в контейнер, чтобы случайное обращение к элементу за концом массива не привело к ошибке (с выдачей исключения времени выполнения).
В дальнейших моих блогах мы разберемся, насколько серьезна эта проблема. Впрочем, там же будет показано, что полезность обобщенных типов Java не ограничивается контейнерами, безопасными по отношению к типам.
В следующем примере в контейнер помещаются объекты Apple
и Orange
, которые затем извлекаются из него. В общем случае компилятор Java выдает предупреждение, потому что в этом примере обобщенные типы не используются. Для подавления предупреждения используется специальная аннотация Java SE5. Аннотации начинаются со знака @
и могут получать аргумент; в данном случае используется аннотация @SuppressWarnings
, а аргумент указывает, что подавляться должны только «непроверяемые» предупреждения:
// Простой пример использования контейнера (с предупреждением компилятора).
// {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 {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ArrayList apples = new ArrayList();
for(int i = 0; i < 3; i++)
apples.add (new Apple());
// He мешает добавить Orange в apples:
apples.add(new Orange());
for(int i = 0; i < apples.size(); i++)
((Apple)apples.get(i)).id();
// Объект Orange обнаруживается только во время выполнения
}
} /* (Выполните., чтобы увидеть результат) *///:~
Apple
и Orange
— совершенно разные классы; они не имеют ничего общего, кроме того, что оба являются разновидностью Object
(помните: если класс, от которого вы наследуете, не указан явно, то автоматически используется наследование от Object
). Так как контейнер ArrayList
содержит элементы Object
, в него можно добавлять методом add()
не только объекты Apple
, но и объекты Orange; ни компилятор, ни исполнительная среда вам в этом не помешают. Но когда придет время прочитать то, что вы считаете объектом Apple
, с использовнием метода get()
класса ArrayList
, вы получите ссылку на Object
, которую необходимо преобразовать в Apple
. Все выражение заключается в круглые скобки, чтобы преобразование было выполнено перед вызовом метода id()
класса Apple; в противном случае вы получите синтаксическую ошибку.
Во время выполнения попытка преобразовать объект Orange
в Apple
приводит к ошибке в форме исключения (см. выше).
Далее в моих блогах вы узнаете, что создание классов с использованием механизма обобщенных типов Java может быть достаточно нетривиальным делом. Впрочем, использование готовых обобщенных классов обычно проходит весьма прямолинейно. Например, чтобы определить контейнер ArrayList
для хранения объектов Apple, следует использовать запись ArrayList<Apple>
вместо простого имени ArrayList
. В угловые скобки заключаются параметры-типы (их может быть несколько); они определяют типы, которые могут храниться в данном экземпляре контейнера.
Использование обобщенных типов предотвращает помещение неверного типа объекта в контейнер на стадии компиляции. Рассмотрим тот же пример с использованием обобщенных типов:
import java.util.*;
public class ApplesAndOrangesWithGenerics {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<Apple>();
for(int i « 0; i < 3; i++)
apples.add(new Apple());
// Ошибка во время компиляции:
// apples.add(new Orange());
fop(int i * 0; i < apples.size(); i++)
System.out.println(apples.get(i).id());
// Использование синтаксиса foreach: for(Apple c : apples)
System.out.println(c.id());
}
} /* Output:
0
1
2
0
1
2
*///:-
Теперь компилятор не позволяет поместить в apples объект Orange
; таким образом, разработчик узнает об ошибке не во время выполнения, а во время компиляции.
Также обратите внимание на то, что при выборке данных из List
преобразование типа становится лишним. Контейнер знает, какой тип в нем хранится, и автоматически выполняет преобразование при вызове get()
. Таким образом, с обобщенными типами вы не только знаете, что компилятор проверит тип объекта, помещаемого в контейнер, но и можете использовать более компактный синтаксис при выборке объектов из контейнера.
Из приведенного примера также видно, что если вам не требуется использовать индекс каждого элемента, то для последовательной выборки элементов можно использовать синтаксис foreach
.
При помещении объекта в контейнер вы не ограничены точным типом, указанным в параметре обобщенного типа. Восходящее преобразование работает с обобщенными типами точно так же, как и с любыми другими типами:
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 GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn(>);
for(Apple c : apples)
System.out.println(c);
}
} /* Output: (Sample)
GrannySmith@7d772e
Gala@llb86e7
Fuji@35ce36
Braeburn@757aef
*///:-
Таким образом, в контейнер для объектов Apple
можно поместить объект одного из субтипов Apple
.
Выходные данные были получены от реализации метода toString()
класса Object
, которая выводит имя класса с последующим шестнадцатеричным числом без знака — представлением хеш-кода объекта (сгенерированным методом hashCode()
).
Практическое задание. Создайте новый класс с именем
Gerbil
с полемgerbilNumber
типа int, инициализируемым в конструкторе. Определите в нем методhop()
, который выводит значениеgerbilNumber
и короткое сообщение. Создайте контейнерArrayList
и добавьте в него объекты Используйте методget()
для перебора элементов и вызоваhop()
для каждого объектаGerbil
.