Is it possible to declare that a Supplier<T> needs to throw an Exception?
Asked Answered
M

6

49

So I am trying to refactor the following code:

/**
 * 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);
}

I came up with the following to start of with:

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

However, it does not compile (obviously), as Supplier cannot throw an IOException. Is there any way I can add that to the method declaration of getFromConfig?

Or is the only way to do it like the following?

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

    public T getWithIO() throws IOException;
}

Update, I just realised that the Supplier interface is a really simple one, as in it has only the get() method. The original reason why I extended Supplier is to preverse the basic functionality, like the default methods for example.

Mathis answered 27/3, 2014 at 12:40 Comment(9)
Just forget about retaining checked exceptions... use a throws Exception signature in the functional interface and wrap it into an uncheckedCall lambda, which propagates the exception without checking (using sneakyThrow).Sistrunk
@MarkoTopolnik That sounds good enough to be an answer (preferably if you'd elaborate a bit on it). I just provided my two cents below but your idea sounds like it could be more accurateExpire
@SimonAndréForsberg JB Nizet already did a fair job here: https://mcmap.net/q/55725/-java-8-mandatory-checked-exceptions-handling-in-lambda-expressions-why-mandatory-not-optional I just wouldn't bother wrapping the exception, instead I use sneakyThrow.Sistrunk
@MarkoTopolnik Can you eloborate more on what uncheckedCall and sneakyThrow are in your example?Mathis
static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); }. sneakyThrow is a standard idiom and can be found for example here: #14039149Sistrunk
@MarkoTopolnik Also, I think this really is an issue with lambdas in Java 8, hence why people are having issues with it. Yet, I am much much much happier with lambdas with a minor issue, than with no lambdas at all.Mathis
I agree it's an issue, but I call out checked exceptions as the drag, not lambdas :)Sistrunk
This was thoroughly discussed in the lambda mailing list. Some alternative solutions are discussed there in case you want to take a look. The wrapper solution suggested by JB Nizet is also discussed there.Plenipotentiary
Possible duplicate of Java 8: Mandatory checked exceptions handling in lambda expressions. Why mandatory, not optional?Commando
S
30

Edit

As pointed many times, you don't need any custom class, use Callable and Runnable instead

Wrong, outdated solution

Consider this generic solution:

// 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));
Spindell answered 23/11, 2017 at 15:21 Comment(2)
You don't need to declare ThrowingSupplier. The built-in class Callable already does what you want.Sur
A minor "problem" with Callable is that it throws an Exception. This might have ripple effects on the referencing code. Solutions with generic exception might degrade to RuntimeException avoiding code-bloat in userspace.Malaya
E
21
  • If you would do that, you wouldn't be able to use it as a Supplier as it would only throw UnsupportedOperationException.

  • Considering the above, why not create a new interface and declare the getWithIO method in it?

    @FunctionalInterface
    public interface SupplierWithIO<T> {
        public T getWithIO() throws IOException;
    }
    
  • Perhaps some things are better of as old-style Java interfaces? Old-style Java isn't gone just because there's Java 8 now.

Expire answered 27/3, 2014 at 12:46 Comment(4)
I just updated my post with the reasoning behind extending. This method would break if new default methods were added to Supplier. On the other hand, updating the JVM to that version will not break my application either, so no harm done.Mathis
The problem with this approach is that this supplier cannot be used with the rest of the JDK Apis expecting an actual Supplier.Plenipotentiary
You don't need to declare SupplierWithIO. The built-in class Callable already does what you want.Sur
@Sur Technically, Callable declares to throw the more generic Exception If one would want to use it with the other JDK APIs, one might want to go that way.Expire
P
21

In the lambda mailing list this was throughly discussed. As you can see Brian Goetz suggested there that the alternative is to write your own combinator:

Or you could write your own trivial combinator:

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

You can write it once, in less that the time it took to write your original e-mail. And similarly once for each kind of SAM you use.

I'd rather we look at this as "glass 99% full" rather than the alternative. Not all problems require new language features as solutions. (Not to mention that new language features always causes new problems.)

In those days the Consumer interface was called Block.

I think this corresponds with JB Nizet's answer suggested by Marko above.

Later Brian explains why this was designed this way (the reason of problem)

Yes, you'd have to provide your own exceptional SAMs. But then lambda conversion would work fine with them.

The EG discussed additional language and library support for this problem, and in the end felt that this was a bad cost/benefit tradeoff.

Library-based solutions cause a 2x explosion in SAM types (exceptional vs not), which interact badly with existing combinatorial explosions for primitive specialization.

The available language-based solutions were losers from a complexity/value tradeoff. Though there are some alternative solutions we are going to continue to explore -- though clearly not for 8 and probably not for 9 either.

In the meantime, you have the tools to do what you want. I get that you prefer we provide that last mile for you (and, secondarily, your request is really a thinly-veiled request for "why don't you just give up on checked exceptions already"), but I think the current state lets you get your job done.

Plenipotentiary answered 27/3, 2014 at 13:46 Comment(0)
S
5

Since I have an additional point to make on this subject, I have decided to add my answer.

You have the choice to write a convenience method which either:

  1. wraps a checked-exception-throwing lambda into an unchecked one;
  2. simply calls the lambda, unchecking the exception.

With the first approach, you need one convenience method per functional method signature, whereas with the second approach, you need a total of two methods (except for primitive-returning methods):

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

This allows you to insert a call to one of these methods passing in a nullary lambda, but one which is closing over any arguments of the outside lambda, which you are passing to your original method. This way you get to leverage the automatic lambda conversion language feature without boilerplate.

For example, you can write

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

compared to

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

I have also found that overloading the wrapping method name for all lambda signatures often leads to ambiguous lambda expression errors. So you need longer distinct names, resulting in ever longer code char-for-char than with the above approach.

In the end, note that the said approach still doesn't preclude writing simple wrapper methods which reuse uncheckedRun/Call, but I found it simply uninteresting because the savings are negligible at best.

Sistrunk answered 28/3, 2014 at 7:58 Comment(0)
M
4

I added my own solution, not neccessarilly a direct answer to my question, which introduces the following after some revisions:

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

This were all one-time only declarations, now I add two variants of the calling code:

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

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

I am not too happy about converting an IOException to an UncheckedIOException, as:

  1. I would need to add an unchecked variant of every method that can throw an IOException.
  2. I prefer obvious handling of the IOException, instead of relying on hoping that you do not forget to catch the UncheckedIOException.
Mathis answered 27/3, 2014 at 13:43 Comment(8)
What do you consider as the typical bad consequence of failing to catch UncheckedIOException?Sistrunk
@MarkoTopolnik You can forget to catch them, ultimately leading to a program failure, in a situation where you ought to recover from the situation and continue normal program flow if you had be aware that a IOException could be thrown.Mathis
Is it your actual experience that your applications fail at production because you had in mind a way to get around a missing config file, but still forgot to put it in your code? My experience is that these are the most trivial of mistakes, easy to spot, easy to fix, and hard to make in the first place because they don't concern some ill-defined, convoluted, unpredictable sequence of events. They never make it to production, unlike for example NullPointerExceptions, which often occur in the above-described way.Sistrunk
Thankfully, I see quite explicit pressure in the core team to remove exception checking altogether from a future version of Java. So even the core team can't find in their hearts a truly justified reason to go on against the tides.Sistrunk
@MarkoTopolnik With a config file in this scenario, no. With other situations that could lead to IOExceptons, I would be a bit scared of them. Out of curiosity, where do you find the news of the future of Java? I'd be interested to at least read, or possibly join in, the discussions. Sadly there are tons of mailing lists and it is hard to pick a correct one.Mathis
Just in the discussion linked to by Edwin here. BTW have you really never experienced hard-to-localize production errors, which ultimately originate with inappropriate checked exception handling? catch (IOException e) { umm... what now? Let's leave it to Eclipse: e.printStackTrace(); } Advanced version: catch (IOException e) { log.error(e); } Advanced-plus version: catch (IOException e) { log.error("IO failure", e)---now I at least get a stacktrace in the log.Sistrunk
Ultra-brainy version: catch (IOException e) { log.error("failure",e); throw new RuntimeException(e);}---now it's "just" contaminating the log with duplicated exception reports. On the other hand, the worst that can happen when an IOException is thrown and not handled, is a clean abort of the current unit of work. That's exactly what I wanted, in 99% of cases. In the remaining 1%, I know very well what I want to do with it and don't gratuitously forget about it.Sistrunk
You don't need to declare CheckedSupplier. The built-in class Callable already does what you want.Sur
M
0

We can use this generic solution also. Any type of exception we handle by this approach.

    @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;
        }
    }

Mylesmylitta answered 14/2, 2020 at 6:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.