Философия Java3
Шрифт:
С другой стороны, вызов метода, возвращающего Fruit, безопасен; мы знаем, что все элементы List должны по меньшей мере относиться к Fruit, поэтому компилятор это позволит.
Насколько умен компилятор?
Казалось бы, из всего сказанного следует, что вызов любых методов с аргументами невозможен, но рассмотрим следующий пример:
//: generics/Compi1erIntel 1 igence.java
import java.util .*;
public class Compilerlntelligence {
public static void main(String[] args) { List<? extends Fruit> flist =
Arrays.asList(new AppleO); Apple a = (Apple)flist.get(O); // Без предупреждений fli st. contains (new AppleO); //Аргумент 'Object' fl i st. indexOf (new AppleO); //Аргумент 'Object'
}
} ///:-
Как
Просмотр документации ArrayList показывает, что компилятор не настолько умен. Если add получает аргумент параметризующего типа, contains и in-dexOf получают аргумент типа Object. Таким образом, когда вы указываете ArrayList<? extends Fruit>, аргумент add превращается в «? extends Fruit». По этому описанию компилятор не может определить, какой именно подтип Fruit требуется в данном случае, поэтому не принимает никакие типы Fruit. Даже если вы предварительно преобразуете Apple в Fruit, компилятор все равно откажется вызывать метод (например, add), если в списке аргументов присутствует метасимвол.
У методов contains и indexOf аргументы относятся к типу Object, метасимволы в них отсутствуют, поэтому компилятор разрешает вызов. Это означает, что проектировщик параметризованного класса должен сам решить, какие вызовы «безопасны», и использовать типы Object для их аргументов. Чтобы сделать невозможным вызов при использовании типа с метасимволами, включите параметр типа в список аргументов.
В качестве примера рассмотрим очень простой класс Holder:
//: generics/Holder.java
public class Holder<T> { private T value; public HolderO {}
public Holder(T val) { value = val; } public void set(T val) { value = val; } public T getО { return value; } public boolean equals(Object obj) { return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new AppleO); Apple d = Apple.getO; Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Повышение невозможно Holder<? extends Fruit> fruit = Apple; // OK Fruit p = fruit.getO;
d = (Apple)fruit.getO; // Возвращает 'Object' try {
Orange с = (Orange)fruit.getO; // Предупреждения нет } catch(Exception e) { System.out.println(e); } // fruit.set(new AppleO); // Вызов setO невозможен // fruit.set(new FruitO); // Вызов setO невозможен System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException. Apple cannot be cast to Orange
true
*///:-
Holder содержит метод set, получающий T; метод get, возвращающий Т; и метод equals, получающий Object. Как вы уже видели, Holder<Apple> невозможно преобразовать в Holder<Fruit>, но зато можно в Holder<? extends Fruit>. При вызове get будет возвращен только тип Fruit — то, что известно компилятору по ограничению «все, что расширяет Fruit». Если вы располагаете дополнительной информацией, то сможете выполнить преобразование к конкретному типу Fruit и обойтись без предупреждений, но с риском исключения ClassCastException. Метод set не работает ни с Apple, ни с Fruit, потому что аргумент set тоже содержит «? extends Fruit»; по сути, он может быть чем угодно, а компилятор не
Впрочем, метод equalsQ работает нормально, потому что он получает Object вместо Т. Таким образом, компилятор обращает внимание только на типы передаваемых и возвращаемых объектов. Он не анализирует код, проверяя, выполняются ли реальные операции чтения или записи.
Контравариантность
Также можно пойти другим путем и использовать метасимволы супертипов. В этом случае вы сообщаете, что метасимвол ограничивается базовым классом некоторого класса; при этом используется запись <? super MyClass>, и даже с параметром типа <? super Т>. Это позволяет безопасно передавать типизованный объект параметризованному типу. Таким образом, с использованием метасимволов супертипов становится возможной запись в коллекцию:
//• generics/SuperTypeWiIdcards java
import java util.*:
public class SuperTypeWiIdcards {
static void writeTo(List<? super Apple> apples) { apples add(new AppleO), apples add(new JonathanO); // apples.add(new FruitO); // Ошибка
}
} /// ~
Аргумент apples является контейнером List для некоторого типа, являющегося базовым для Apple; из этого следует, что Apple и производные от Apple типы могут безопасно включаться в контейнер. Но, поскольку нижним ограничением является Apple, мы не знаем, безопасно ли включать Fruit в такой List, так как это откроет List для добавления типов, отличных от Apple, с нарушением статической безопасности типов.
Ограничения супертипов расширяют возможности по передаче аргументов методу:
//. generics/GenericWriting.java
import java.util.*;
public class GenericWriting {
static <T> void writeExact(List<T> list. T item) { list.add(item),
}
static List<Apple> apples = new ArrayList<Apple>;
static List<Fruit> fruit = new ArrayList<Fruit>;
static void flO {
writeExact(apples, new AppleO);
// writeExact(fruit, new AppleO); // Ошибка:
// Несовместимые типы: обнаружен Fruit, требуется Apple
}
static <T> void
writeWithWildcard(List<? super T> list, T item) { list.add(item);
}
static void f20 {
writeWithWildcard(apples, new AppleO); writeWithWildcard(fruit, new AppleO);
}
public static void main(String[] args) { flO, f2; }
} ///.-
Метод writeExact использует параметр типа «как есть», без метасимволов. На примере fl мы видим, что этот способ отлично работает — при условии, что в List<Apple> помещаются только объекты Apple. Однако writeExact не позволяет поместить Apple в List<Fruit>, хотя мы знаем, что это должно быть возможно.
В writeWithWildcard используется аргумент List<? super Т>, поэтому List содержит конкретный тип, производный от Т; следовательно, Т или производные от него типы могут безопасно передаваться в аргументе методов List. Пример встречается в f2: как и прежде, Apple можно поместить в List<Apple>, но, как и предполагалось, также стало можно поместить Apple в List<Fruit>.
Неограниченные метасимволы
Казалось бы, неограниченный метасимвол <?<@062> должен означать «все, что угодно», а его использование эквивалентно использованию низкоуровневого типа. В самом деле, на первый взгляд компилятор подтверждает эту оценку: