Обобщения в Java: часть 1 - Верхние пределы

ОГЛАВЛЕНИЕ

Верхние пределы

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

public static <T> T max(T obj1, T obj2)

Использовали бы его, как показано ниже:

System.out.println(max(new Integer(1), new Integer(2)));

Спрашивается, как доделать реализацию метода max()? Попытаемся сделать это:

public static <T> T max(T obj1, T obj2)
{
    if (obj1 > obj2) // ошибка
    {
        return obj1;
    }
    return obj2;
}

Это не сработает. Оператор > не определен в ссылках. Как же тогда сравнить два объекта? Вспоминается интерфейс Comparable(сравнимый). Почему бы не использовать интерфейс comparable для выполнения этой задачи:

public static <T> T max(T obj1, T obj2)
{
    // Не изящный код
    Comparable c1 = (Comparable) obj1;
    Comparable c2 = (Comparable) obj2;

    if (c1.compareTo(c2) > 0)
    {
        return obj1;
    }
    return obj2;
}

Несмотря на то, что этот код работает, есть две проблемы. Во-первых, он некрасив. Во-вторых, приходится учитывать случай, когда приведение к Comparable не удается. Поскольку так сильна зависимость от типа, реализующего этот интерфейс, почему бы не попросить компилятор навязать его? Именно это делают верхние пределы. Ниже приведен код:

public static <T extends Comparable> T max(T obj1, T obj2)
{
    if (obj1.compareTo(obj2) > 0)
    {
        return obj1;
    }
    return obj2;
}

Компилятор проверит, чтобы убедиться, что параметризованный тип, заданный при вызове этого метода, реализует интерфейс Comparable. Если попытаться вызвать max() с экземплярами некоторого типа, не реализующего интерфейс Comparable, выдастся строгая ошибка компиляции.

Подстановочный знак

Перейдем к более интересным принципам обобщений. Рассмотрим следующий пример:

public abstract class Animal
{
    public void playWith(Collection<Animal> playGroup)
    {

    }
}

public class Dog extends Animal
{
    public void playWith(Collection<Animal> playGroup)
    {
    }
}

Класс Animal(животное) имеет метод playWith(), принимающий коллекцию Animal. Dog(собака), расширяющая Animal, переопределяет этот метод. Попробуем использовать класс Dog в примере:

Collection<Dog> dogs = new ArrayList<Dog>();
       
Dog aDog = new Dog();
aDog.playWith(dogs); //ошибка

Здесь создается экземпляр Dog и отправляется коллекция Dog его методу playWith(). Выдается ошибка компиляции:

Error:  line (29) cannot find symbol 
method playWith(java.util.Collection<com.agiledeveloper.Dog>)

Причина состоит в том, что коллекцию Dog нельзя рассматривать как коллекцию Animal, которую ожидает метод playWith() (смотрите раздел “Обобщения и заменяемость” выше). Однако было бы логично иметь возможность отправить коллекцию Dog этому методу, не так ли? Как это сделать? Здесь вступает в дело подстановочный знак или неизвестный тип.

Оба метода playMethod()(в Animal и Dog) изменяются следующим образом:

public void playWith(Collection<?> playGroup)

Collection не имеет тип Animal. Вместо этого она имеет неизвестный тип (?). Неизвестный тип – не Object, он просто неизвестный или неопределенный.
Теперь код:

aDog.playWith(dogs);

компилируется без ошибок. Однако есть проблема. Также можно написать:

ArrayList<Integer> numbers = new ArrayList<Integer>();
aDog.playWith(numbers);

Изменение, сделанное, чтобы позволить отправить коллекцию Dog методу playWith(), теперь позволяет отправить и коллекцию Integer. Если разрешить это, получится странная собака. Как сказать, что компилятор должен разрешать коллекции Animal или коллекции любого типа, расширяющего Animal, но не любую коллекцию других типов? Это позволяет осуществить применение верхних пределов, как показано ниже:

public void playWith(Collection<? extends Animal> playGroup)

Ограничение применения подстановочных знаков состоит в том, что разрешено извлекать элементы из Collection<?>, но нельзя добавлять элементы в такую коллекцию – компилятор не знает, с каким типом имеет дело.