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 或 IllegalArgumentExceptions 将被忽略,而 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 实例处理 Exceptions 也使得处理流程能够优雅地改变。假设有许多地方执行业务逻辑,使用 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 表达式捕获和传递任意数量的输入参数。

