Вопрос или проблема
Итак, в библиотеке, которую я использую, есть метод get или create, который выглядит так:
A getOrCreateA(Function<U, A>) {}
B extends A
И в моем коде я принимаю Function<U, B>
, но не могу передать его в вышеуказанный метод. Можете объяснить, почему это не работает? Я вижу, почему это не будет работать для списка, но, безусловно, все, что создает B, по своей природе создает A.
Или кто-нибудь может предложить альтернативное решение.
Мой случай использования – это, по сути, специфичный для типа getOrCreate:
public B getOrCreate_b(U ignore, Function<U, B> constructor) {
A return_val = ignore.getOrCreate_a(constructor); //<- ошибка
if (return_val instanceof B)
return (B)return_val;
else
return constructor.apply(ignore);
}
(да, я в курсе, что это на самом деле не сохранит B, если функция возвращает A, с этим разбираются в другом месте)
Java не знает, что второй аргумент Function
по сути является только ‘производящим’. Функция создает B и никогда их не читает. Ни один метод в реализации Function не принимает B в качестве параметра; он появляется только в типе возвращаемого значения.
Но это сегодня. Возможно, завтра кто-то добавит это (никто не добавит; java совместим с предыдущими версиями, но спецификация языка не требует этого и не дает способов для Function
, чтобы заявить, что его второй параметр типа не может появиться в роли потребителя).
Таким образом, java применяет обычные правила к обобщениям, которые являются инвариантными. Если нужен List<Number>
, подойдет только List<Number>
; ни List<Integer>
, ни List<Object>
не подойдут, хотя Integer и является подклассом Number. В конце концов:
List<Integer> ints = new ArrayList<Integer>();
List<Number> nums = ints;
Double d = 5.5;
nums.add(d);
int x = ints.get(0);
Попробуйте осознать этот код, и вы поймете, почему обобщения инвариантны, в то время как базовые типы (не внутри <>
) являются ковариантными (ковариантность = подкласс так же хорош, как и один из его суперклассов).
Решение состоит в том, что функция getOrCreateA
сломана. Ее следовало определить следующим образом:
public <U, A> A getOrCreate(U v, Function<? super U, ? extends A> constructor) {
Обратите внимание на синтаксис ? super
и ? extends
, который говорит: Первый аргумент контрвариантен, второй – ковариантен: только потребляйте первый, только производите второй. Это срабатывает, так как именно так функции на самом деле ‘работают’. Если бы это была сигнатура, ваш код бы сработал. Этот трюк (Function<? super I, ? extends O>
) повсюду в API коллекций java, потому что нет причин этого не делать.
Если вы не можете изменить исходную функцию, вы не сможете это исправить, не углубляясь в неаккуратные преобразования и получая много предупреждений. Это обычно происходит, когда вы используете плохо написанные, сломанные библиотеки. Вы не должны использовать такие библиотеки или сказать их авторам устранить проблемы.
Ответ или решение
Ваш вопрос касается работы с обобщениями (генериками) в Java и специфики их инвариантности. Давайте разберём вашу ситуацию подробнее.
Ваша проблема заключается в том, что метод getOrCreateA
ожидает Function<U, A>
, в то время как вы передаёте Function<U, B>
, где B
является подклассом A
. В Java обобщения являются инвариантными, что означает, что Function<U, B>
не будет рассматриваться как подтип Function<U, A>
, несмотря на то, что B
расширяет A
.
Для объяснения давайте рассмотрим, что означает инвариантность. В обобщениях Java, такие как List<Number>
, требует точно такой же тип, а не подтип, поэтому List<Integer>
не может быть использован в качестве List<Number>
. Это сделано для безопасности типов, чтобы избежать потенциальных проблем, например, если бы вы могли добавить элемент неправильного типа в коллекцию.
Ваш метод getOrCreateA
можно улучшить, чтобы он принимал функцию с более гибкой сигнатурой:
public <U, A> A getOrCreate(U v, Function<? super U, ? extends A> constructor) {
// реализация метода
}
В этой сигнатуре ключевыми моментами являются ? super U
и ? extends A
. Это обозначает, что первая часть (параметр функции) может принимать более высокий уровень иерархии (т.е. U
или его суперархия), а вторая часть (возвращаемый результат функции) будет возвращать подтип A
(например, B
).
Теперь, если вы не имеете доступа для изменения библиотеки и её метода getOrCreateA
, альтернативный подход может включать использование приведения типов, что будет не очень элегантным и может привести к предупреждениям о небезопасном приведении:
public B getOrCreate_b(U ignore, Function<U, B> constructor) {
A return_val = (A) ignore.getOrCreate_a((Function<U, A>) constructor); // ?! потенциально небезопасный код
if (return_val instanceof B)
return (B) return_val;
else
return constructor.apply(ignore);
}
Этот подход работает, но он не является безопасным и рекомендуется использовать его только в тех случаях, когда другие варианты отсутствуют.
Таким образом, оптимальным решением будет предложить авторам библиотеки изменить сигнатуру метода на более универсальную. Если это невозможно, вы можете попробовать использовать приведенные методы, будучи осторожным с потенциальными исключениями, которые могут возникнуть при неправильных приведениях типов.