Дженерик: получить первый элемент коллекции произвольного типа

48. Получить первый элемент списка с сохранением типа

Условие задачи:
Необходимо реализовать универсальный метод, который получает первый элемент списка и сохраняет его тип.

Элементы списка могут иметь произвольный тип. Решение должно быть типобезопасным и не требовать явного приведения типов.

Код:

class A {

    public static T getFirstElementOfList(List<T> list) {
        return list.size() > 0 ? list.get(0) : null;
    }
}

Также необходимо объяснить использование wildcard:

method(List<? extends T> producer)

method(List<? super T> consumer)

Спойлеры к решению

Подсказки
💡 Параметр типа метода указывается перед возвращаемым типом: <T>.
💡 Вместо null можно возвращать Optional<T>.
💡 Если список предоставляет значения типа T, используй ? extends T.
💡 Если в список записываются значения типа T, используй ? super T.
💡 Это правило называется PECS: Producer Extends, Consumer Super.

Решение

Типобезопасный вариант с Optional:

class A {

    public static <T> Optional<T> firstOf(
            List<? extends T> list
    ) {
        if (list == null || list.isEmpty()) {
            return Optional.empty();
        }

        return Optional.ofNullable(list.get(0));
    }
}

Пример использования:

List<Integer> numbers = List.of(1, 2, 3);

Optional<Integer> first = A.firstOf(numbers);

first.ifPresent(System.out::println);

Если по контракту допустимо возвращать null:

public static <T> T firstOrNull(
        List<? extends T> list
) {
    if (list == null || list.isEmpty()) {
        return null;
    }

    return list.get(0);
}

Пример producer:

public static <T> void readValues(
        List<? extends T> producer
) {
    for (T value : producer) {
        System.out.println(value);
    }
}

Из List<? extends T> можно безопасно читать элементы как T, но нельзя добавлять новые значения, поскольку конкретный подтип неизвестен.

Пример consumer:

public static <T> void addValue(
        List<? super T> consumer,
        T value
) {
    consumer.add(value);
}

В List<? super T> можно безопасно добавлять значения типа T, но результат чтения доступен только как Object.

Пример с иерархией числовых типов:

List<Integer> integers = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

Optional<Integer> firstInteger = firstOf(integers);
Optional<Number> firstNumber = firstOf(integers);

addValue(numbers, 10);
addValue(objects, 10);

Integer и AtomicInteger являются разными наследниками класса Number:

Object
└── Number
    ├── Integer
    └── AtomicInteger