Scala's Try-Success-Failure for Java 8
This API is a Java 8 implementation of the Scala Try API, originally implemented by Twitter Engineers and later added to the Scala Standard Library.
The Try
type represents a computation that may fail. If the computation is successful it returns the value wrapped in a Try.Success
otherwise the java.lang.Exception
wrapped in a Try.Failure
.
In order to use Try
you need to call the Try.apply(FailableSupplier)
method providing a lambda with the same signature used for a common java.util.function.Supplier
. Indeed FailableSupplier
is just a java.util.function.Supplier
with a throws Exception
added to its get
method.
Note that I'm not saying here that the try-catch
approach must be abandoned in favour of Try-Success-Failure
. Indeed there are cases where you would use the traditional try-catch
pattern but, in general, I think this API provides a more fluent interface to deal with exceptions.
Using Try as a Maven dependency
Binary release artifacts are published to the Sonatype OSS Repository Hosting service. Add the following dependency into your pom.xml to use Try
:
<dependency>
<groupId>com.lambdista</groupId>
<artifactId>try</artifactId>
<version>$VERSION</version>
</dependency>
where $VERSION
is the version you want to use (0.1.0, 0.2.0, and so on). You can grab the version you're interested in from the available releases
Build
This project is managed with Maven so it can be built using:
$ git clone https://github.com/lambdista/try.git
$ cd try
$ mvn package
You'll find the jar
under the usual target
directory.
Running the examples in source code
Using Maven and the exec-maven-plugin you can run the main classes representing the examples for this project. For instance, to run the ReadFileLines
main class you can use:
$ mvn exec:java -Dexec.mainClass="com.lambdista.example.ReadFileLines"
The changing part is the full path to the main class you intend to run.
Examples
In order to get you acquainted with this API each example will be provided using both the same old try-catch
pattern and the new Try
API.
Example 1: Read a file line by line
As a first example consider the code you need to implement a method to read a file line by line in Java 8.
Using the traditional try-catch block
public static List<String> readFile(String file) {
List<String> lines;
try {
lines = Files.readAllLines(new File(file).toPath());
} catch (IOException e) {
lines = Arrays.asList("Could not read the file: " + file);
}
return lines;
}
readFile
reads the content of a file, line by line, into a List<String>
. In case of exception the method returns a List<String>
with just one line: "Could not read...".
Using the Try API
public static List<String> readFile(String file) {
return Try.apply(() -> Files.readAllLines(new File(file).toPath()))
.getOrElse(Arrays.asList("Could not read the file: " + file));
}
In this case a lambda is passed to Try.apply
. The Try
's getOrElse
method returns the value obtained by the call to Files.readAllLines(new File(file).toPath())
if no exception is thrown or whatever you passed to it in case of exception. Of course everything is type safe, in the sense that getOrElse
will accept only arguments whose type is the same of Try
's. Thanks to the type inferer there's no need to specify the type for Try
in the previous code. In fact, it is equivalent to Try.<List<String>>apply(() -> Files.readAllLines(new File(file).toPath()))
where you explicitly say that the Try
's type is List<String>
.
Which version do you like more? The try-catch
approach or the Try
one? It may be a matter of taste or just because I'm used to it but I prefer the latter--also because otherwise I wouldn't have written this API! :-)
Example 2: Read the content of a URL into a String
Using the traditional try-catch-finally block
public static String urlToString(String url, String errorMessage) {
Scanner scanner = null;
try {
scanner = new Scanner(new URL(url).openStream(), "UTF-8");
String result = scanner.useDelimiter("\\A").next();
scanner.close();
return result;
} catch (IOException e) {
return errorMessage;
} finally {
if (scanner != null) {
scanner.close();
}
}
}
urlToString
reads the content of a URL into a String
. The method takes two parameters: url
which is the String
representing the URL and errorMessage
which is the String
to return if the URL content retrieving fails. Notice the boilerplate code. You need to initialize the scanner
reference to null
. You also have to use a finally block and close the Scanner
object after checking if it is not null
. Wouldn't it be great if you could avoid such a boilerplate code and let an API do it for you? Well, take a look at the, semantically, same code in the following example.
Using the Try API
public static String urlToString(String url, String errorMessage) {
Try<Scanner> scanner = Try.apply(() -> new Scanner(new URL(url).openStream(), "UTF-8"));
String result = scanner.map(s -> s.useDelimiter("\\A").next()).getOrElse(errorMessage);
scanner.forEach(s -> s.close());
return result;
}
Look ma, no null
initialization, no try-catch-finally
block and no null
check before closing scanner
! The first line of the method creates a Try<Scanner>
object which can be, as usual, a Success<Scanner>
or a Failure<Scanner>
depending on the result of the lambda. The map
method is then used to transform it into a Try<String>
, taking care of the fact that if the result of Try.apply
is a Failure<Scanner>
now it just becomes a Failure<String>
otherwise it gets mapped into a Success<String>
. getOrElse
then extracts its content (a String
) if it's a Success
or returns errorMessage
if it's a Failure
. Afterward the forEach
method takes care of closing the Scanner
object if it is of type Success<Scanner>
otherwise it does nothing. Finally the result is returned.
Typically you use map
to transform something into something else, while you employ forEach
to consume something, that is to use it someway. As a matter of fact forEach
has a void
return type.
The Try
version is declarative whilst the try-catch-finally
one is imperative. Expressing the Try
version in words you have: "Try to create a Scanner
object for the given URL. Afterward map this object into a String
or else use this other String
if it's a failure. In the end close the Scanner
object."
Example 3: Integer division
This is an interesting one because it shows another peculiarity of the Try
API. You may already know that Java has both checked and unchecked exceptions. For checked exceptions the compiler won't accept your code if you forget to handle them. However unchecked exceptions such as NullPointerException
, IllegalArgumentException
, RuntimeException
and so on are not notified by the compiler if you don't handle them. Consider the following code snippet:
System.out.println("Enter the dividend press Return and then enter the divisor: ");
Scanner dividend = new Scanner(System.in);
Scanner divisor = new Scanner(System.in);
int num = dividend.nextInt();
int denum = divisor.nextInt();
String res = "The quotient is: " + (num / denum);
System.out.println(res);
The previous code asks the user to enter two integers and then performs their division. The problem is that it could throw two types of unchecked exceptions and the compiler of course wouldn't tell you. You are required to know it yourself. The two unchecked exceptions I'm talking about are java.util.InputMismatchException
and java.lang.ArithmeticException
if the user enter a non-integer or zero as the divisor, respectively. Now, if you have a decent mathematical background you know you can't divide by zero. Furthermore you can also imagine that Scanner
's nextInt
method may throw some type of exception if you enter a non-integer. However, in both cases you have to look up the type of exception. Yes, you can use a generic catch(Exception e)
and capture them all if you're not interested in the specific type or you could avoid using try-catch
in the first place thanks to Try
. Here are both implementations.
Using the traditional try-catch block
public static void divideWithoutTry() {
System.out.println("Enter the dividend press Return and then enter the divisor: ");
Scanner dividend = new Scanner(System.in);
Scanner divisor = new Scanner(System.in);
String res;
try {
res = "The quotient is: " + (dividend.nextInt() / divisor.nextInt());
} catch(InputMismatchException|ArithmeticException e) {
res = "The integers you entered are not valid or the divisor is zero.";
}
System.out.println(res);
}
Using the Try API
public static void divideWithTry() {
System.out.println("Enter the dividend press Return and then enter the divisor: ");
Scanner dividend = new Scanner(System.in);
Scanner divisor = new Scanner(System.in);
String res = Try.apply(() -> dividend.nextInt() / divisor.nextInt())
.map(quotient -> "The quotient is: " + quotient)
.getOrElse("The integers you entered are not valid or the divisor is zero.");
System.out.println(res);
}
In the previous code map
maps the Integer
result into a String
. After the map
call you have something that was a Try<Integer>
transformed into a Try<String>
. This is another important aspect of Try
. Its type can be mapped into another type without the need to do explicit exception-handling in all of the places that an exception might occur. I mean if dividend.nextInt() / divisor.nextInt()
caused an exception the result of Try.apply
would be a Try.Failure
instance. Nevertheless it's type would be Try<String>
. This lets you call getOrElse
passing a String
to it.
Integer sum
An important property of Try
is its ability to pipeline (chain if you prefer) operations, catching exceptions along the way thanks to its flatMap
method. If you are not a functional programmer concepts such as flatMap/map
might not be easy to grasp at first. However you'll get used to them when you become one and, in the end, you'll love them. Moreover you're going to encounter these methods more and more often since some important Java 8 classes already implement them (e.g. java.util.Optional
and java.util.stream.Stream
). Anyway for the moment just take for granted that to pipeline more than two operations, say N, you just need to chain them by using N - 1 flatMap
calls and a last call to map
. For example, suppose you have 3 variables--x, y and z--being of type Try<Integer>
and you just want to sum them up. Here is the code you need:
x.flatMap(a -> y.flatMap(b -> z.map(c -> a + b + c)))
Apart from the methods seen in these examples, such as map
, flatMap
and getOrElse
, Try
has many other useful methods. See the TryTest
class for a thorough coverage of all its methods.
Known differences with the original library
Exception handling
In Scala exceptions are all unchecked so when you use the get
method you're not forced to deal with the exception it throws in case this Try
object is a Failure
. In order to reflect this behaviour, calling get
on Failure
objects will throw a GetOfFailureException
which wraps the original Exception
. Since GetOfFailureException
is unchecked you're not forced to handle it. However, if for some reason you need to deal with checked exceptions this API provides a further method, checkedGet
, which may throw an Exception
that must be handled.
As a final note, the original library traps Throwable
types and rethrows only NonFatal
ones which, in its opinion, are: VirtualMachineError
, ThreadDeath
, InterruptedException
, LinkageError
and ControlThrowable
. The latter belongs to Scala so Java has no counterpart. Previous versions of this library didn't catch Throwable
types but only Exception
ones. From the 0.3.0 version on, thanks to Gregor Trefs suggestion, I changed the implementation so that now it catches all Throwable
types. However it rethrows all Error
types. From the Java API: "An Error
is a subclass of Throwable
that indicates serious problems that a reasonable application should not try to catch".
Javadoc
API documentation for this project.
Bugs and Feedback
For bugs, questions and discussions please use the Github Issues.
License
Copyright 2014 Alessandro Lacava.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.