Почему метод вывода типа Java 8 не учитывает исключения, выделенные Lambdas при выборе перегрузки?
У меня есть вопрос относительно вывода Java 8 относительно lambdas и связанных с ними исключений.
Если я определю некоторый метод foo:
public static <T> void foo(Supplier<T> supplier) {
//some logic
...
}
тогда я получаю приятную и лаконичную семантику, заключающуюся в возможности писать foo(() -> getTheT());
в большинстве случаев для данного T
. Однако в этом примере, если моя операция getTheT
объявляет, что она throws Exception
, мой метод foo
, который принимает Поставщик, больше не компилируется: подпись метода поставщика для get
не генерирует исключения.
Кажется, что достойным способом обойти это было бы перегрузить foo, чтобы принять любую опцию, причем перегруженное определение:
public static <T> void foo(ThrowingSupplier<T> supplier) {
//same logic as other one
...
}
где ThrowingSupplier определяется как
public interface ThrowingSupplier<T> {
public T get() throws Exception;
}
Таким образом, у нас есть один тип поставщика, который генерирует исключения, а другой - нет. Желаемый синтаксис будет примерно таким:
foo(() -> operationWhichDoesntThrow()); //Doesn't throw, handled by Supplier
foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier
Однако это вызывает проблемы из-за двусмысленности типа лямбда (предположительно неспособного разрешить между Поставщиком и ThrowingSupplier). Выполнение явного приведения a la foo((ThrowingSupplier)(() -> operationWhichThrows()));
будет работать, но оно избавится от большей части краткости желаемого синтаксиса.
Я предполагаю, что основной вопрос заключается в следующем: если компилятор Java способен решить тот факт, что один из моих lambdas несовместим из-за того, что он бросает исключение в случае только для поставщика, почему он не может использовать тот же самый информацию для получения типа лямбда во вторичном случае типа-вывода?
Любая информация или ресурсы, которые могли бы мне указать на меня, также будут высоко оценены, поскольку я просто не слишком уверен, где искать дополнительную информацию по этому вопросу.
Спасибо!
Ответы
Ответ 1
Если вам станет легче, эта тема была действительно тщательно рассмотрена в процессе проектирования JSR-335.
Вопрос не в том, "почему он не способен", а "почему мы решили не делать этого". Когда мы находим несколько потенциально применимых перегрузок, мы, конечно же, могли бы предпочесть атрибутивно привязать тело лямбда к каждому набору подписей и обрезать тех кандидатов, для которых тело лямбда не смогло проверить тип.
Однако мы пришли к выводу, что выполнение этого скорее всего принесет больше вреда, чем пользы; это означает, например, что небольшие изменения в теле метода в соответствии с этим правилом могут привести к тому, что некоторые решения о перегрузке метода будут молчаливо изменяться, если пользователь не намеревается это сделать. В заключение мы пришли к выводу, что использование наличия ошибок в теле метода для отбрасывания потенциально применимого кандидата вызовет больше путаницы, чем пользы, особенно учитывая, что существует простой и безопасный обходной путь - обеспечить целевой тип. Мы чувствовали, что надежность и предсказуемость здесь перевешивают оптимальную последовательность.
Ответ 2
Прежде всего, вам не нужно перегружать: D - перегрузка никогда не нужна; используйте 2 разных имени метода, например. foo
и fooX
Во-вторых, я не понимаю, зачем вам здесь 2 метода. Если вы хотите по-разному обрабатывать проверенные и непроверенные исключения, это можно сделать во время выполнения. Чтобы добиться "прозрачности исключений", вы можете сделать
interface SupplierX<T, X extends Throwable>
{
T get() throws X;
}
<T, X extends Throwable> void foo(Supplier<T, X> supplier) throws X { .. }
foo( ()->"" ); // throws RuntimeException
foo( ()->{ throw new IOException(); } ); // X=IOException
Наконец, двусмысленность может быть достигнута путем возврата типа лямбда; компилятор использует возвращаемый тип, как если бы использовался тип аргумента для выбора наиболее конкретного метода. Это дает нам идею обернуть значение вместе с типом исключения, как Result<T,X>
, "монадой", как говорится.
interface Result<T, X extends Throwable>
{
T get() throws X;
}
// error type embedded in return type, not in `throws` clause
static Result<String, Exception> m1(){ return ()->{ throw new Exception();}; }
static Result<String, RuntimeException> m2(){ return ()->{ return "str"; }; }
// better to have some factory method, e.g. return Result.success("str");
public static void main(String[] args)
{
foo(()->m1()); // foo#2 is not applicable
foo(()->m2()); // both applicable; foo#2 is more specific
}
interface S1<T> { T get(); }
static <T> void foo(S1<Result<T, ? extends Exception>> s)
{
System.out.println("s1");}
}
interface S2<T> { T get(); } // can't have two foo(S1) due to erasure
static <T> void foo(S2<Result<T, ? extends RuntimeException>> s)
{
System.out.println("s2");
}
Ответ 3
Любая лямбда, которая может быть принята как Supplier<T>
, также может быть принята как ThrowingSupplier<T>
. Следующие компиляции:
public static interface ThrowingSupplier<T>{
public T get() throws Exception;
}
public static <T> void foo(ThrowingSupplier<T> supplier) {
}
public static String getAString(){
return "Hello";
}
public static String getAnotherString() throws Exception{
return "World";
}
public static void main(String[] args) {
foo(()->getAString());
foo(()->getAnotherString());
}
Учитывая вышеизложенное, вам, вероятно, это не нужно, но если foo
должен принимать неброска Supplier<T>
, вы всегда можете обернуть метод исключения из метода в методе, который освобождает его в unchecked Exception:
public static <T> void foo(Supplier<T> supplier) {
}
public static String getAString(){
return "Hello";
}
public static String getAnotherString() throws Exception{
return "World";
}
public static String getAnotherStringUnchecked(){
try{
return getAnotherString();
} catch(Exception e){
throw new RuntimeException("Error getting another string",e);
}
}
public static void main(String[] args) throws Exception{
foo(()->getAString());
foo(()->getAnotherStringUnchecked());
}