JAVA Параметризация метода двумя типами, которые могут быть разными типами и могут совпадать

Есть метод, который параметризован двумя типами:

protected <T, R> Page<T> savePageInSeparatedTransaction(
     Function<Pageable, Page<T>>  sourceSupplier,
     Function<T,  R>   converter,
     Consumer<List<R>> saver,
     Pageable pageRequest
) {

     TransactionTemplate  transactionTemplate = new TransactionTemplate(transactionManager);
     transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
     return transactionTemplate.execute(s -> {

          Page<T> sourcePage = sourceSupplier.apply(pageRequest);

          List<R> converted = converter == null
                ?   (List<R>)  sourcePage.getContent()
                :   sourcePage.stream().map(converter).collect(Collectors.toList());
          saver.accept(converted);
          return sourcePage;
       })

}

В этом методе:
T - исходный тип, а
R - результирующий тип.

конвертер converter передан в метод. Но если converter == null, то значит исходный тип совпадает с результирующим типом.
В таком случае я хочу коллекцию исходного типа sourcePage привести к типу коллекции результирующего типа.
Мне нужно, чтобы эти два дженерика могли быть разными типами и могли быть одним типом.
Но тогда возникает ошибка при компиляции в вызывающем контексте:

Вызов этого метода такой:

savePageInSeparatedTransaction(
    skaRepository::findAll,
    null,
    bufferRepository::insertRecords,
    PageRequest.of(0, 20_000, Sort.Direction.ASC, "id.lim")
);

Текст ошибки компиляции:

reason:  No instance of Type variables R  exist so that List<R> conforms to VmCertEntity

Как, не делая перегрузок, можно исправить метод таким образом, чтобы дженерики могли восприниматься разными типами и одним типом?


Ответы (2 шт):

Автор решения: AP-JavaCod

У вас есть два дженерики <T, R> для java это значит что используются два разных типа данных и нужна явно показать что это один и тот же тип использовать вырожение в виде t -> t вместо null.

→ Ссылка
Автор решения: Nowhere Man

Во-первых, проблема с исходным кодом НЕ воспроизвелась, т.е. ошибка при компиляции НЕ возникла, см. упрощённый пример на online GDB.
В зависимости от выбранной реализации saver могло быть выброшено ClassCastException.

Во-вторых, можно было бы передавать в качестве конвертера функцию идентичности в виде стандартной реализации Function.identity() или лямбды t -> t, как предложил @AP-JavaCod в предыдущем ответе, но тогда проверка на null в представленной реализации savePageInSeparatedTransaction теряет смысл и можно было бы обойтись без неё:

private TransactionTemplate getTransactionTemplate() {
    TransactionTemplate tt = new TransactionTemplate(transactionManager);
    tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    return tt;  
}

protected <T, R> Page<T> savePageInSeparatedTransaction(
    Function<Pageable, Page<T>> sourceSupplier,
    Function<T, R> converter,
    Consumer<List<R>> saver,
    Pageable pageRequest
) {
    Objects.requireNonNull(converter);
    return getTransactionTemplate().execute(s -> {

        Page<T> sourcePage = sourceSupplier.apply(pageRequest);

        List<R> converted = sourcePage.stream()
                .map(converter)
                .collect(Collectors.toList());
        saver.accept(converted);
        return sourcePage;
   });
}

Соответственно, при использовании такого варианта возникает нежелательная конвертация списка посредством функции идентичности, и более логичное решение для одинаковых типов T и R состоит имхо в использовании перегруженной функции с одним типом T без лишнего конвертера в списке аргументов:

protected <T> Page<T> savePageInSeparatedTransaction(
    Function<Pageable, Page<T>> sourceSupplier,
    Consumer<List<T>> saver,
    Pageable pageRequest
) {
    return getTransactionTemplate().execute(s -> {
    
        Page<T> sourcePage = sourceSupplier.apply(pageRequest);
    
        saver.accept(sourcePage.getContent());
        return sourcePage;
    });
}

И такие решения действительно позволят отлавливать некорректные консьюмеры на этапе компиляции:

savePageInSeparatedTransaction(
    repoFoo::findAll,  // returns Page<Foo>
    t -> t,            // Foo -> Foo
    repoBar::saveAll,  // expects Iterable<Bar>
    PageRequest.of(0, 20_000, Sort.Direction.ASC, "id.lim")
);
/*
error: incompatible types: incompatible types: inference variable R has incompatible bounds
        new Main().savePageInSeparatedTransaction(
                                                 ^
    equality constraints: Bar
    lower bounds: Foo
  where R,T are type-variables:
    R extends Object declared in method <T,R>savePageInSeparatedTransaction(Function<Pageable,Page<T>>,Function<T,R>,Consumer<List<R>>,Pageable)
    T extends Object declared in method <T,R>savePageInSeparatedTransaction(Function<Pageable,Page<T>>,Function<T,R>,Consumer<List<R>>,Pageable)
*/
savePageInSeparatedTransaction(
    repoFoo::findAll,  // returns Page<Foo>
    repoBar::saveAll,  // expects Iterable<Bar>
    PageRequest.of(0, 20_000, Sort.Direction.ASC, "id.lim")
);
/*
error: incompatible types: inference variable T has incompatible equality constraints Bar,Foo
*/
→ Ссылка