65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2018年2月27日

CPOL

10分钟阅读

viewsIcon

15612

这是三集中的第一集,旨在介绍我开源库 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)` 等,它们支持原始类型和对象类型以及数组的各种组合。

本系列文章包括:

  1. 可抛出异常的函数式接口
  2. 元组
  3. 存储库

引言

函数式接口(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);

NullPointerExceptionIllegalArgumentExceptions 将被忽略,而 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 实例:

  1. ThrowsRuntimeException:将抛出 RuntimeException,并包含函数式接口抛出的任何 Exception
  2. 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 来执行:

  1. 输入参数(param1param2)直接提供,此外还可以直接或间接提供 junitTestRunner
  2. 创建了 InvokableStep 实例 step ,它只接受一个 Callee 实例,并直接使用给定的 Callee 实例(需要转换为 callee)消耗输入参数(param1param2)。
  3. 新创建的实例 step 被推送到 static 堆栈中。
  4. 然后 junitTestRunner 触发唯一的 public 实例方法 dontCallMeDirectly(),该方法将获取在 2) 中创建的 step 实例,并立即使用该 Callee 实例进行应用。
  5. 由于在我的情况下没有竞争条件,并且除了 Callee 实例之外的所有东西都被 Lambda 实例 step 捕获了,业务逻辑将使用所有必需的项被触发。

嗯,这个过程仍然相当曲折:Caller 类实例调用 Callee 类的 static 方法,这使得 junitTestRunner 创建一个新的 Callee 实例,并让它使用自己的 private/public 实例资源来执行预期的进程。

但是,您可以清楚地看到,Lambda 表达式可以用新的数据创建,从而消除了许多用于传递信息以执行业务逻辑的不必要的变量。

如何使用

该项目托管在 github.com,要包含该库,请在您的 maven 项目中添加以下信息:

<dependency>

    <groupId>io.github.cruisoring</groupId>

    <artifactId>functionExtensions</artifactId>

    <version>1.0.1</version>

</dependency>

下表总结了 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 实例来执行任何可抛出函数式接口,作为 FunctionsReturnDefaultValue

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 表达式捕获和传递任意数量的输入参数。
© . All rights reserved.