Почему метод вывода типа 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());
}