40 votes

Java 8 échoue dans l'inférence pour les lambdas lanceuses d'exceptions

J'ai une question concernant l'inférence de Java 8 en ce qui concerne les lambdas et les signatures d'exception correspondantes.

Si je définis une méthode foo :

public static <T> void foo(Supplier<T> supplier) {
    //some logic
    ...
}

puis j'obtiens la sémantique agréable et concise d'être capable d'écrire foo(() -> getTheT()); dans la plupart des cas, pour un T . Cependant, dans cet exemple, si mon getTheT L'opération déclare qu'elle throws Exception , mon foo qui prend un fournisseur ne se compile plus : la signature de la méthode Fournisseur pour la méthode get ne lève pas d'exceptions.

Il semble qu'une façon décente de contourner ce problème serait de surcharger foo pour qu'il accepte l'une ou l'autre option, avec la définition surchargée étant :

public static <T> void foo(ThrowingSupplier<T> supplier) {
   //same logic as other one
   ...
}

où ThrowingSupplier est défini comme suit

public interface ThrowingSupplier<T> {
   public T get() throws Exception;
}

De cette façon, nous avons un type de fournisseur qui lève des exceptions et un autre qui ne le fait pas. La syntaxe souhaitée serait quelque chose comme ceci :

foo(() -> operationWhichDoesntThrow()); //Doesn't throw, handled by Supplier
foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier

Toutefois, cela pose des problèmes en raison de l'ambiguïté du type lambda (probablement incapable de faire la distinction entre Supplier et ThrowingSupplier). En effectuant un cast explicite à la foo((ThrowingSupplier)(() -> operationWhichThrows())); fonctionnerait, mais il se débarrasserait de la plupart de la concision de la syntaxe souhaitée.

Je suppose que la question sous-jacente est la suivante : si le compilateur Java est capable de résoudre le fait que l'une de mes lambdas est incompatible parce qu'elle lève une exception dans le cas du fournisseur seulement, pourquoi n'est-il pas capable d'utiliser cette même information pour dériver le type de la lambda dans le cas secondaire d'inférence de type ?

Toute information ou ressource que vous pourriez m'indiquer serait également très appréciée, car je ne sais pas trop où chercher pour obtenir plus d'informations sur le sujet.

Merci !

30voto

Brian Goetz Points 6062

Si cela peut vous rassurer, ce sujet a en effet été soigneusement examiné lors du processus de conception de la JSR-335.

La question n'est pas "pourquoi ce n'est pas possible", mais "pourquoi avons-nous choisi de ne pas le faire". Lorsque nous trouvons plusieurs surcharges potentiellement applicables, nous aurions certainement pu choisir d'attribuer spéculativement le corps lambda sous chaque ensemble de signatures, et élaguer les candidats pour lesquels le corps lambda n'a pas réussi à vérifier le type.

Cependant, nous avons conclu que cela ferait probablement plus de mal que de bien ; cela signifie, par exemple, que de petites modifications du corps de la méthode, en vertu de cette règle, pourraient entraîner un changement silencieux de certaines décisions de sélection de surcharge de méthode sans que l'utilisateur le veuille. En fin de compte, nous avons conclu que le fait d'utiliser la présence d'erreurs dans le corps de la méthode pour écarter un candidat potentiellement applicable entraînerait plus de confusion que d'avantages, d'autant plus qu'il existe une solution de rechange simple et sûre - fournir un type cible. Nous avons estimé que la fiabilité et la prévisibilité l'emportaient ici sur la concision optimale.

5voto

bayou.io Points 3680

Tout d'abord, vous n'êtes pas obligé de surcharger :D - la surcharge n'est jamais une nécessité ; utilisez 2 noms de méthodes différents, par ex. foo y fooX

Deuxièmement, je ne vois pas pourquoi vous avez besoin de 2 méthodes ici. Si vous voulez gérer différemment les exceptions vérifiées et non vérifiées, vous pouvez le faire au moment de l'exécution. Pour obtenir la "transparence des exceptions", vous pouvez faire ce qui suit

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

Enfin, la désambiguïsation peut être obtenue par le type de retour lambda ; le compilateur utilise le type de retour comme s'il s'agissait d'un type d'argument pour choisir la méthode la plus spécifique. Cela nous donne l'idée d'envelopper la valeur avec le type d'exception, comme suit Result<T,X> une "monade" comme on dit.

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");
}

2voto

augray Points 537

Tout lambda qui pourrait être accepté comme un Supplier<T> peut également être accepté comme un ThrowingSupplier<T> . Ce qui suit se compile :

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());
} 

Compte tenu de ce qui précède, vous n'en avez probablement pas besoin, mais si foo doit accepter une Supplier<T> vous pouvez toujours envelopper la méthode qui déclenche l'exception dans une méthode qui la transforme en une exception non vérifiée :

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());
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X