Преодоление дженериковых паттернов

Я читал об общих правилах get и put, которые должны помешать вам добавить Banana к List<? extends Fruit>:

public abstract class Fruit { }
public class Banana extends Fruit { }
public class Apple extends Fruit { }

List<? extends Fruit> lst = new ArrayList<>();
lst.add(new Banana()); // Compile error "The method add(capture#6-of ? extends Fruit) in the type List<capture#6-of ? extends Fruit> is not applicable for the arguments (Banana)"

Я понимаю, что если у вас не было ошибки компиляции, вы могли бы сделать:

List<? extends Fruit> lst = new ArrayList<>();
lst.add(new Banana());
lst.add(new Apple());

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

Однако, если вы реализуете следующее:

public class ListFruit implements List<Fruit> {

    List<Fruit> list;

    public ListFruit() {
        list = new ArrayList<>();
    }

    @Override public int size() { return list.size(); }
    // All delegates of "list"
    @Override public List<Fruit> subList(int fromIndex, int toIndex) { return list.subList(fromIndex, toIndex); }
}

а затем сделать:

ListFruit lst = new ListFruit();
lst.add(new Banana());
lst.add(new Apple());
for (Fruit fruit : lst)
    System.out.println(fruit.getClass().getName());

вы получаете не ошибку компиляции, а следующий (ожидаемый) вывод:

Banana
Apple

Я нарушаю какой-либо контракт, делая это? Или я обхожу защиту и не должен этого делать?

3 ответа

Решение

Нет, совершенно нормально иметь два разных вида Fruit в List<Fruit>,

Проблема возникает, когда вы на самом деле создали List<Banana>, Например:

List<Banana> bananas = new ArrayList<>();
bananas.add(new Banana());
// This is fine!
List<? extends Fruit> fruits = bananas;
// Calling fruits.get(0) is fine, as it will return a Banana reference, which
// is compatible with a Fruit reference...

// This would *not* be fine
List<Fruit> badFruits = bananas;
badFruits.add(new Apple());
Banana banana = bananas.get(0); // Eek! It's an apple!

Ваш последний код просто отлично, но тот же код работал бы, используя только List<Fruit>, Разница в том, что List<? extends Fruit> может быть List чей родовой тип - это любой фрукт, такой как Appleи вы знаете, что зовет get() на List<Apple> даст вам Apple,

Вполне приемлемо добавить Apple к List<Fruit>, Разница в том, что когда вы вытаскиваете объект из ListЗнаешь только, что это Fruit, а не какой конкретно вид фруктов это.

Рассмотрим случай, когда ваш список является параметром функции, а не локальной переменной:

public void eatFruit(List<? extends Fruit> list)
{
    for (Fruit fruit : list)
    {
        eat(fruit);
    }
}

Вы можете передать List или List этому методу, или даже List. Компилятор знает только то, что "?" в "расширяет Fruit" представляет некоторый класс, который расширяет Fruit, но не то, чем является этот класс. У него нет возможности узнать, является ли Banana экземпляром этого класса, поэтому он не позволит вам добавить его в список. Это позволит вам читать элементы списка и использовать их как фрукты, как я.

Если вы определите список как просто "Список list", то это позволит вам добавить банан в список или любой другой тип фруктов. Но это не позволит вам вызвать функцию и передать List, потому что List может содержать только бананы, но ваша функция может добавить любой тип фруктов.

Весь смысл Generics в том, что если вы создаете List, вы никогда не сможете добавить к нему банан (если вы не приведете его к необработанному списку, не победив цель). Компилятор знает, что ваш список содержит только фрукты определенного типа (возможно, любого типа), но он не знает, что это за тип. Так что это не даст вам ничего добавить к этому. Это единственный способ убедиться, что список остается чистым.

Другие вопросы по тегам