functionExtentions is a Java library with Throwable Functional Interfaces, Tuples and Repositories implemented to expedite Functional Programming with JAVA 8. It is released on Maven, with following goals accomplished:
- Declares a rich set of functional interfaces throwing Exceptions, that can be converted to conventional ones with Checked Exceptions handled with shared
exceptionHandler
s, thus allow developers to define the concerned business logic only within the lambda expressions. - Implements an immutable data structure to keep and retrieve up to first 20 strong-typed values as a set of
Tuple
classes. - Provides Repositories as a kind of Map-based intelligent utility with pre-defined business logic to evaluate a given key or keys (upto 7 strong-typed values as a single Tuple key) to get corresponding value or values (upto 7 strong-typed values as a single Tuple value), buffer and return them if no Exception happened.
- Multiple powerful generic utilities to support above 3 types utilities, mainly build with Repositories. For example: `
Object getNewArray(Class clazz, int length)
`, `String deepToString(Object obj)
`, `Object copyOfRange(Object array, int from, int to)
`, `T convert(Object obj, Class<T> toClass)
`, `boolean valueEquals(Object obj1, Object obj2)
`, etc. that support various combinations of primitive and objective types and arrays.
This serial of posts include:
- Throwable Functional Interfaces
- Tuples
- Repositories
Introduction
The introduction of Functional Interface and Lambda Expression open the gate to treat business logic as first class members in JAVA 8.
However, the contract of checked Exception handling of almost all Java Functional Interface methods declaration (any codes throwing any Java Exceptions need to be wrapped by try{}catch{}
) makes it extremely hard to define the concerned business logic as Lambda Expressions without Exception Handling of boilerplate codes.
This article introduces some techniques, used in my open source project io.github.cruisoring.functionExtensions, to enable JAVA developers focusing on the business logic and handling the exceptions in a more efficient and managable manner.
Background
In Java exceptions under Error and RuntimeException classes are unchecked exceptions, everything else under throwable is checked. If a method has any checked Exception, then it must either handle the exception explicitly or specify the exception using throws keyword.
There is no exemption with Functional Interface in Java 8: if the single abstract
method defined doesn't throw some Exception, as those defined in java.util.function
package, then the Lambda Expressions must handle any potential exceptions properly before being assigned to any Functional Interface instance.
For example, since Function<T,R>
is defined with apply
as below:
R apply(T t);
Suppose I want to keep the business logic of converting a String
to a Class
to getClass
variable of Function<String, Class>
type, a succinct Lambda Expression as below would always get error of "Unhandled exception: ClassNotFoundException
":
Function<String, Class> getClass = className -> <s>Class.forName(className)</s>;
The signature of Class.forName
doesn't match the Function<T,R>.apply(T t)
that demands you handle any Exceptions accompanied with the concerned business logic.
static Class<?> forName(String className) throws ClassNotFoundException
Make Functions Throwable
To cope with this inconvenience, changing the corresponding Functional Interface method with "throws Exception
" would delay the Exception Handling to some later stage, and make them much more friendly with Lambda Expressions.
@FunctionalInterface
interface FunctionThrowable<T, R> extends AbstractThrowable {
R apply(T t) throws Exception;
To keep the expected operations reuseable, I would advise you to append "throws Exception
" to the single abstract
method signature and immediately, you can see Functional Interfaces to be much more friendly. Now assigning Class.forName
to a FunctionalThrowable
variable works.
FunctionThrowable<String, Class> getClass = Class::forName;
Such simple changes with Functional Interfaces makes Java method much more maintainable and flexible by treating business logic as variables.
I encountered this issue when trying to develop some generic OJDBC based Prepared/CallableStatement execution functions with signature like:
int call(Supplier<Connection> connectionSupplier, String procSignature, Object... args)
Given different types of args, the callableStatement
generated from procSignature
that has many "?
" as placeholders would call different setXXX or registerXXX methods to call the statement and retrieve returned values properly.
The piling of dozens of if
-else
s presents not only an awkward, but also a rigid code structure that is hard to extend. My first attempt was to define a Functional Interface:
TriConsumer<PreparedStatement, Integer, Object> { void accept(T t, U u, S s) throws Exception;}
Then a map can be defined as below:
public static final Map<Class<?>, TriConsumer<PreparedStatement,
Integer, Object>> setters = new HashMap<>()
setters.put(LocalDateTime.class, (statement, position, argument)
-> statement.setTimestamp(position, Timestamp.valueOf((LocalDateTime) argument)));
setters.put(LocalDate.class, (statement, position, argument)
-> statement.setDate(position, Date.valueOf((LocalDate) argument)));
setters.put(boolean.class, (statement, position, argument)
-> statement.setBoolean(position, (boolean) argument));
setters.put...;
Enables the following method to handle all kinds of arguments:
private static void setArgument(PreparedStatement ps, int position, Object argument) throws Exception {
Class clazz = argument.getClass();
if(argument == null || !setters.containsKey(clazz)){
defaultSetter.accept(ps, position, argument);
} else {
setters.get(clazz).accept(ps, position, argument);
}
}
Instead of using dozens of if else()...
, the main processing flow is quite straight: choose the right business logic for a specific type of argument from a Map
, or a default one if it is not defined, then applying it with the relative argument. Although I have delayed the Exception Handling by "throws Exception
" with the above method, it is fully possible to use a single try{}catch{}
to handle any Exceptions thrown by setTimestamp()
/setDate()
/setBoolean()
.
More importantly, without a single line of code changes, it is possible to override the default Lambda for a specific class, or add new methods to handle any other customer
classes would change the overall setArgument()
behaviours gracefully.
Conversions of Functional Interfaces
Once these throwable functional interfaces are declared, sooner or later, they need to be evaluated and then Exception Handling codes must be provided.
I have tried to convert them as either RunnableThrowable
(if there is no value returned) or SupplierThrowable<R>
(if a value of type R
is returned) with asRunnable(...)
or asSupplier(...)
as example of converting TriFunctionThrowable<T,U,V,R>
to SupplierThrowable<R>
as below:
@FunctionalInterface
interface TriFunctionThrowable<T,U,V,R> extends AbstractThrowable {
R apply(T t, U u, V v) throws Exception;
default SupplierThrowable<R> asSupplier(T t, U u, V v){
return () -> apply(t, u, v);
}
}
Though it works with generic static
methods of Functions.java
as I would cover later, the functional interfaces without returning values (like ConsumerThrowable<T>
, BiConsumerThrowable<T,U>
, etc.) can be bundled with a Consumer<Exception>
thus converting them gracefully to the Non-Throwable versions. For example:
default BiConsumer<T,U> withHandler(Consumer<Exception> exceptionHandler){
BiConsumer<T,U> biConsumer = (t, u) -> {
try {
accept(t, u);
} catch (Exception e) {
if(exceptionHandler != null)
exceptionHandler.accept(e);
}
};
return biConsumer;
}
For those functional interfaces with value returned (like SupplierThrowable<R>, FunctionThrowable<T,R>, BiFunctionThrowable<T,U,R>, etc., all implementing the empty marker WithValueReturned<R> interface), two methods are defined to convert them to the Non-Throwable counterparts.
Taken BiFunctionThrowable<T, U, R> for instance, following two default methods
are defined to convert it to FunctionThrowable<T, U, R> (which is defined in java.util.function):
default BiFunction<T, U, R> withHandler(BiFunction<Exception,
WithValueReturned, Object> exceptionHandler) {
BiFunction<T, U, R> function = (t, u) -> {
try {
return apply(t, u);
} catch (Exception e) {
return exceptionHandler == null ? null : (R)exceptionHandler.apply(e, this);
}
};
return function;
}
default BiFunction<T,U, R> orElse(R defaultValue){
BiFunction<T,U, R> function = (t, u) -> {
try {
return apply(t, u);
} catch (Exception e) {
return defaultValue;
}
};
return function;
}
The BiFunction<Exception, WithValueReturned, Object> exceptionHandler
outlines almost everything needed to handle an Exception thrown by such a throwable functional interface:
- Exception thrown
- The lambda expression throwing this Exception
Consequently, before evaluating any of these throwable functional interfaces, a shared BiFunction<Exception, WithValueReturned, R> exceptionHandler
can be used to convert it to something similar to a normal Java method with Exception handled by that exceptionHandler
instance. For example, it is possible to define a complex static exceptionHandler
instance shared by EVERY throwable functional interface:
static Class<? extends Exception>[] negligibles = new Class[]{
NullPointerException.class, IllegalArgumentException.class
};
static Class<? extends Exception>[] noticeables = new Class[]{
SQLException.class, NumberFormatException.class
};
static BiFunction<Exception, WithValueReturned, Object> defaultReturner = (ex, lambda) -> {
final Class<? extends Exception> exceptionType = ex.getClass();
if(IntStream.range(0, noticeables.length).anyMatch
(i -> noticeables[i].isAssignableFrom(exceptionType))){
String msg = ex.getMessage();
msg = ex.getClass().getSimpleName() + (msg == null?"":":"+msg);
logs.add(msg);
}else if(IntStream.range(0, negligibles.length).allMatch
(i -> !negligibles[i].isAssignableFrom(exceptionType))){
throw new RuntimeException(ex);
}
final Class returnType = TypeHelper.getReturnType(lambda);
if(returnType == Integer.class || returnType == int.class)
return -1;
return TypeHelper.getDefaultValue(returnType);
};
To validate its behaviour:
SupplierThrowable.BiFunctionThrowable<String[],
Integer, Integer> ff = (sArray, index) -> sArray[index].length();
Integer result = (Integer)customFunctions.apply(ff, new String[]{"a", "ab"}, 1);
assertEquals(Integer.valueOf(2), result);
result = (Integer)customFunctions.apply(ff, new String[]{null, null}, 1);
assertEquals(Integer.valueOf(-1), result);
The NullPointerException
or IllegalArgumentException
s would be neglected, while NumberFormatException
would be logged and both cases would return -1
when Exception
is caught. Other unexpected Exception
s like ArrayIndexOutOfBoundsException
would be re-thrown as RuntimeException
and terminate the application.
Then for a simple function accepting 2 arguments and returning an Integer, the concerned business logic would be evaluated with invalid arguments directly after being converted to non-throwable BiFunction<String, Boolean, Integer>
:
BiFunctionThrowable<String, Boolean, Integer> f9 = (s, b) -> Integer.valueOf(s) + (b ? 1 : 0);
assertEquals(Integer.valueOf(-1), f9.withHandler(defaultReturner).apply("8.0", true));
The Functions
class defined two static
instances:
ThrowsRuntimeException
: would throw RuntimeException
with any Exception
thrown by the functional interfaces ReturnsDefaultValue
: would swallow the caught exception silently and return the default value expected from the lambda expression. (0
for WithValueReturned<Integer>
, false
for WithValueReturned<Boolean>
, 0f
for WithValueReturned<Float>
...)
In this way, the functional interfaces can define the business logic only, leave the handling of exceptional cases to a single method.
Change Exception Handlings Globally
Letting static
Functions instances to handle Exception
s also enable the processing flows changed gracefully. Suppose there are many places that execute business logic with the Functions.Default
as below, then depending on if the app is switched to RELEASE
mode by setting a system property of "isRelease
", the following sample method would return default value '0
' in RELEASE
mode, but throws RuntimeException
in DEBUG
.
@Test
public void switchingFunctions(){
String inReleaseMode = (String)Functions.ReturnsDefaultValue.apply
(s -> System.getProperty((String) s), "isRelease");
Functions.Default = (inReleaseMode != null) ?
Functions.ReturnsDefaultValue : Functions.ThrowsRuntimeException;
SupplierThrowable.BiFunctionThrowable<String[],
Integer, Integer> ff = (sArray, index) -> sArray[index].length();
Integer result = (Integer) Functions.Default.apply
(ff, new String[]{"a", "ab"}, 1);
assertEquals(Integer.valueOf(2), result);
result = (Integer)Functions.Default.apply(ff, new String[]{null, null}, 1);
assertEquals(Integer.valueOf(0), result);
}
Lambda with Data
I have come across quite a crooked framework: Caller
classes need to invoke business logic of Callee
classes that need multiple input parameters. However, the Caller
Class instance can only call TestResult execute(Class<?> testClass, String methodName) of Arquillian JUnitTestRunner that accepts only Class of the Callee
and name of the called method, no place for extra arguments at all.
First, I managed to keep the input parameters as static
variables of Caller
classes or Callee
classes to allow the Callee
instances created by JunitTestRunner
to access them to trigger the business logic accordingly. However, if there are multiple Caller
classes are created to interact with multiple Callee
classes, then invoking business logic that refers to multiple static
variables scattered in Caller
/Callee
classes is a nightmare for code reuse.
Thanks to the data captuable nature of Lambda created by higher-order functions, this issue could be solved with following simplified pseudo codes. First, a Functional Interface is created as below:
@FunctionalInterface
public interface InvokableStep {
void execute(Callee callee) throws Exception;
}
In the Callee
classes, codes like below are created:
private static Stack<InvokableStep> stepStack = new Stack<>();
@Test
public void dontCallMeDirectly() throws Exception {
InvokableStep step = stepStack.pop();
Objects.requireNonNull(step);
step.execute(this);
}
public static void doSomething(JUnitTestRunner junitTestRunner, String param1, String param2)
throws Throwable {
Objects.requireNonNull(junitTestRunner);
InvokableStep step = calleeObj -> {
Callee callee = (Callee)calleeObj;
callee.privateMethod(param1);
callee.privateField = param2;...
};
stepStack.push(step);
junitTestRunner.execute(Callee.class, "dontCallMeDirectly");
}
In the Caller
class, now it is possible to invoke something by calling doSomething
directly:
- The input parameters (
param1
, param2
) are provided directly, in addition to the junitTestRunner
directly or indirectly - The
InvokableStep
instance step is created, accepting one instance of Callee
only, consume the input parameters (param1
, param2
) directly with the given Callee
instance that needs to be casted to callee
. - The newly created instance
step
is pushed to the static
stack. - Then
junitTestRunner
trigger the only public
instance method dontCallMeDirectly()
would get the step instance created in 2), and applied with the Callee
instance itself immediately. - Since there is no contention in my case, and everything except
Callee
instance has been captured by the Lambda instance step, the business logic would be triggered with everything needed.
Well, the process is still quite twisted: the Caller
class instance calls the static
method of Callee
class, which makes the junitTestRunner
to create a new Callee
instance, and make it execute the expected processes with its own private
/public
instance resources.
However, you can see it is quite clear that a Lambda Expression could be created with fresh data, thus eliminate many unnecessary variables used to convey the information to execute the business logic.
How to use
The project is hosted at github.com, to include the library, add following info to your maven project:
< dependency >
< groupId >io.github.cruisoring</ groupId >
< artifactId >functionExtensions</ artifactId >
< version >1.0.1</ version >
</ dependency >
|
Following table summarize the 18 throwable Funtional Interfaces:
Throwable Interfaces | # of Inputs | With Value Returned | Non-Throwable function | Convert with Handler | Convert with defaultValue |
RunnableThrowable | 0 | No | Runnable | Yes | No |
SupplierThrowable<R> | 0 | Yes | Supplier<R> | Yes | Yes |
ConsumerThrowable<T> | 1 | No | Consumer<T> | Yes | No |
FunctionThrowable<T,R> | 1 | Yes | Function<T,R> | Yes | Yes |
PredicateThrowable<T> | 1 | Yes | Function<T,Boolean> | Yes | Yes |
BiConsumerThrowable<T,U> | 2 | No | BiConsumer<T,U> | Yes | No |
BiFunctionThrowable<T,U,R> | 2 | Yes | BiFunction<T,U,R> | Yes | Yes |
BiPredicateThrowable<T,U> | 2 | Yes | BiFunction<T,U,Boolean> | Yes | Yes |
TriConsumerThrowable<T,U,V> | 3 | No | TriConsumer<T,U,V> | Yes | No |
TriFunctionThrowable<T,U,V,R> | 3 | Yes | TriFunction<T,U,V,R> | Yes | Yes |
QuadConsumerThrowable<T,U,V,W> | 4 | No | QuadConsumer<T,U,V,W> | Yes | No |
QuadFunctionThrowable<T,U,V,W,R> | 4 | Yes | QuadFunction<T,U,V,W,R> | Yes | Yes |
PentaConsumerThrowable<T,U,V,W,X> | 5 | No | PentaConsumer<T,U,V,W,X> | Yes | No |
PentaFunctionThrowable<T,U,V,W,X,R> | 5 | Yes | PentaFunction<T,U,V,W,X,R> | Yes | Yes |
HexaConsumerThrowable<T,U,V,W,X,Y> | 6 | No | HexaConsumer<T,U,V,W,X,Y> | Yes | No |
HexaFunctionThrowable<T,U,V,W,X,Y,R> | 6 | Yes | HexaFunction<T,U,V,W,X,Y,R> | Yes | Yes |
HeptaConsumerThrowable<T,U,V,W,X,Y,Z> | 7 | No | HeptaConsumer<T,U,V,W,X,Y,Z> | Yes | No |
HeptaFunctionThrowable<T,U,V,W,X,Y,Z,R> | 7 | Yes | HeptaFunction<T,U,V,W,X,Y,Z,R> | Yes | Yes |
Declaring instances is easier than the conventional ones:
BiFunctionThrowable<Integer, String, Integer> biFunctionThrowable = (i, s) -> i + Integer.valueOf(s);
In above example, though Integer.valueOf(s)
could NumberFormatException
, declaration of doesn't need to handle it.
Then the above biFunctionThrowable
can be converted to BiFunction<Integer, String, Integer> with either BiFunction<Exception, WithValueReturned, Object> exceptionHandler
or a default value as below:
BiFunction<Integer, String, Integer> biFunction = biFunctionThrowable.withHandler((ex, fun) -> -3);
assertEquals(Integer.valueOf(10), biFunctionThrowable.apply(3, "7"));
assertEquals(Integer.valueOf(-3), biFunction.apply(3, "seven"));
biFunction = biFunctionThrowable.orElse(-1);
assertEquals(Integer.valueOf(-1), biFunction.apply(3, "seven"));
Thanks to the Type Erasure, it is possible to use the same instance of BiFunction<Exception, WithValueReturned, Object> exceptionHandler
to execute any throwable Functional Interfaces as ReturnDefaultValue
of Functions:
private static BiFunction<Exception, WithValueReturned, Object> returnDefaultValue =
(Exception ex, WithValueReturned throwable) -> TypeHelper.getDefaultValue(TypeHelper.getReturnType(throwable));
@SuppressWarnings("unchecked")
public static final Functions ReturnsDefaultValue = new Functions(
ex -> {},
returnDefaultValue
);
The TypeHelper.getReturnType(throwable)
method would get the returned type of the lambda expression, and TypeHeleper.getDefaultValue(Class)
would return system default values that would be introduced in more detail in Repositories.
Consequently, if the evaluation failed with some Exception, default value (0 for Integer in the example) would be returned:
Integer r = (Integer) Functions.ReturnsDefaultValue.apply(s -> Integer.valueOf((String)s), "33.3");
r = (Integer) Functions.ReturnsDefaultValue.apply(f8, "33.3");
Assert.assertEquals(Integer.valueOf(0), r);
Summary
To conclude the techniques discussed in this post:
- Functional Interface without "
throws Exception
" forces the business logic to handle Exceptions, that hinder the Functional Programming with Lambda in Java 8. - A set of XxxxThrowable Functional Interfaces makes it possible to define only concerned business logic as Lambda variables, that can make the code more modular, reuseable and concise.
- All Functional Interfaces could be converted to functional interfaces with
Exception
handled. - The overall behaviours of Exception Handling could be switched gracefully with
Functions.Default
instance for those business logic being executed by it. - Finally, an interesting case to use Lambda expression to capture and carry any number of input parameters.