Why Try? Better Exception Handling in Java With Try
Concurrency and streams put up special challenges for exception handling, particularly checked exceptions. Fortunately, you can try to improve your lot with Try.
Join the DZone community and get the full member experience.
Join For FreeException handling in Java is difficult with streams, IO, and concurrency. A lot of people are saying that it even might be a failed experiment. Broken. I am not saying it is but that is what I've heard, just ask around. Checked exceptions are not what they used to be. They won't write about it but I will. Make the Try great again. &:0
Do or Do Not, There Is No Try
Yoda says there is no Try in Java 8. You have to write it yourself. Sad. Oracle doesn't provide a Try and throws exceptions at innocent methods — NO JVM INSTALLS? &:0
It is not that difficult, though. A Try is an object that wraps an exception so it is not immediately thrown. A Try is either a success or a failure. A developer can inspect the Try and ask if it is a success or not. According to the Scala documentation, a Try can be chained, catching exceptions along the way. A good explanation is found at: What's wrong with Java 8, part IV.
The Try Construct
There are several implementations of Try, including the basic implementation we go over here. A more elaborate implementation is found at: https://github.com/jasongoodwin/better-java-monads. The basic implementation tries a function call, wraps the exception, and returns a success or failure. The Try can be chained with the map and flatMap operations:
public abstract class Try < V > {
private Try() {}
public abstract Boolean isSuccess();
public abstract Boolean isFailure();
public abstract void throwException();
public abstract Throwable getMessage();
public abstract Vget();
public abstract < U > Try < U > map(CheckedFunction << ? super V, ? extends U > f);
public abstract < U > Try < U > flatMap(CheckedFunction << ? super V, Try < U >> f);
public static < V > Try < V > failure(Throwable t) {
Objects.requireNonNull(t);
return new Failure < > (t);
}
public static < V > Try < V > success(V value) {
Objects.requireNonNull(value);
return new Success < > (value);
}
public static < T > Try < T > failable(CheckedSupplier < T > f) {
Objects.requireNonNull(f);
try {
return Try.success(f.get());
} catch (Throwable t) {
return Try.failure(t);
}
}
private static class Failure < V > extends Try < V > {
private RuntimeException exception;
public Failure(Throwable t) {
super();
this.exception = new RuntimeException(t);
}
@Override
public Boolean isSuccess() {
return false;
}
@Override
public void throwException() {
throw this.exception;
}
@Override
public Vget() {
throw exception;
}
@Override
public Boolean isFailure() {
return true;
}
@Override
public < U > Try < U > map(CheckedFunction << ? super V, ? extends U > f) {
Objects.requireNonNull(f);
return Try.failure(exception);
}
@Override
public < U > Try < U > flatMap(CheckedFunction << ? super V, Try < U >> f) {
Objects.requireNonNull(f);
return Try.failure(exception);
}
@Override
public Throwable getMessage() {
return exception;
}
}
private static class Success < V > extends Try < V > {
private final V value;
public Success(V value) {
super();
this.value = value;
}
@Override
public Boolean isSuccess() {
return true;
}
@Override
public void throwException() {
return;
}
@Override
public Vget() {
return value;
}
@Override
public Boolean isFailure() {
return false;
}
@Override
public < U > Try < U > map(CheckedFunction << ? super V, ? extends U > f) {
Objects.requireNonNull(f);
try {
return Try.success(f.apply(value));
} catch (Throwable t) {
return Try.failure(t);
}
}
@Override
public < U > Try < U > flatMap(CheckedFunction << ? super V, Try < U >> f) {
Objects.requireNonNull(f);
try {
return f.apply(value);
} catch (Throwable t) {
return Try.failure(t);
}
}
@Override
public Throwable getMessage() {
throw new IllegalStateException("no messages when success");
}
}
}
A Try is an abstract class with a static initializer method, failable(), which returns either a Success or a Failure. A Success contains a value and a Failure contains a RuntimeException with the original exception as the cause. The exception is unchecked so that if it is thrown, it will halt the program and doesn't need to be caught again.
A Scala Example in Java
In Scala, an example is provided on how to use the Try, available at: http://www.scala-lang.org/api/2.9.3/scala/util/Try.html. This example takes two integers from the console and tries to divide them. If one of the strings can't be parsed into an integer, or if the divisor is zero, an exception is thrown. The Try returns a failure and that event can be handled gracefully:
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
Try<Integer> dividend = Try.failable( () -> {
System.out.println("Enter an Int that you'd like to divide:");
return Integer.parseInt(bufferedReader.readLine());
} );
Try<Integer> divisor = Try.failable( () -> {
System.out.println("Enter an Int that you'd like to divide by:");
return Integer.parseInt(bufferedReader.readLine());
});
Try<Integer> problem = dividend.<Integer>flatMap(x -> divisor.<Integer>map(y -> x/y));
if(problem.isSuccess()) {
System.out.println("Result of " + dividend.get() + "/"+ divisor.get() +" is: " + problem.get());
} else if(problem.isFailure()) {
System.out.println("You must've divided by zero or entered something that's not an Int. Try again!");
System.out.println("Info from the exception: " + problem.getMessage());
}
In this example, all functionality of the Try is clearly visible. Exceptions are chained and saved for later so that they can be handled gracefully in code.
Try Eith Streams
Streams can't handle checked exceptions. Throwing exceptions from within the stream will break the type system. Wrapping exceptions is perfectly all right. See Brian Goetz's answer on Stackoverflow: How can I throw checked exceptions from inside Java 8 streams. The Try wraps the checked exception, but doesn't throw it. Instead, it provides the option to inspect the result and decide to either handle the failure or throw an unchecked exception. This is also legal in streams. For example:
List<Integer> results = Stream.iterate(0, i -> i + 1)
.<Try<Integer>>map(i -> {
return Try.failable( () -> checkedExceptionThrowingMethod(i) );
})
.filter(t -> t.isSuccess())
.map(t -> t.get())
.collect(Collectors.toList());
In the above example, a method which may throw a checked exception, is called in the stream. The compiler will flag this, but when wrapped in a Try, the code is executed and the eventual exceptions are stored in the Try. With the filter method, all failures are filtered out and we get a list of results. It is easy to see how success and failure can be handled. The results can be passed to a different location for further inspection.
Try With Future
Concurrency is another area where exception handling can be difficult. With the Java Executor, you get a Future, which may contain an exception. When calling the get method, the exception is immediately thrown. You don't know beforehand whether the result is available or not. Maybe you don't want to throw and catch the exception. Maybe you're only interested in those Futures that did finish successfully. Either way, the following example shows how the result of a Future is stored in a Try and made available for inspection:
ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List < Integer > ints = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
List < Try < Integer >> results = new ArrayList < > ();
List < Future < Integer >> futures = new ArrayList < > ();
for (Integer i: ints) {
int n = i % 3;
Callable < Integer > task = () - > {
return 1 / n;
};
Future < Integer > f = service.submit(task);
futures.add(f);
}
for (Future < Integer > f: futures) {
results.add(Try.failable(() - > f.get()));
}
int failures = 0;
for (Try < Integer > t: results) {
if (t.isSuccess()) {
System.out.println("result: " + t.get());
} else {
failures++;
}
}
System.out.println(failures + " failures");
No try-catch blocks needed and the code still handles exceptions gracefully.
Why Not Try Try
?
In the previous examples, we saw that the Try is useful in several scenarios, and there are more. It is a good replacement for Optional, for example. If Try is so useful, why is it not part of the Java JDK? The Future and the Optional would be easier if they had Try semantics. In streams, a Try is useful when dealing with checked exceptions to wrap them in unchecked exceptions and still be able to handle the exceptions gracefully. I suggest Oracle take the Try seriously, but not literally. &:0
Opinions expressed by DZone contributors are their own.
Comments