functionExtensions 技术 1:可抛出的函数式接口





5.00/5 (6投票s)
这是三集中的第一集,旨在介绍我开源库 functionExtensions 中定义的、可抛出异常的函数式接口所使用的考虑因素和技术。
functionExtentions 是一个 Java 库,实现了可抛出异常的函数式接口、元组(Tuples)和存储库(Repositories),旨在加速 JAVA 8 的函数式编程。它已在 Maven 上发布,并实现了以下目标:
- 声明一组丰富的、可抛出异常的函数式接口,这些接口可以转换为传统的接口,并使用共享的
exceptionHandler
处理已检查的异常,从而使开发人员能够仅在 lambda 表达式中定义相关的业务逻辑。 - 实现了一个不可变的(immutable)数据结构,用于存储和检索最多 20 个强类型值,作为一组
Tuple
类。 - 提供存储库(Repositories)作为一种基于 Map 的智能实用工具,其中包含预定义的业务逻辑,用于评估给定的键或键(最多 7 个强类型值作为单个 Tuple 键)以获取相应的、最多 7 个强类型值作为单个 Tuple 值的元组,并在未发生异常的情况下进行缓存和返回。
- 多个强大的通用实用程序用于支持上述 3 种类型的实用程序,主要构建在存储库之上。例如:`
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)
` 等,它们支持原始类型和对象类型以及数组的各种组合。
本系列文章包括:
引言
函数式接口(Functional Interface)和 Lambda 表达式(Lambda Expression)的引入为将业务逻辑作为一等成员(first class members)引入 JAVA 8 打开了大门。
然而,几乎所有 Java 函数式接口方法的声明的已检查异常(checked Exception)处理契约(任何抛出任何 Java 异常的代码都需要用 try{}catch{}
包装)使得在没有样板式异常处理代码的情况下,将相关业务逻辑定义为 Lambda 表达式变得极其困难。
本文介绍了一些在我的开源项目 io.github.cruisoring.functionExtensions 中使用的技术,使 JAVA 开发人员能够更高效、更易于管理地专注于业务逻辑并处理异常。
背景
在 Java 中,Error 和 RuntimeException 类下的异常是未检查异常,所有其他可抛出(throwable)类型下的都是已检查异常。如果一个方法有任何已检查异常,那么它必须显式处理该异常,或者使用 throws 关键字指定该异常。
Java 8 中的函数式接口也不例外:如果定义的单个 abstract
方法不抛出任何异常,就像 java.util.function
包中定义的那些一样,那么 Lambda 表达式在被分配给任何函数式接口实例之前,必须正确处理任何潜在的异常。
例如,由于 Function<T,R>
定义的 apply
方法如下:
R apply(T t);
假设我想将一个 String
转换为 Class
的业务逻辑保存在 getClass
变量中,其类型为 Function<String, Class>
,一个简洁的 Lambda 表达式如下所示,但总是会遇到 "Unhandled exception: ClassNotFoundException
" 的错误:
Function<String, Class> getClass = className -> <s>Class.forName(className)</s>;
Class.forName
的签名与 Function<T,R>.apply(T t)
不匹配,后者要求您处理伴随相关业务逻辑的任何异常。
static Class<?> forName(String className) throws ClassNotFoundException
让函数式接口可抛出异常
为了应对这种不便,将相应的函数式接口方法修改为 "throws Exception
" 会将异常处理推迟到稍后阶段,并使其对 Lambda 表达式更加友好。
@FunctionalInterface
interface FunctionThrowable<T, R> extends AbstractThrowable {
R apply(T t) throws Exception;
为了使期望的操作可重用,我建议在单个 abstract
方法签名中添加 "throws Exception
",然后您将立即看到函数式接口变得更加友好。现在将 Class.forName
分配给 FunctionalThrowable
变量就可以正常工作了。
FunctionThrowable<String, Class> getClass = Class::forName;
对函数式接口进行如此简单的更改,通过将业务逻辑视为变量,使 Java 方法更易于维护和灵活。
我在尝试开发一些通用的基于 OJDBC 的 Prepared/CallableStatement 执行函数时遇到了这个问题,其签名如下:
int call(Supplier<Connection> connectionSupplier, String procSignature, Object... args)
给定不同类型的参数,由 procSignature
生成的 callableStatement
,其中包含许多 "?
" 作为占位符,将调用不同的 setXXX 或 registerXXX 方法来调用语句并正确检索返回值。
大量的 if
-else
语句不仅呈现出笨拙的代码结构,而且僵化且难以扩展。我的第一个尝试是定义一个函数式接口:
TriConsumer<PreparedStatement, Integer, Object> { void accept(T t, U u, S s) throws Exception;}
然后可以按如下方式定义一个 map:
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...;
使下面的方法能够处理所有类型的参数:
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);
}
}
不用几十个 if else()...
,主处理流程就很简单:从 Map
中选择特定类型参数的正确业务逻辑,或者选择一个默认的(如果没有定义),然后将其应用于相对的参数。尽管我通过上面的方法将异常处理推迟到了 "throws Exception
",但完全有可能使用单个 try{}catch{}
来处理 setTimestamp()
/setDate()
/setBoolean()
抛出的任何异常。
更重要的是,无需更改一行代码,就可以为特定类覆盖默认 Lambda,或者添加新方法来处理任何其他 customer
类,从而优雅地改变整体 setArgument()
的行为。
函数式接口的转换
一旦声明了这些可抛出异常的函数式接口,它们迟早需要被评估,然后必须提供异常处理代码。
我尝试将它们转换为 RunnableThrowable
(如果没有返回值)或 SupplierThrowable<R>
(如果返回类型为 R
的值),例如使用 asRunnable(...)
或 asSupplier(...)
将 TriFunctionThrowable<T,U,V,R>
转换为 SupplierThrowable<R>
,如下所示:
@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);
}
}
虽然这与我稍后将介绍的 Functions.java
中的通用 static
方法一起工作,但没有返回值的函数式接口(如 ConsumerThrowable<T>
、BiConsumerThrowable<T,U>
等)可以与 Consumer<Exception>
捆绑,从而优雅地将它们转换为非可抛出版本。例如:
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;
}
对于那些有返回值(如 SupplierThrowable<R>、FunctionThrowable<T,R>、BiFunctionThrowable<T,U,R> 等,它们都实现了空的标记接口 WithValueReturned<R>)的函数式接口,定义了两个方法将它们转换为非可抛出对应版本。
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;
}
BiFunction<Exception, WithValueReturned, R> exceptionHandler
概述了处理此类可抛出函数式接口抛出的异常所需的一切。
- 抛出的异常
- 抛出此异常的 lambda 表达式
因此,在评估任何这些可抛出函数式接口之前,可以使用共享的 BiFunction<Exception, WithValueReturned, R> exceptionHandler
将其转换为类似于正常 Java 方法的东西,并通过该 exceptionHandler
实例处理异常。例如,可以定义一个由所有可抛出函数式接口共享的复杂的 static exceptionHandler
实例:
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);
};
验证其行为:
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);
//Following statement would throw RuntimeException caused by ArrayIndexOutOfBoundsException
//result = (Integer)customFunctions.apply(new String[]{"a", "ab"}, 2, ff);
NullPointerException
或 IllegalArgumentException
s 将被忽略,而 NumberFormatException
将被记录,并且在捕获到 Exception
时,两种情况都将返回 -1
。其他意外的 Exception
,如 ArrayIndexOutOfBoundsException
,将被重新抛出为 RuntimeException
并终止应用程序。
然后,对于一个接受 2 个参数并返回 Integer 的简单函数,相关的业务逻辑将在转换为非可抛出的 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));
Functions
类定义了两个 static
实例:
ThrowsRuntimeException
:将抛出RuntimeException
,并包含函数式接口抛出的任何Exception
。ReturnsDefaultValue
:将默默地吞噬捕获到的异常,并返回 lambda 表达式预期的默认值。(对于WithValueReturned<Integer>
为0
,对于WithValueReturned<Boolean>
为false
,对于WithValueReturned<Float>
为0f
……)
这样,函数式接口就可以只定义业务逻辑,而将异常情况的处理留给一个方法。
全局更改异常处理
允许 static
Functions 实例处理 Exception
s 也使得处理流程能够优雅地改变。假设有许多地方执行业务逻辑,使用 Functions.Default
如下所示,那么根据应用程序是否通过设置系统属性 "isRelease
" 切换到 RELEASE
模式,下面的示例方法将在 RELEASE
模式下返回默认值 '0
',而在 DEBUG
模式下抛出 RuntimeException
。
@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
我遇到过一个相当迂腐的框架:Caller
类需要调用 Callee
类的业务逻辑,而 Callee
类需要多个输入参数。然而,Caller
类实例只能调用 Arquillian JUnitTestRunner 的 TestResult execute(Class<?> testClass, String methodName),该方法只接受 Callee
的 Class 和被调用方法的名称,根本没有地方容纳额外的参数。
首先,我设法将输入参数保留为 Caller
类或 Callee
类的 static
变量,以允许 JunitTestRunner
创建的 Callee
实例访问它们以相应地触发业务逻辑。然而,如果创建了多个 Caller
类来与多个 Callee
类交互,那么调用引用 Caller
/Callee
类中分散的多个 static
变量的业务逻辑,对于代码重用来说是一场噩梦。
得益于高阶函数(higher-order functions)创建的 Lambda 捕获数据的特性,这个问题可以通过以下简化伪代码来解决。首先,创建一个函数式接口,如下所示:
@FunctionalInterface
public interface InvokableStep {
void execute(Callee callee) throws Exception;
}
在 Callee
类中,创建如下代码:
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");
}
在 Caller
类中,现在可以通过直接调用 doSomething
来执行:
- 输入参数(
param1
、param2
)直接提供,此外还可以直接或间接提供junitTestRunner
。 - 创建了
InvokableStep
实例 step ,它只接受一个Callee
实例,并直接使用给定的Callee
实例(需要转换为callee
)消耗输入参数(param1
、param2
)。 - 新创建的实例
step
被推送到static
堆栈中。 - 然后
junitTestRunner
触发唯一的public
实例方法dontCallMeDirectly()
,该方法将获取在 2) 中创建的 step 实例,并立即使用该Callee
实例进行应用。 - 由于在我的情况下没有竞争条件,并且除了
Callee
实例之外的所有东西都被 Lambda 实例 step 捕获了,业务逻辑将使用所有必需的项被触发。
嗯,这个过程仍然相当曲折:Caller
类实例调用 Callee
类的 static
方法,这使得 junitTestRunner
创建一个新的 Callee
实例,并让它使用自己的 private
/public
实例资源来执行预期的进程。
但是,您可以清楚地看到,Lambda 表达式可以用新的数据创建,从而消除了许多用于传递信息以执行业务逻辑的不必要的变量。
如何使用
该项目托管在 github.com,要包含该库,请在您的 maven 项目中添加以下信息:
|
下表总结了 18 个可抛出异常的函数式接口:
可抛出异常的接口 | 输入数量 | 有返回值 | 非可抛出异常的函数 | 带处理器的转换 | 带默认值的转换 |
---|---|---|---|---|---|
RunnableThrowable | 0 | 否 | Runnable | 是 | 否 |
SupplierThrowable<R> | 0 | 是 | Supplier<R> | 是 | 是 |
ConsumerThrowable<T> | 1 | 否 | Consumer<T> | 是 | 否 |
FunctionThrowable<T,R> | 1 | 是 | Function<T,R> | 是 | 是 |
PredicateThrowable<T> | 1 | 是 | Function<T,Boolean> | 是 | 是 |
BiConsumerThrowable<T,U> | 2 | 否 | BiConsumer<T,U> | 是 | 否 |
BiFunctionThrowable<T,U,R> | 2 | 是 | BiFunction<T,U,R> | 是 | 是 |
BiPredicateThrowable<T,U> | 2 | 是 | BiFunction<T,U,Boolean> | 是 | 是 |
TriConsumerThrowable<T,U,V> | 3 | 否 | TriConsumer<T,U,V> | 是 | 否 |
TriFunctionThrowable<T,U,V,R> | 3 | 是 | TriFunction<T,U,V,R> | 是 | 是 |
QuadConsumerThrowable<T,U,V,W> | 4 | 否 | QuadConsumer<T,U,V,W> | 是 | 否 |
QuadFunctionThrowable<T,U,V,W,R> | 4 | 是 | QuadFunction<T,U,V,W,R> | 是 | 是 |
PentaConsumerThrowable<T,U,V,W,X> | 5 | 否 | PentaConsumer<T,U,V,W,X> | 是 | 否 |
PentaFunctionThrowable<T,U,V,W,X,R> | 5 | 是 | PentaFunction<T,U,V,W,X,R> | 是 | 是 |
HexaConsumerThrowable<T,U,V,W,X,Y> | 6 | 否 | HexaConsumer<T,U,V,W,X,Y> | 是 | 否 |
HexaFunctionThrowable<T,U,V,W,X,Y,R> | 6 | 是 | HexaFunction<T,U,V,W,X,Y,R> | 是 | 是 |
HeptaConsumerThrowable<T,U,V,W,X,Y,Z> | 7 | 否 | HeptaConsumer<T,U,V,W,X,Y,Z> | 是 | 否 |
HeptaFunctionThrowable<T,U,V,W,X,Y,Z,R> | 7 | 是 | HeptaFunction<T,U,V,W,X,Y,Z,R> | 是 | 是 |
声明实例比传统的更简单
BiFunctionThrowable<Integer, String, Integer> biFunctionThrowable = (i, s) -> i + Integer.valueOf(s);
在上面的例子中,尽管 Integer.valueOf(s)
可能会抛出 NumberFormatException
,但声明不需要处理它。
然后,上述 biFunctionThrowable
可以转换为 BiFunction<Integer, String, Integer>,方法是使用 BiFunction<Exception, WithValueReturned, Object> exceptionHandler
或默认值,如下所示:
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"));
得益于 类型擦除(Type Erasure),可以使用同一个 BiFunction<Exception, WithValueReturned, Object> exceptionHandler
实例来执行任何可抛出函数式接口,作为 Functions 的 ReturnDefaultValue
。
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 );
TypeHelper.getReturnType(throwable)
方法将获取 lambda 表达式的返回类型,而 TypeHeleper.getDefaultValue(Class)
将返回将在 Repositories 中详细介绍的系统默认值。
因此,如果评估因某种异常而失败,将返回默认值(在示例中是 Integer 的 0)。
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);
摘要
总结本文讨论的技术:
- 没有 "
throws Exception
" 的函数式接口迫使业务逻辑处理异常,这阻碍了 Java 8 中的函数式编程和 Lambda。 - 一组 XxxxThrowable 函数式接口使得有可能仅将业务逻辑定义为 Lambda 变量,这可以使代码更加模块化、可重用和简洁。
- 所有函数式接口都可以转换为已处理
Exception
的函数式接口。 - 对于由
Functions.Default
执行的业务逻辑,其整体异常处理行为可以通过Functions.Default
实例进行优雅切换。 - 最后,一个有趣的用例是使用 Lambda 表达式捕获和传递任意数量的输入参数。