web-dev-qa-db-fra.com

Est-il possible de déclarer qu'un fournisseur <T> doit lever une exception?

J'essaie donc de refactoriser le code suivant:

/**
 * Returns the duration from the config file.
 * 
 * @return  The duration.
 */
private Duration durationFromConfig() {
    try {
        return durationFromConfigInner();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

/**
 * Returns the duration from the config file.
 * 
 * Searches the log file for the first line indicating the config entry for this instance.
 * 
 * @return  The duration.
 * @throws FileNotFoundException If the config file has not been found.
 */
private Duration durationFromConfigInner() throws IOException {
    String entryKey = subClass.getSimpleName();
    configLastModified = Files.getLastModifiedTime(configFile);
    String entryValue = ConfigFileUtils.readFileEntry(configFile, entryKey);
    return Duration.of(entryValue);
}

J'ai trouvé ce qui suit pour commencer:

private <T> T getFromConfig(final Supplier<T> supplier) {
    try {
        return supplier.get();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

Cependant, il ne compile pas (évidemment), car Supplier ne peut pas lancer un IOException. Existe-t-il de quelque manière que ce soit je peux ajouter cela à la déclaration de méthode de getFromConfig?

Ou la seule façon de le faire est-elle la suivante?

@FunctionalInterface
public interface SupplierWithIO<T> extends Supplier<T> {
    @Override
    @Deprecated
    default public T get() {
        throw new UnsupportedOperationException();
    }

    public T getWithIO() throws IOException;
}

pdate, je viens de réaliser que l'interface Supplier est une interface vraiment simple, car elle n'a que la méthode get(). La raison initiale pour laquelle j'ai étendu Supplier est de faire prévaloir les fonctionnalités de base, comme les méthodes par défaut par exemple.

26
skiwi

Dans la liste de diffusion lambda, c'était longuement discuté . Comme vous pouvez le voir, Brian Goetz a suggéré que l'alternative est d'écrire votre propre combinateur:

Ou vous pouvez écrire votre propre combinateur trivial:

static<T> Block<T> exceptionWrappingBlock(Block<T> b) {
     return e -> {
         try { b.accept(e); }
         catch (Exception e) { throw new RTE(e); }
     };
}

Vous pouvez l'écrire une fois, en moins du temps qu'il a fallu pour rédiger votre e-mail d'origine. Et de même une fois pour chaque type de SAM que vous utilisez.

Je préfère que nous considérions cela comme du "verre rempli à 99%" plutôt que comme une alternative. Tous les problèmes ne nécessitent pas de nouvelles fonctionnalités linguistiques comme solutions. (Sans oublier que les nouvelles fonctionnalités du langage posent toujours de nouveaux problèmes.)

À cette époque, l'interface Consumer s'appelait Block.

Je pense que cela correspond à réponse de JB Nizet suggéré par Marko ci-dessus.

Plus tard Brian explique pourquoi ceci a été conçu de cette façon (la raison du problème)

Oui, vous devez fournir vos propres SAM exceptionnels. Mais la conversion lambda fonctionnerait bien avec eux.

Le GE a discuté de la prise en charge supplémentaire de la langue et de la bibliothèque pour ce problème, et a finalement estimé qu'il s'agissait d'un mauvais compromis coût/bénéfice.

Les solutions basées sur les bibliothèques provoquent une explosion 2x dans les types SAM (exceptionnels vs non), qui interagissent mal avec les explosions combinatoires existantes pour la spécialisation primitive.

Les solutions basées sur la langue disponibles ont été perdantes d'un compromis complexité/valeur. Bien qu'il existe des solutions alternatives que nous allons continuer à explorer - mais clairement pas pour 8 et probablement pas pour 9 non plus.

En attendant, vous avez les outils pour faire ce que vous voulez. Je comprends que vous préférez que nous vous fournissions ce dernier kilomètre (et, accessoirement, votre demande est vraiment une demande à peine voilée pour "pourquoi ne renoncez-vous pas déjà aux exceptions vérifiées"), mais je pense que l'état actuel permet vous faites votre travail.

15
Edwin Dalorzo
  • Si vous le faites, vous ne pourrez pas l'utiliser comme Supplier car cela ne lèverait que UnsupportedOperationException.

  • Compte tenu de ce qui précède, pourquoi ne pas créer une nouvelle interface et y déclarer la méthode getWithIO?

    @FunctionalInterface
    public interface SupplierWithIO<T> {
        public T getWithIO() throws IOException;
    }
    
  • Peut-être que certaines choses valent mieux que les anciennes interfaces Java? L'ancien style Java n'est pas parti simplement parce qu'il y a Java 8 maintenant.

10
Simon Forsberg

Considérez cette solution générique:

// We need to describe supplier which can throw exceptions
@FunctionalInterface
public interface ThrowingSupplier<T> {
    T get() throws Exception;
}

// Now, wrapper
private <T> T callMethod(ThrowingSupplier<T> supplier) {
    try {
        return supplier.get();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
        return null;
}

// And usage example
String methodThrowsException(String a, String b, String c) throws Exception {
    // do something
}

String result = callMethod(() -> methodThrowsException(x, y, z));
7
Vladimir Dobrikov

Étant donné que j'ai un point supplémentaire à faire à ce sujet, j'ai décidé d'ajouter ma réponse.

Vous avez le choix d'écrire une méthode pratique qui soit:

  1. encapsule un lambda à levée d'exception vérifiée dans un lambda non contrôlé;
  2. simplement appelle le lambda, décochant l'exception.

Avec la première approche, vous avez besoin d'une méthode de commodité par signature de méthode fonctionnelle, tandis qu'avec la deuxième approche, vous avez besoin d'un total de deux méthodes (à l'exception des méthodes à retour primitif):

static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
 static void uncheckRun(RunnableExc r) {
  try { r.run(); } catch (Exception e) { sneakyThrow(e); }
}
interface RunnableExc { void run() throws Exception; }

Cela vous permet d'insérer un appel à l'une de ces méthodes en passant dans un lambda nul, mais qui est fermant tous les arguments du lambda extérieur, que vous passez à votre méthode d'origine. De cette façon, vous pouvez utiliser la fonction de langue conversion lambda automatique sans passe-partout.

Par exemple, vous pouvez écrire

stream.forEachOrdered(o -> uncheckRun(() -> out.write(o)));

par rapport à

stream.forEachOrdered(uncheckWrapOneArg(o -> out.write(o)));

J'ai également constaté que la surcharge du nom de la méthode d'encapsulation pour toutes les signatures lambda conduit souvent à des erreurs expression lambda ambiguë. Vous avez donc besoin de noms distincts plus longs, ce qui entraîne un code char-pour-char toujours plus long qu'avec l'approche ci-dessus.

À la fin, notez que ladite approche n'empêche toujours pas d'écrire des méthodes d'encapsuleur simples qui réutilisent uncheckedRun/Call, mais je l'ai trouvé tout simplement inintéressant car les économies sont au mieux négligeables.

2
Marko Topolnik

J'ai ajouté ma propre solution, pas nécessairement une réponse directe à ma question, qui introduit ce qui suit après quelques révisions:

@FunctionalInterface
public interface CheckedSupplier<T, E extends Exception> {
    public T get() throws E;
}

private <R> R getFromConfig(final String entryKey, final Function<String, R> converter) throws IOException {
    Objects.requireNonNull(entryKey);
    Objects.requireNonNull(converter);
    configLastModified = Files.getLastModifiedTime(configFile);
    return ConfigFileUtils.readFileEntry(configFile, entryKey, converter);
}

private <T> T handleIOException(final CheckedSupplier<T, IOException> supplier) {
    Objects.requireNonNull(supplier);
    try {
        return supplier.get();
    } catch (IOException ex) {
        throw new IllegalStateException("The config file (\"" + configFile + "\") has not been found.");
    }
}

Ce ne sont que des déclarations ponctuelles, maintenant j'ajoute deux variantes du code appelant:

private Duration durationFromConfig() {
    return handleIOException(() -> getFromConfig(subClass.getSimpleName(), Duration::of));
}

private int threadsFromConfig() {
    return handleIOException(() -> getFromConfig(subClass.getSimpleName() + "Threads", Integer::parseInt));
}

Je ne suis pas trop content de convertir un IOException en UncheckedIOException, comme:

  1. J'aurais besoin d'ajouter une variante non vérifiée de chaque méthode qui peut lancer un IOException.
  2. Je préfère une manipulation évidente du IOException, au lieu de compter sur l'espoir de ne pas oublier d'attraper le UncheckedIOException.
1
skiwi

Nous pouvons également utiliser cette solution générique. Tout type d'exception que nous gérons par cette approche.

    @FunctionalInterface
    public interface CheckedCall<T, E extends Throwable> {
        T call() throws E;
    }

    public <T> T logTime(CheckedCall<T, Exception> block) throws Exception {

        Stopwatch timer = Stopwatch.createStarted();
        try {
            T result = block.call();
            System.out.println(timer.stop().elapsed(TimeUnit.MILLISECONDS));
            return result;
        } catch (Exception e) {
            throw e;
        }
    }

0
Deepanshu mishra