Казалось бы, из всего сказанного следует, что вызов любых методов с аргументами невозможен, но рассмотрим следующий пример:
//: generics/CompilerIntelligence.java
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
}
Как видите, вызовы contains() и indexOf() с аргументами Apple воспринимаются нормально. Означает ли это, что компилятор действительно анализирует код, чтобы узнать, модифицирует ли некоторый метод свой объект? Просмотр документации ArrayList показывает, что компилятор не настолько умен. Если add() получает аргумент параметризующего типа, contains() и indexOf() получают аргумент типа Object. Таким образом, когда вы указываете ArrayList<? extends Fruit>, аргумент add() превращается в <? extends Fruit>. По этому описанию компилятор не может определить, какой именно подтип Fruit требуется в данном случае, поэтому не принимает никакие типы Fruit. Даже если вы предварительно преобразуете Apple в Fruit, компилятор все равно откажется вызывать метод (например, add()), если в списке аргументов присутствует метасимвол. У методов contains() и indexOf() аргументы относятся к типу Object, метасимволы в них отсутствуют, поэтому компилятор разрешает вызов. Это означает, что проектировщик параметризованного класса должен сам решить, какие вызовы «безопасны», и использовать типы Object для их аргументов. Чтобы сделать невозможным вызов при использовании типа с метасимволами, включите параметр типа в список аргументов. В качестве примера рассмотрим очень простой класс Holder:
//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get();
}
}
//---------------------------------------
//: generics/Holder.java
public class Holder<T> {
private T value;
public Holder() {}
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 Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Повышение невозможно
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Возвращает 'Object'
try {
Orange c = (Orange)fruit.get(); // Предупреждения нет
java.lang.ClassCastException. Apple cannot be cast to Orange
true
</spoiler> Holder содержит метод set(), получающий T; метод get(), возвращающий Т; и метод equals(), получающий Object. Как вы уже видели, Holder<Apple> невозможно преобразовать в Holder<Fruit>, но зато можно в Holder<? extends Fruit>. При вызове get() будет возвращен только тип Fruit — то, что известно компилятору по ограничению «все, что расширяет Fruit». Если вы располагаете дополнительной информацией, то сможете выполнить преобразование к конкретному типу Fruit и обойтись без предупреждений, но с риском исключения ClassCastException. Метод set() не работает ни с Apple, ни с Fruit, потому что аргумент set() тоже содержит «? extends Fruit»; по сути, он может быть чем угодно, а компилятор не может проверить безопасность типов для «чего угодно». Впрочем, метод equals()работает нормально, потому что он получает Object вместо Т. Таким образом, компилятор обращает внимание только на типы передаваемых и возвращаемых объектов. Он не анализирует код, проверяя, выполняются ли реальные операции чтения или записи.
Контравариантность
Также можно пойти другим путем и использовать метасимволы супертипов. В этом случае вы сообщаете, что метасимвол ограничивается базовым классом некоторого класса; при этом используется запись <? super MyClass>, и даже с параметром типа <? super Т>. Это позволяет безопасно передавать типизованный объект параметризованному типу. Таким образом, с использованием метасимволов супертипов становится возможной запись в коллекцию:
//: generics/SuperTypeWildcards.java
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
Аргумент 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 f1() {
writeExact(apples, new Apple());
// writeExact(fruit, new Apple()); // Ошибка:
// Несовместимые типы: обнаружен Fruit, требуется Apple
}
static <T> void
writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) { f1(); f2(); }
}
Метод writeExact() использует параметр типа «как есть», без метасимволов. На примере f1() мы видим, что этот способ отлично работает — при условии, что в List<Apple> помещаются только объекты Apple. Однако writeExact() не позволяет поместить Apple в List<Fruit>, хотя мы знаем, что это должно быть возможно. В writeWithWildcard() используется аргумент List<? super Т>, поэтому List содержит конкретный тип, производный от Т; следовательно, Т или производные от него типы могут безопасно передаваться в аргументе методов List. Пример встречается в f2: как и прежде, Apple можно поместить в List<Apple>, но, как и предполагалось, также стало можно поместить Apple в List<Fruit>.